Download Estructuras de Datos Avanzadas

Document related concepts

Heap Binomial wikipedia , lookup

Montículo de Fibonacci wikipedia , lookup

Montículo binario wikipedia , lookup

Montículo (informática) wikipedia , lookup

Estructura de datos para conjuntos disjuntos wikipedia , lookup

Transcript
Técnicas Avanzadas de Programación
Estructuras de Datos Avanzadas
Héctor Navarro
Heaps
• Árbol esencialmente completo: si todo nodo
interno, con la posible excepción de un nodo
especial, tiene exactamente dos hijos. El
nodo especial, si existe, está situado en el
nivel K-1 y posee un hijo izquierdo, pero no
derecho. Todas la hojas están en el nivel K o
en los niveles K y K-1. Ninguna hoja de nivel K1 está a la izquierda de un nodo interno del
mismo nivel
Heaps
• Árbol esencialmente completo: si todo nodo
interno, con la posible excepción de un nodo
especial, tiene exactamente dos hijos. El nodo
especial, si existe, está situado en el nivel K-1
y posee un hijo izquierdo, pero no derecho.
Todas la hojas están en el nivel K o en los
niveles K y K-1. Ninguna hoja de nivel K-1 está
a la izquierda de un nodo interno del mismo
nivel
Heaps
• Árbol esencialmente completo: si todo nodo
interno, con la posible excepción de un nodo
especial, tiene exactamente dos hijos. El nodo
especial, si existe, está situado en el nivel K-1 y
posee un hijo izquierdo, pero no derecho.
Todas la hojas están en el nivel K o en los
niveles K y K-1. Ninguna hoja de nivel K-1 está
a la izquierda de un nodo interno del mismo
nivel
Heaps
• Árbol esencialmente completo: si todo nodo
interno, con la posible excepción de un nodo
especial, tiene exactamente dos hijos. El nodo
especial, si existe, está situado en el nivel K-1 y
posee un hijo izquierdo, pero no derecho.
Todas la hojas están en el nivel K o en los
niveles K y K-1. Ninguna hoja de nivel K-1 está
a la izquierda de un nodo interno del mismo
nivel
Heaps
• Intuitivamente es un árbol en el que los nodos
internos se han subido lo más posible en el
árbol y los nodos hoja del último nivel lo más
a la izquierda posible
Heaps
• En un árbol binario esencialmente completo
con altura K hay un nodo en el nivel 0, dos
nodos en el nivel 1, y así sucesivamente 2K-1
nodos en el nivel K-1 y al menos uno, pero no
más de 2K en el nivel K. La altura de un árbol
esencialmente completo es K=log n
Heaps
• Estos árboles pueden fácilmente almacenarse
en un arreglo. Si la raíz está almacenada en la
posición 1 del arreglo, el hijo izquierdo de un
nodo p está en la posición 2p y su hijo
derecho en la posición 2p+1
• El padre del nodo en la posición p está en p/2.
Heaps
• Un heap es un AEC, en donde la clave de cada
subárbol es mayor o igual a la clave de todos
sus hijos, y cada subárbol es también un heap.
• En un heap existe una relación de orden
parcial, a diferencia de los árboles de
búsqueda
• Esta relación existe entre cada nodo y sus
descendientes y ascendientes
Heaps
Pos
0
Valor 10
1
2
3
4
5
6
7
8
9
10
1
7
3
11
8
5
9
14
12
10
Heaps
Pos
0
Valor 10
1
2
3
4
5
6
7
8
9
10
1
7
3
11
8
5
9
14
12
10
• La posición 0 del arreglo contiene la posición
del último elemento del heap
• En un heap encontrar el elemento mayor (o
menor) siempre es de O(1)
• Suele hablarse de MaxHeap y MinHeap
Heaps
• Inserción
– Insertar el elemento en la última posición libre del
arreglo
– El elemento “flota” hasta su posición final
• Por ejemplo, insertar 2 en el heap del ejemplo
Pos 0
1
2
3
4
5
6
7
8
9
10
11
Val
1
7
3
11
8
5
9
14
12
10
2
10
Heaps
• Inserción
– Insertar el elemento en la última posición libre del
arreglo
– El elemento “flota” hasta su posición final
• Por ejemplo, insertar 2 en el heap del ejemplo
Pos 0
1
2
3
4
5
6
7
8
9
10
11
Val
1
7
3
11
8
5
9
14
12
10
2
11
p=11. ppadre = 11/2 = 5
Heaps
• Inserción
– Insertar el elemento en la última posición libre del
arreglo
– El elemento “flota” hasta su posición final
• Por ejemplo, insertar 2 en el heap del ejemplo
Pos 0
1
2
3
4
5
6
7
8
9
10
11
Val
1
7
3
11
2
5
9
14
12
10
8
11
p=5. ppadre = 5/2 = 2
Heaps
• Inserción
– Insertar el elemento en la última posición libre del
arreglo
– El elemento “flota” hasta su posición final
• Por ejemplo, insertar 2 en el heap del ejemplo
Pos 0
1
2
3
4
5
6
7
8
9
10
11
Val
1
2
3
11
7
5
9
14
12
10
8
11
p=2. ppadre = 2/2 = 1
Heaps
• Encontrar la siguiente posición libre del heap:
O(1)
• Flotar el elemento: O(log n)
• Inserción: O(1)+O(log n) = O(log n)
Heaps
• Eliminación
– Siempre se elimina el elemento de la raíz
– El último elemento se coloca de primero
– El elemento se hunde hasta su posición final
• Por ejemplo, eliminar en el heap anterior
Pos 0
1
2
3
4
5
6
7
8
9
10
11
Val
1
2
3
11
7
5
9
14
12
10
8
11
Heaps
• Eliminación
– Siempre se elimina el elemento de la raíz
– El último elemento se coloca de primero
– El elemento se hunde hasta su posición final
• Para hundir se compara con ambos hijos. Se
intercambia con el mayor (menor)
Pos 0
1
2
3
4
5
6
7
8
9
10
11
Val
8
2
3
11
7
5
9
14
12
10
--
10
p=1. HI: 1*2 = 2. HD: 1*2+1 = 3
Heaps
• Eliminación
– Siempre se elimina el elemento de la raíz
– El último elemento se coloca de primero
– El elemento se hunde hasta su posición final
• Para hundir se compara con ambos hijos. Se
intercambia con el mayor (menor)
Pos 0
1
2
3
4
5
6
7
8
9
10
11
Val
2
8
3
11
7
5
9
14
12
10
--
10
p=2. HI: 2*2 = 4. HD: 2*2+1 = 5
Heaps
• Eliminación
– Siempre se elimina el elemento de la raíz
– El último elemento se coloca de primero
– El elemento se hunde hasta su posición final
• Para hundir se compara con ambos hijos. Se
intercambia con el mayor (menor)
Pos 0
1
2
3
4
5
6
7
8
9
10
11
Val
2
7
3
11
8
5
9
14
12
10
--
10
p=5. HI: 5*2 = 10. HD: 5*2+1 = 11
Heaps
• Heapify: convertir un arreglo de elementos
comparables en un heap:
heapify(T[0..N])
{
for(i = N/2; i>0; i--)
hundir(T, i);
}
Heaps
• Heapify: convertir un arreglo de elementos
comparables en un heap:
heapify(T[0..N])
{
for(i = N/2; i>0; i--)
hundir(T, i);
}
7
2
5
1
8
9
5
3
6
4
Heaps
• Heapify: convertir un arreglo de elementos
comparables en un heap:
heapify(T[0..N])
{
for(i = N/2; i>0; i--)
hundir(T, i);
}
7
2
5
1
8
9
5
3
6
4
Heaps
• Heapify: convertir un arreglo de elementos
comparables en un heap:
heapify(T[0..N])
{
for(i = N/2; i>0; i--)
hundir(T, i);
}
7
2
5
1
8
9
5
3
6
4
Heaps
• Heapify: convertir un arreglo de elementos
comparables en un heap:
heapify(T[0..N])
{
for(i = N/2; i>0; i--)
hundir(T, i);
}
7
2
5
8
1
9
5
3
6
4
Heaps
• Heapify: convertir un arreglo de elementos
comparables en un heap:
heapify(T[0..N])
{
for(i = N/2; i>0; i--)
hundir(T, i);
}
7
2
6
8
1
9
5
3
5
4
Heaps
• Heapify: convertir un arreglo de elementos
comparables en un heap:
heapify(T[0..N])
{
for(i = N/2; i>0; i--)
hundir(T, i);
}
7
9
6
8
1
3
5
2
5
4
Heaps
• Heapify: convertir un arreglo de elementos
comparables en un heap:
heapify(T[0..N])
{
for(i = N/2; i>0; i--)
hundir(T, i);
}
O(n log n)
9
8
6
7
1
3
5
2
5
4
Heaps
• HeapSort
HeapSort(T[0..N])
{
Heapify(T);
for(i = N; i > 1; i--)
{
swap(T[1], T[i]);
T[0]--;
hundir(T, 1);
}
}
Heaps
• HeapSort
HeapSort(T[0..N])
{
Heapify(T);
for(i = N; i > 1; i--)
{
swap(T[1], T[i]);
T[0]--;
hundir(T, 1);
}
}
7
2
5
1
8
9
5
3
6
4
Heaps
• HeapSort
HeapSort(T[0..N])
{
Heapify(T);
O(N log N)
for(i = N; i > 1; i--)
{
swap(T[1], T[i]);
T[0]--;
hundir(T, 1);
1
}
}
9
8
6
7
3
5
2
5
4
Heaps
• HeapSort
HeapSort(T[0..N])
{
Heapify(T);
for(i = N; i > 1; i--)
{
swap(T[1], T[i]); O(1)
T[0]--;
hundir(T, 1);
}
}
9
8
6
7
1
3
5
2
5
4
Heaps
• HeapSort
HeapSort(T[0..N])
{
Heapify(T);
for(i = N; i > 1; i--)
{
swap(T[1], T[i]);
T[0]--; O(1)
hundir(T, 1);
}
}
2
8
6
7
1
3
5
9
5
4
Heaps
• HeapSort
HeapSort(T[0..N])
{
Heapify(T);
for(i = N; i > 1; i--)
{
swap(T[1], T[i]);
T[0]--;
hundir(T, 1); O(log N)
}
}
2
8
6
7
1
3
5
9
5
4
Heaps
• HeapSort
HeapSort(T[0..N])
{
Heapify(T);
for(i = N; i > 1; i--)
{
swap(T[1], T[i]);
T[0]--;
hundir(T, 1);
}
}
8
7
6
5
1
3
2
9
5
4
Heaps
• HeapSort
HeapSort(T[0..N])
{
Heapify(T);
for(i = N; i > 1; i--)
{
swap(T[1], T[i]);
T[0]--;
4
hundir(T, 1);
7
8
}
}
O(N log N) + O(N)*(O(1) +O(1) +O(log N))=
O(N log N) + O(N)*O(log N)=
O(N log N) + O(N log N) = O(N log N)
1
2
3
5
9
5
6
ANÁLISIS AMORTIZADO
Análisis Amortizado
• En análisis de complejidad muchas veces se
toma el peor caso como referencia para
estudiar el comportamiento de un algoritmo
• Algunas veces esto no se hace ya que el peor
caso es mucho peor que el caso promedio
• Por ejemplo, QuickSort
Análisis Amortizado
• Cuando se analizan las operaciones de
estructuras de datos muchas veces no basta
tomar el peor caso sino ver el
comportamiento a lo largo del tiempo
• Un ejemplo propuesto por Cormen propone la
operación Multipop(p,k) que realiza k
operaciones de pop sobre una pila p
Análisis Amortizado
• Si analizamos una llamada aislada a Multipop
veremos que es de O(K)
• Ahora analicemos el comportamiento de N
llamadas a Multipop
• La primera llamada es de O(K), suponiendo
K>=S (tamaño de la pila)
• Las siguientes llamadas serán todas de O(1)
Análisis Amortizado
• Finalmente la complejidad promedio será:
N
O( S )   O(1)
i 2
N

O( S )  O( N )
 O(1)
N
Ya que podemos suponer que hay suficientes
llamadas a Multipop para que N>S
• Este tipo de análisis se llama análisis amortizado
ya que una de las operaciones está “comprando a
crédito” el costo de otras operaciones
Análisis Amortizado
• Existen diversas técnicas para hacer análisis
amortizado. Estudiaremos el método del
potencial
• Suponemos una estructura de datos inicial D0
sobre la que se realizan n operaciones
• ci es el costo de aplicar la operación sobre la
estructura Di-1 y obtener Di
Análisis Amortizado
• Existe una función de potencial φ que mapea
cada estructura de datos Di a un número real
φ(Di).
• El potencial puede irse acumulando hasta que
en algún momento el potencial es liberado
• El costo amortizado de hacer la operación iésima se calcula como:
Análisis Amortizado
• El costo total de hacer n operaciones será:
Análisis Amortizado
• Para el ejemplo de la pila, podemos definir φ
como el número de elementos de la pila
• SI la pila está vacía, el potencial es 0
• A medida en que se incrementan el número
de elementos de la pila, el potencial se
incrementa, hasta “liberarlo” llamando a
multipop
Análisis Amortizado
• El costo amortizado de la operación push será:
• Para la operación multipop(p,k) con
k’=min(k,s), en donde s es el número de
elementos en la pila, k’ elementos serán
sacados de la pila. El costo de la operación es
k’
Análisis Amortizado
• Podemos concluir entonces que el costo
amortizado de la operación de multipop es de
O(1).
• Luego, hacer n operaciones arbitrarias será de
O(n) en promedio
HEAPS DE FIBONACCI
Heaps de Fibonacci
• Tienen la ventaja de que las operaciones en
donde no se eliminan elementos son de O(1)
• Sumamente útiles cuando el número de
operaciones de eliminación son pocas en
comparación a las demás operaciones
• En particular el algoritmo de Dijkstra es
sumamente eficiente con esta
implementación
Heaps de Fibonacci
• Un heap de Fibonacci es una colección de
árboles ordenados
Heaps de Fibonacci
• Representación
Heaps de Fibonacci
• Nótese el apuntador al mínimo elemento del
heap (min[H])
• En la representación se usan listas circulares
para hacer todos los nodos iguales
• Todo nodo tiene un apuntador a algún hijo
(cualquiera) y a su padre
Heaps de Fibonacci
• Las listas circulares tienen como ventaja que
se pueden eliminar nodos con O(1)
• Dos listas circulares pueden concatenarse con
O(1)
Heaps de Fibonacci
struct NodoHeapFib{
NodoHeapFib *p, *h;
NodoHeapFib *izq, *der;
int k;
int grado;
bool marca;
};
Heaps de Fibonacci
struct NodoHeapFib{
NodoHeapFib *p, *h;
NodoHeapFib *izq,*der;
int k;
Almacena la clave del nodo
int grado;
bool marca;
};
Heaps de Fibonacci
struct NodoHeapFib{
NodoHeapFib *p, *h;
NodoHeapFib *izq,*der;
int k;
Indica el grado del nodo
int grado;
(número de hijos)
bool marca;
};
Heaps de Fibonacci
struct NodoHeapFib{
NodoHeapFib *p, *h;
NodoHeapFib *izq,*der;
int k;
int grado;
Indica si un nodo ha
bool marca;
perdido algún hijo desde la
};
última vez que se convirtió
en hijo de otro nodo.
Inicialmente es falso
Heaps de Fibonacci
• Es importante el número de nodos n en un
heap de Fibonacci
• Luego veremos que el grado máximo de
cualquier heap de Fibonacci con n nodos es
D(n) = O(log n)
Heaps de Fibonacci – Función de
potencial
• Cada árbol en la raíz del heap suma un punto
a la función de potencial, ya que la extracción
del valor mínimo trabaja sobre el número de
árboles
• También hay que tomar en cuenta el número
de nodos ya marcados, ya que la operación de
decrementar clave debe recorrer todos los
nodos marcados
Heaps de Fibonacci – Función de
potencial
• En donde T(Di) es el número de árboles en la
raíz del heap y m(Di) es el número de nodos
marcados
• Para evitar problemas numéricos se utiliza:
Heaps de Fibonacci - Operaciones
• Creación: hacer n=0 y min[H] = NULL
• Inserción: para insertar un nodo x en un heap
H, primero iniciar los valores del nodo:
x.p = NULL;
x.h = NULL;
x.izq = x.der = x;
x.grado = 0;
x.marca = false;
Heaps de Fibonacci - Operaciones
• Enlazar el nodo x con la lista de árboles de H:
min[H].izq.der = x;
min[H].izq = x;
if(min[H]==NULL || x.clave < min[H].clave)
min[H] = x;
Heaps de Fibonacci - Operaciones
• Valor mínimo: es trivial ya que se tiene el
apuntador min[H]
• Unión: La unión de dos heaps consiste en
concatenar las listas de los heaps H1 y H2, y
buscar el elemento mínimo. Ambas
operaciones son de O(1) ya que hay sólo dos
candidatos a ser el mínimo: min[H1] y min[H2]
Heaps de Fibonacci - Operaciones
• Extracción del nodo mínimo: es la operación
más complicada. Primero se localiza el nodo
mínimo mediante min[H]. Todos sus hijos
pasan directamente a la raíz.
Heaps de Fibonacci - Operaciones
• El siguiente paso es consolidar el bosque para
reducir el número de árboles en la raíz de H.
Esto lo hace la función consolidar(H):
1. Encontrar dos nodos x, y en la raíz con el mismo
grado (x.k < y.k)
2. Enlazar y a x, removiendo y de la raíz y
poniéndolo como hijo de x. El grado de x se
incrementa y la marca de y se pone en falso
Heaps de Fibonacci - Operaciones
• Se hace uso de un arreglo auxiliar A de
apuntadores a nodos tal que A[i] apunta al
último nodo de grado i encontrado (o a NULL
si no se ha encontrado). Puede demostrarse
que el tamaño de A nunca será mayor a log(n)
Heaps de Fibonacci - Operaciones
Heaps de Fibonacci - Operaciones
• Decremento de una clave: para decrementar
el valor de un nodo x en un heap H primero es
necesario comprobar que efectivamente el
nuevo valor de la clave es menor que el actual
• Luego se compara el nuevo valor con el valor
del padre de x (y)
• Si x es menor se remueve de la lista de hijos
de y y se pone en la raíz de H (poniendo la
marca de x en falso)
Heaps de Fibonacci - Operaciones
• Ahora el nodo y debe marcarse ya que perdió
a un hijo
• En caso de que y ya hubiera sido marcado
previamente, hay que llevar al nodo y a la raíz
y repetir el proceso (marcar al padre de y)
Heaps de Fibonacci - Operaciones
• Eliminación de nodos: para eliminar un nodo
x de un HF H simplemente se decrementa la
clave de x hasta -∞ y luego extraemos el
mínimo nodo de H.
Complejidad – Decremento del
mínimo
• Si antes de hacer la operación habían T(Hn-1)
árboles, ahora habrán T(Hn-1)+m’(Hn-1)
• En donde m’(H) es cuántos nodos marcados
seguidos hay en el heap H
Complejidad – Decremento del
mínimo
• Ahora, el costo de una operación de
decremento de mínimo será:
• El nuevo heap tendrá ahora m(Hn) nodos
marcados:
Complejidad – Decremento del
mínimo
• Luego:
CONJUNTOS DISJUNTOS
Conjuntos Disjuntos
• Útiles para el procesamiento de conjuntos o
clases de equivalencia para responder
preguntas del tipo ¿es x equivalente a y?
• Se definen dos tipos de operaciones básicas:
unión y pertenencia
Conjuntos Disjuntos
• La estructura de datos a usar es un bosque
• La unión equivale a unir dos árboles
• La pertenencia equivale a conocer si dos
elementos se encuentran en el mismo árbol
Conjuntos Disjuntos
• En un comienzo cada árbol del bosque
contiene a un solo elemento. Se supone que al
comienzo cada elemento pertenece a un único
conjunto
• Haciendo operaciones de unión se forman
árboles más complejos
Conjuntos Disjuntos
{A} {B} {C} {D} {E} {F} {G}
Conjuntos Disjuntos
• unión(D,G) genera:
{A,D,G} {B} {C} {D} {E} {F}
Conjuntos Disjuntos
• unión(A,G) genera:
{A,D,G} {B} {C} {E} {F}
Conjuntos Disjuntos
• unión(B,F) genera:
{A,D,G} {B,F} {C} {E}
Conjuntos Disjuntos
• unión(E,F) genera:
{A,D,G} {B,F,E} {C}
Estructura de Datos
• Para el tipo de operaciones que soportan los
conjuntos disjuntos no es necesario tener
apuntadores a los hijos, sino a los padres
• ¿Cuándo dos elementos están en el mismo
conjunto?
Estructura de Datos
• Se puede usar un arreglo de índices del nodo
padre
• Este arreglo lo llamaremos padre
• Cuando padre[i]=0, significa que ese nodo
es la raíz del árbol
Estructura de Datos
1
i
2
3
4
5
6
7
1
2
3
4
5
6
7
padre[i] 0
0
0
0
0
0
0
Estructura de Datos
1
3
i
7
2
6
4
5
1
2
3
4
5
6
7
padre[i] 0
1
2
2
2
1
0
Unión pertenencia
• Unión-pertenencia: la operación de
pertenencia busca la raíz de los árboles en
donde están ambos elementos. Si la raíz
coincide retorna verdad. En caso contrario
falso. El parámetro unión indica si los
conjuntos deben unirse
Unión pertenencia
int pertenencia(int x, int y, int union)
{
int i=x, j=y;
while(padre[i]>0) i=padre[i];
while(padre[j]>0) j=padre[j];
if (union && (i!=j)) padre[j]=i;
return i!=j;
}
Unión pertenencia
• La implementación es muy sencilla, aunque su
comportamiento en el peor caso es muy malo.
¿cuál es la complejidad en el peor caso?
• La complejidad depende de la altura del árbol.
Para mantener la complejidad en lo mínimo la
idea es mantener siempre el árbol con altura
mínima
Unión pertenencia
1
1
2
6
2
3
3
4
5
7
4
5
7
6
Unión pertenencia
• Cuando se unen dos árboles, uno queda como
raíz, y el otro baja un nivel. Para minimizar la
altura del árbol, parece lógico que quede
como raíz el árbol con mayor cantidad de
descendientes
Unión pertenencia
• Con este fin se debe mantener siempre la
información sobre la cantidad de
descendientes de cada nodo raíz
• Para evitar el uso de otro arreglo, en lugar de
usar 0 para nodos raíces, se coloca un número
negativo cuyo valor absoluto es igual al
número de descendientes de este nodo
Unión pertenencia
1
2
5
6
4
3
7
i
1
padre[i] -3
2
3
4
5
6
7
1
5
5
-2
1
6
Compresión de caminos
• Otra idea consiste en que cada vez que se
hace una operación de unión pertenencia,
todos los nodos visitados se colocan como
hijos directos de la raíz del árbol respectivo
Compresión de caminos
1
2
5
6
4
7
8
10
3
9
union-pertenencia(7, 10)
Compresión de caminos
1
2
5
6
4
7
8
10
3
9
union-pertenencia(7, 10)
Compresión de caminos
1
2
5
6
4
7
8
10
3
9
union-pertenencia(7, 10)
Compresión de caminos
1
2
5
6
7
4
10
8
9
union-pertenencia(7, 10)
3
Compresión de caminos
int pertenencia(int x, int y, int union)
{
int i=x, j=y;
while(padre[i]>0) i=padre[i]; // buscar la raiz de este arbol
while(padre[j]>0) j=padre[j]; // y de este tambien
while(padre[x]>0) {t=x; x=padre[x]; padre[t]=i;}
while(padre[y]>0) {t=y; y=padre[y]; padre[t]=j;}
if (union && (i!=j))
{
if (padre[j] < padre[i])
{
padre[j]+=padre[i]-1; padre[i]=j;
} else
{
padre[i]+=padre[j]-1; padre[j]=I;
}
}
return i!=j;
}
Complejidad en tiempo
• No es fácil analizar la complejidad en tiempo
de la operación de unión-pertenencia
• Tarjan demostró que realizar una operación de
u-p se ejecuta en tiempo proporcional a
log*(n) – logaritmo iterado de n (¿cuántas
veces hay que aplicarle logaritmo a n para
llegar a 1?)
• Como log*(n) crece tan lentamente, en la
práctica se considera una operación de O(1)
TABLAS HASH
Tablas Hash
• Un método para hacer referencia
directamente a los registros en una tabla por
medio de transformaciones sobre la clave para
obtener direcciones en la tabla
• Si se sabe que las claves son enteros distintos
entre 0 y N-1, puede almacenarse la clave i en
la posición i-ésima
Tablas Hash
• La dispersión o hashing es una generalización
de este método en aplicaciones en donde no
se tiene ningún conocimiento concreto sobre
los valores de las claves
Tablas Hash
• Una búsqueda por dispersión
consiste de dos pasos:
• Evaluar la función de dispersión
• Resolver las colisiones
Funciones de dispersión
• Se necesita una función que transforme claves
(habitualmente enteros o cadenas cortas de
caracteres) en direcciones de la tabla en el
intervalo [0, M-1]
• M es el número de registros que se pueden
calcular en función a la memoria disponibe
Funciones de dispersión
Una función de dispersión debe:
• Ser fácil de calcular
• Ser una aproximación de una función
“aleatoria”, que para cada entrada toda salida
sea igualmente probable
Funciones de dispersión
• El primer paso consiste en transformar la clave
en un número entero para poder hacer
operaciones aritméticas sobre él
• Para claves pequeñas esto es trivial, usando la
representación binarias internas de los
caracteres
• Para representaciones mayores, extraer
algunos bits de la clave y empaquetarlos
Funciones de dispersión
• El segundo paso consiste en tomar éste
número y aplicarle alguna transformación
para llevarlo al intervalo [0,M-1]. La forma
más sencilla de realizar esto consiste en
seleccionar M primo y para cualquier clave k
aplicar la transformación h(k)=k mod M
Funciones de dispersión
• Supongamos por ejemplo la clave “CLAVE” y
M=101. Obteniendo la representación:
0001101100000011011000101
• Equivalente al número:
3540677 en base 10
• Luego, 3540677 mod 101 = 21, por lo que a la
palabra “CLAVE” le correspondería la posición
21 de la tabla
Funciones de dispersión
• Hay muchas claves posibles, y relativamente
pocas posiciones en la tabla, por lo que a
muchas claves le corresponden la misma
posición
• Si la clave es muy grande, por ejemplo
“CLAVEGRANDE” obtenemos un número no
representable en una palabra, por lo que hay
que buscar alternativas
Manejo de colisiones
• Encadenamiento separado: cada entrada de
la tabla contiene una lista con todos los
elementos que fueron mapeados a esa
entrada. Para buscar un elemento es
necesario recorrer toda la lista hasta
encontrarlo o determinar que no está
Manejo de colisiones
• Exploración lineal (M>N): si hay una colisión
se busca en la próxima entrada libre en la
tabla
• Cuando se está buscando un elemento, se
busca hasta encontrarlo, o hasta llegar a una
posición vacía de la tabla
• Al llegar a una posición vacía de la tabla,
significa que el elemento no está en la tabla
Funciones hash
Cuando se va a aplicar una función hash a un
string es necesario decidir cuales caracteres
tomar:
• Todos los caracteres
• Caracteres al final o en el medio
Métodos para strings
• Método de adición: sumar los valores de los
caracateres del string y al final aplicar el
módulo de ser necesario
unsigned char hash(char *str) {
unsigned char h = 0;
while (*str) h += *str++;
return h;
}
Métodos para strings
• Método del XOR: se aplican XORs exclusivos
sobre los caracteres del string y al final aplicar
el módulo de ser necesario
unsigned char hash(char *str) {
unsigned char h = 0;
while (*str) h ^= *str++;
return h;
}
Métodos para strings
• Método de la multiplicación: Cuando M=2N.
La clave es multiplicada por una constante y
luego se extraen los bits necesarios para
indexar la tabla. Knuth recomienda usar el
radio dorado para encontrar esta constante
(Radio dorado)
• Es la proporción ϕ tal que la suma de dos
números es al mayor de estos lo que el mayor
es al menor. Esto es, si a>b:
Métodos para strings
• Por ejemplo si la tabla tiene M=32 entradas, y
se utilizará una tabla de 8 bits para indexarlo,
el multiplicador será 28ϕ≈158
• Luego, dada cualquier clave K, lo primero que
se hace es mutiplicarla por 158. El resultado
tendrá 16 bits, y se toman los 5 bits mas
significativos del byte menos significativo
Métodos para strings
• Por ejemplo, para K=101, tenemos
158K=158*101=15958=(0011111001010110)2
• Los 5 bits mas significativos del byte menos
significativo son (01010) 2=10
• Luego, a la clave 101 le corresponde la
entrada 10 de la tabla
Métodos para strings
• Método aleatorio: Introduce una introducción
aleatoria para intentar dispersar mejor los datos:
unsigned char rand8[256];
unsigned char hash(char *str) {
unsigned char h = 0;
while (*str != NULL) h = rand8[h ^ *str++];
return h;
}
rand8 contiene los 256 caracteres barajados aleatoriamente
hashpjw
• P. J. Weinberger’s Compiler: función
implementada por Weinberger para usarla en
tablas de símbolo de compiladores, es todavía
sumamente usadada
hashpjw
#define M 211
int hashpjw(char *s)
{
char *p;
unsigned h=0, g;
for (p=s; *p!=0; p++)
{
h = (h << 4) + *p;
if (g=h & 0xf0000000)
{
h=h^(g>>24);
h=h^g;
}
}
return h % M;
}
Evaluación de tablas hash
• Para evaluar que tan buena es una tabla hash
en comparación a otra una idea es medir el
tamaño promedio de las listas en cada entrada
de la tabla
• Esto, no funciona, ya que puede observarse
que este promedio será siempre igual para el
mismo número de elementos
Evaluación de tablas hash
• Otra idea es medir el número total de accesos
necesarios para visitar todas las claves de la
tabla
• Suponiendo que el número de elementos en
la entrada i de la tabla es Bi, es fácil ver que
para acceder a todos los elementos de esa
entrada se necesitan:
Evaluación de tablas hash
• Luego, para visitar todos los elementos de la
tabla hash, se necesitarían:
BINARY INDEXED TREE O
FENWICK TREE
¿Qué son?
• Es una estructura de datos que provee
métodos eficientes para el cálculo y
manipulación de frecuencias y frecuencias
acumulativas
• Supongamos que tenemos un arreglo de n
posiciones y podemos sumarle uno a
cualquier posición del arreglo o conocer la
suma de los valores del arreglo entre dos
índices k,l
¿Qué son?
• Una primera aproximación al problema
permite realizar la operación de suma en O(1)
y la operación de consulta en O(n)
• Si realizamos m operaciones de consulta
tenemos O(n*m)
• Usando árboles de Fenwick la consulta es de
O(m log n)
Notación
• f[i]: frecuencia del elemento i. Por ejemplo, si
f[4]=7 significa que el 4 se repite 7 veces
• c[i]: frecuencia acumulada. c[i]=f[1]+…+f[i]
• MaxVal: Valor máximo que tendrá fecuencia
mayor a 0
• tree[i]: suma de frecuencias almacenadas en
el árbol
Notación
• tree[i] almacena la suma de frecuencias entre
i0 y i1 en donde:
• r es la posición del bit menos significativo
encendido de i. Por ejemplo, si i=12 (1100), r
será 2 (de derecha a izquierda el primer bit
está en la posición 0)
• i0 = i-2^r+1
• i1 = i
Notación
Luego, para tree[12] tenemos i0=9, i1=12
• tree[12] = f[9] + f[10] + f[11] + f[12]
Calcular:
• tree[6]
• tree[8]
• tree[3]
Ejemplo
18
10
2
4
1
5
1
1
1
3
2
3
1
2
1
0
i
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
f
1
0
1
0
3
2
2
1
1
0
2
1
1
2
0
1
c
1
1
2
2
5
7
9
10
11 11
13
14
15
17
17
18
Ejemplo
• Para calcular por ejemplo c[14]:
c[14] = tree[14]+tree[12]+tree[8]
• Para calcular por ejemplo c[9]:
c[9] = tree[9]+tree[8]
Obtener el bit 1 menos significativo
• Sea ~x el complemento de un número
• Recordemos que cualquier número x puede
escribirse como a1b en donde b son todos bits
en 0
• Por ejemplo, para 101000:
– a = 10
– b = 000
Obtener el bit 1 menos significativo
• Luego, recordemos que –x = ~x + 1
~x + 1 = ~(a1b)+1 = ~(a)0~(b) + 1 =
~(a)0~(0...0)+1 = ~(a)01…1 + 1 = ~(a)10…0
= ~(a)1b
• Luego, el bit encendido menos significativo
puede calcularse con x & (-x)
Frecuencia acumulada
• Si deseamos calcular la frecuencia acumulada
en un índice i, primero debemos sumar tree[i].
• Luego si i tiene la forma a1b tenemos que
tree[i] almacena f[a0b+1]+…+f[i]
• Resta calcular f[1]+…+f[a0b]
• Es decir, ahora debemos calcular la frecuencia
acumulada en el índice i-(i&-i)
Frecuencia acumulada
int acum(int idx)
{
int sum = 0;
while (idx > 0)
{
sum += tree[idx];
idx -= (idx & -idx);
}
return sum;
}
Modificar el valor de una frecuencia
• Supongamos que se desea modificar la
frecuencia de f[i]
• Primero debemos modificar el valor en tree[i]
• Suponiendo que i=a1b
• Si hacemos a1b+1b obtenemos:
i’=(a+1)0b
• tree[i’]=f[(a+1)0b-2^r+1]+…+f[(a+1)0b]
• Por lo tanto tree[i’] también contiene a f[i]
Modificar el valor de una frecuencia
void update(int idx ,int val)
{
while (idx <= MaxVal)
{
tree[idx] += val;
idx += (idx & -idx);
}
}