Tablas Hash - Universidad Austral

Anuncio
Tablas de Dispersión (Hash Tables)
Introducción: Algoritmos de Búsqueda
Un algoritmo de búsqueda consiste en buscar un elemento k en un conjunto A. El conjunto A está formado
por elementos del mismo tipo y cada elemento, a su vez, está constituido por uno o más atributos. El atributo
que identifica unívocamente a un elemento se lo denomina clave (key). En un conjunto no puede haber dos
elementos con la misma clave.
Ejemplos
Conjunto
Elementos
Atributos de los elementos
Clave
Curso
Estudiantes
Matricula, apellido, nombre, año que cursa
Matricula
Biblioteca
Libros
Código, titulo, autor, tema
Código
Banco
Clientes
Número de cuenta corriente, apellido, nombre, Número
saldo
corriente
de
cuenta
Formalmente el problema de búsqueda es:
Dada una clave k y un conjunto A de elementos del
mismo tipo, verificar si existe algún elemento en A
cuya clave sea k.
Cuando ningún atributo sirve de clave, se puede definir la clave como la concatenación de dos o más
atributos. Por ejemplo, en la Universidad la clave de cada estudiante es facultad_matricula; las cuentas
corrientes en un banco en general se identifican como nroDeSucursal_nroDeCtaCte.
Debido a que la búsqueda es el algoritmo básico de la consulta, ésta debe ser lo más óptima posible en cuanto
al tiempo. Pero tampoco podemos permitir un algoritmo sumamente ineficiente en espacio ni algoritmos
demasiados lentos para las otras operaciones vinculadas con la administración de la información que son el
insertar nuevos elementos (altas), eliminar elementos (bajas) y la generación de reportes ordenados.
Entonces cabe preguntarse cómo guardar la información para obtener algoritmos de máximo rendimiento. La
forma más eficiente en cuanto al tiempo es que la clave k sea un número entero mayor o igual a cero. La
información se almacena en un arreglo A de manera que el elemento de clave k se guarda en A[k].
Conjunto de elementos
Tabla de acceso directo
.
.
k
i
I
.
.
i
j
m
J
.
J
.
k
K
.
Alicia Gioia
1/23
Tablas de Dispersión
Para recuperar el elemento de clave, tendremos simplemente que retornar A[k].Si la información es mucha
y no cabe en memoria RAM, en lugar de un arreglo se utiliza un archivo, y tendremos que recuperar el
registro k del archivo.
Igualmente sencillos son los algoritmos de inserción y eliminación, ambos de O(1), como también la
generación de un informe ordenado, siendo éste de O(n).
EJERCICIO 1
a) Escribir las funciones para insertar y eliminar elementos de un conjunto de datos y la función
para obtener un reporte ordenado considerando que los elementos se almacenan de tal forma que
el elemento de clave k se guarda en la posición k de un arreglo.
Inconvenientes de esta representación
1. Mal uso del espacio.
Supongamos que se desea asignar como clave para los integrantes de un curso de 30 alumnos al número de su
DNI. El número de DNI más bajo es 25.678.901 y el más alto 27.234.567. La diferencia entre ambos es
1.555.666. Si al alumno de DNI número 25.678.901 lo almacenamos en la posición cero del arreglo al alumno
de DNI número 27.234.567 lo deberíamos almacenar en la posición 1.555.666. O sea que habría que definir
un arreglo o archivo de 1.555.666 elementos sólo para guardar 30. ¡Absurdo!.
2. Elección de claves.
Si bien las claves enteras son muy cómodas para el profesional de sistemas, no lo son para el usuario del
mismo. Supongamos una fábrica que confecciona 10 modelos de ropa. Cada modelo se hace en 7 colores y en
6 talles, esto da 420 prendas. Podríamos numerarlas como 1, 2, ...., 420 pero para el usuario es mucho más
difícil recordar que 300 es el pullover de cuello V de color celeste talle 46 que si la clave fuera, por ejemplo:
P V C 4 6
P: pullover, V: cuello en V, C: color celeste, 46: número del talle
En general, es deseable que las claves sean cadenas de caracteres no demasiadas largas y fáciles de recordar
para el usuario del sistema.
Una forma de evitar estos inconvenientes es almacenar la información en una lista. Una lista puede ser
desordenada u ordenada según la clave y además puede estar implementada en forma estática (en un arreglo)
o en forma dinámica (listas encadenadas).
En la siguiente tabla se muestran los órdenes de los algoritmos básicos en las distintas implementaciones.
Lista
Desordenada
Algoritmos
Estática
búsqueda
O(n)
inserción
O(1)
eliminación
O(n)
reportes ordenados
(#)
O(nlog2n)
Ordenada
Dinámica
(*)
Dinámica
O(n)
O(log2n)
O(1)
O(n)
O(1)
O(n)
O(1)
O(n)
O(n)
O(1)
(+)
Estática
O(nlog2n)
(+)
(*)
O(n)
resulta de aplicar búsqueda binaria
resulta de aplicar el algoritmo de quicksort para ordenar previamente los dados
(#)
resulta de agregar al final
El menor tiempo de la operación de búsqueda se obtiene en la lista ordenada implementada en forma estática
pero esta implementación es ineficiente para los algoritmos de inserción y eliminación frente a las otras
implementaciones. Lo que buscamos son algoritmos eficientes en tiempo y espacio tanto para las búsquedas
(+)
Alicia Gioia
2/23
Tablas de Dispersión
como también para las inserciones, eliminaciones y generación de reportes ordenados. En un árbol binario de
búsqueda equilibrado tenemos que las operaciones de inserción, eliminación y recuperación son del orden
logarítmico, pero es costoso tener el árbol equilibrado y cuando la información es mucha un orden logarítmico
puede ser ineficiente.
Una forma de almacenar la información para lograr todos los requisitos anteriores, excepto la generación de
reportes ordenados, son las Tablas de Dispersión (Hash Tables).
Definiciones y Conceptos Básicos
Supongamos que el conjunto de claves posibles, denominado de ahora en más espacio de claves, sea el
conjunto de números enteros no negativos, N. Supongamos también que tenemos una función h: N  0, 1, ...
m-1 para algún m  1.
Sea T un arreglo o un archivo de m posiciones donde en cada posición se encuentra la información relativa a
un elemento o está vacía. Podemos representar un conjunto A de elementos poniendo
T[j] = x
para todo x  A, con k = clave(x) y j = h(k).
Conjunto de elementos
Tabla de dispersión
.
.
i
K1
K2
.
.
K2
J
m
donde
i = h(k2)
j = h(k3)
k =h(k1)
K3
.
.
K3
k
K1
.
Ejemplo
Sean m = 13, el conjunto de elementos A y el espacio de claves C:
A = {(22, “José”, 1), (39, “Mario”, 1), (46, “María”, 1), (54, “Juan”, 2 ), (79, “Silvia”, 4 ), (198, “Martin”, 3)}
C = {22, 39, 46, 54, 79, 198}
Función h(k) = k % 13
Aplicando la función h(k) a cada clave, resulta:
h(22)
=
22 % 13 = 9
h(39)
=
39 % 13 = 0
h(46)
h(54)
=
=
46 % 13 = 7
54 % 13 = 2
h(79) = 79 % 13 = 1
h(198) = 198 % 13 = 3
Resultando el siguiente arreglo T:
0
39, “Mario”, 1
1
79, “Silvia”, 4
Alicia Gioia
3/23
Tablas de Dispersión
2
54, “Juan”, 2
3
198, “Martin”, 3
4
5
6
7
46, “María”, 1
8
9
22, “José”, 1
10
11
12
La función h se denomina función de dispersión (hash function), el arreglo T es la tabla de dispersión (hash
table) o área primaria y h(k) es la dirección o posición en T de la clave k.
Una función de dispersión transforma el espacio de claves en un conjunto de enteros de 0 a m-1, siendo m el
tamaño o capacidad de la tabla.
Si las claves son números enteros la función de dispersión más simple es h(k) = k % m.
EJERCICIO 2
Sea m = 17 y el siguiente espacio de claves: {39, 35, 53, 43, 8, 50}. Hallar las direcciones a donde son
dispersadas las claves si la función de dispersión es h(k) = k % 17.
EJERCICIO 3
Encontrar el valor de m más pequeño tal que sea primo y el espacio de claves {14, 22, 39, 46, 54, 74, 198} se
dispersen a direcciones distintas con la función de dispersión h(k) = k % m.
Claves equivalentes y colisiones
Si ahora agregamos al conjunto A del ejemplo anterior el elemento: (14, “Pedro”, 3). El espacio de claves es:
{14}  C
y h(14) = 14 % 13 = 1
Vemos que tanto el elemento de clave 14 como el de clave 79 ocuparían la misma posición de la tabla.
Cuando dos o más claves ocupan la misma posición se dicen que son claves equivalentes y se ha producido
una colisión. Cuando se produce una colisión hay diversos caminos a tomar:
 Evitar las colisiones, ya sea
 Usando funciones de dispersión perfectas
 Aumentando la capacidad de la tabla de dispersión
 Permitir que haya colisiones y proveer mecanismos para su tratamiento.
EJERCICIO 4
Agregar tres claves al espacio de claves del ejercicio 2 de manera que colisionen con la clave 43.
Funciones perfectas
Una función de dispersión que transforma cada clave del espacio de claves en un valor entero positivo distinto
se dice perfecta. Es digno calcular cuantas funciones perfectas hay dado un conjunto de n claves y m
posiciones (1  n  m).
Consideremos, por ejemplo, n = m = 2.
Alicia Gioia
4/23
Tablas de Dispersión
Las posibles funciones son:
h1(C1) = 0 y h1(C2) = 0
h2(C1) = 0 y h2(C2) = 1
h3(C1) = 1 y h3(C2) = 0
h4(C1) = 1 y h4(C2) = 1
Hay cuatro funciones, de las cuales sólo h2 y h3 son perfectas. En este caso hay un 50% de funciones
perfectas. Pero si consideramos n = m = 3, encontraremos que sólo el 26% de funciones son perfectas. En
general puede demostrarse que la cantidad de funciones es mn siendo a lo sumo m!/(m-n)! la cantidad de
perfectas. Así resulta que si n = 4 hay 9% de funciones perfectas y para n = 5 sólo un 3%. Para los valores de
n que ocurren en la realidad (que está en los órdenes de cientos de miles o incluso millones) la búsqueda de
funciones perfectas es imposible.
EJERCICIO 5
Escribir todas las funciones que transformen un espacio de claves de tamaño 2 en los enteros 0, 1, 2. Indicar
las perfectas. Verificar que la cantidad de funciones y la cantidad de funciones perfectas coinciden con las
fórmulas dadas arriba.
EJERCICIO 6
Escribir todas las funciones que transformen un espacio de claves de tamaño 3 en los enteros 0, 1. Indicar las
perfectas.
Tablas con más Capacidad
La otra opción es usar tablas de dispersión con mayor capacidad. Supongamos, igual que antes que la
capacidad de la tabla es m = 13, pero el espacio de claves es:
C = 753, 546, 695, 125, 320, 276, 402, 855, 963, 689, 603, 662, 128 y h(k) = k % 13
k
753
546
695
125
320
276
402
855
963
689
603
662
128
h(k)
12
0
6
8
8
3
12
10
1
0
5
12
11
Así vemos que a dos claves distintas le corresponden la posición 0, produciéndose una colisión, otras dos van
a la posición 8, produciéndose otra colisión y tres van a 12 produciéndose allí dos colisiones. Por otro lado las
posiciones 2, 4, 7, y 9 están desocupadas, originando un 30% de lugar desperdiciado.
Si duplicamos el tamaño de la tabla a m = 26 y por consiguiente usamos la función de dispersión h(k) = k %
26, obtenemos 2 colisiones pero un 57% de lugar desperdiciado. En la siguiente tabla se muestra para
distintos valores de m, la cantidad de colisiones y el espacio desperdiciado.
Alicia Gioia
5/23
Tablas de Dispersión
m
Colisiones Lugares libres
13
4
13%
16
5
50%
17
3
41%
26
2
57%
En vista de los resultados, vemos que no tiene sentido agrandar demasiado la tabla. Lo aconsejable es que su
capacidad sea aproximadamente un 20% mayor que la cantidad de claves y que además sea un número primo.
Al cociente entre la cantidad de claves y la capacidad de la tabla se denomina factor de carga (se lo simboliza
con la letra ) y es una medida de lo saturada que ésta se encuentra, esto es
 = n/m
EJERCICIO 7
Una escuela tiene capacidad para 1000 alumnos. Calcular el valor de m adecuado para guardar la información
en una tabla de dispersión.
Funciones De Dispersión
Si m es el tamaño de la tabla, la función de dispersión debe:
 garantizar que el valor h(k)  k perteneciente al espacio de claves esté entre 0 y m-1
 dispersar lo más uniformemente posible (evitar que haya un número excesivo de colisiones y demasiados
lugares libres)
 debe ser calculable de modo eficiente, o sea, estar compuesta de un número reducido de operaciones
aritméticas básicas.
No hay reglas que permitan determinar cual será la función más apropiada para un conjunto de claves, con el
fin de asegurar una uniformidad máxima. Hacer un análisis de las características de las claves, puede sin
embargo ayudar en la elección de la misma.
La implementación de la función de dispersión depende del tipo de clave. No va a ser la misma si la clave es
un int, un float o un String
A continuación algunas funciones a modo de ejemplo :
 Función Modulo: Esta función también llamada resto o residuo consiste en tomar el resto de la división
entre la clave y la capacidad m. Estadísticamente se puede verificar que para una mayor uniformidad en la
distribución, m debe ser un número primo, o al menos que sea divisible por pocos números.
H(k) = (K % m)
 Función Cuadrado: Elevamos al cuadrado la clave y tomamos los dígitos centrales como dirección. El
número de dígitos a tomar es determinado por el rango del claves (por ejemplo si tengo 100 claves, tomo solo
2 dígitos)
H(k) = dig_centrales(K2)
Alicia Gioia
6/23
Tablas de Dispersión
Con claves no enteras la función de dispersión debe primero transformar la clave a un número entero
denominado valor intermedio.
Ejemplo
Una universidad le asigna a cada estudiante una clave que está formada por la inicial del nombre más la
inicial del apellido más 4 dígitos que corresponden al año de ingreso más un dígito que corresponde al
semestre en que ingresó y un número consecutivo de 4 dígitos. Algunas claves posibles son:
AG200020001
AG200020002
SM200110001
La universidad tiene aproximadamente 3000 alumnos, por consiguiente el área primaria debería tener una
capacidad mayor en 20% a 3000. Elegimos como valor para m al número primo 3607.
Supongamos que la función de dispersión fuera la suma de los valores ASCII de las letras más la suma de los
dígitos y al valor intermedio obtenido le aplicamos la función resto. Resulta:
AG200020001  65 + 71 + 5
= 142  142 % 3607 = 142
AG200020002  65 + 71 + 6
= 143  143 % 3607 = 143
SM200110001  83 + 77 + 5 =
165  165 % 3607 = 165
¿Cuál es el valor intermedio más grande?
ZZ199929999  90 + 90 + 66 = 226
y el valor más pequeño
AA200010001  65 + 65 + 4 = 134
Como vemos el valor intermedio está en 134, 226. Como todos son menores que 3607, el resto es él mismo.
Esto significa que las 3000 claves se transforman en alguno de los 92 valores que hay entre 134 y 226. Por lo
tanto es de esperar que haya en promedio 3000/92 = 33 colisiones y 3515 lugares libre o sea el 97% de lugar
desperdiciado. Esta función si bien cumple con el primer requisito, no cumple con el segundo.
Otra forma de obtener el valor intermedio es multiplicar el valor ASCII de las letras por la suma de los
dígitos.
AG200020001  65 * 71 * 5
=
23075 
23075 % 3607 = 1433
AG200020002  65 * 71 * 6
=
27690 
27690 % 3607 = 2441
SM200110001  83 * 77 * 5
=
31955 
31955 % 3607 = 3099
ZZ199929999  90 * 90 * 66 = 534600  534600 % 3607 =
AA200010001  65 * 65 * 4
=
16900 
764
16900 % 3607 = 2472
Como vemos esta función dispersa más uniformemente que la anterior.
Observar que si k1  k2 no es siempre válido que h(k1)  h(k2).
En una tabla de dispersión no existe noción de
orden, no hay anterior, ni sucesor, ni primero, ni
último.
Por esta razón las tablas de dispersión no sirven para obtener informes ordenados.
A continuación veremos otras técnicas para construir funciones de dispersión. Consideraremos a modo de
ejemplo a m = 3607.
1.
Funciones de división
 Si las claves son enteros muy grandes para que quepan en una variable long int, las guardamos en
cadenas y luego las partimos en dos del mismo número de dígitos y se suman, repitiendo el proceso
hasta obtener una dirección válida.
Alicia Gioia
7/23
Tablas de Dispersión
38.998.787 → 3.899 + 8.787 = 12.686
12.686 → 126 + 86 = 212
2.
Funciones de truncamiento

Eliminar alternativamente el primero y último dígitos, hasta obtener una dirección válida.
38.998.787.665 → 899.878.766 → 9.987.876 → 98.787 → 878

Truncar suficientes dígito en el medio de la cadena numérica, hasta obtener una dirección válida.
38.998.787.665 → 3865
3.
Funciones sobre un espacio intermedio
La clave puede ser alfanumérica. Se aplica primero una función que obtenga un valor entero y sobre
el resultado obtenido se aplica la función resto.

Por ejemplo podríamos multiplicar el código ASCII de cada carácter de la cadena y luego obtener el
resto de la división entre este número y m:
Si k = “PTRD0”, entonces h(k) = 80*84*82*68*48 % 3607 =
80
Si k = “ZZZZZ”, entonces h(k) = 90*90*90*90*90 % 3607 = 2938
Pero si las cadenas tienen longitud 6 el producto de los códigos ASCII va a dar overflow.

Otra función de dispersión es tratar los caracteres de la clave k como los dígitos de un entero en una
base b. Por ejemplo, b = 37 anda muy bien. Si ei es el código ASCII del carácter ci, entonces,
h(k) = (ek * bk + ek-1 * bk-1 + . . . + e0 * b0) % m
Si k = “PTRD0”, resulta
h(k) = (80*374 + 84*373 + 82*372 + 68* 371 + 48*370) % 3607 = 3250
Esta expresión, aplicando la regla de Horner y las propiedades de la aritmética modular, puede ser
escrita como:
h(k) = ((((80*37 + 84)*37 + 82)*37 + 68)* 37 + 48) % 3607 =
= (((((((80*37 + 84) % 3607) *37 + 82) % 3607) *37 + 68) % 3607)* 37 + 48) % 3607
evitando de esta manera problemas de overflow.

Concatenar los dígitos que representan los caracteres ASCII de la clave.
Si k = “CASA”, resulta
h(k) =
67.658.465 %
3607 = 1966
Alicia Gioia
8/23
Tablas de Dispersión
EJERCICIO 8
El plan de estudios de una carrera universitaria consta de 5 años y en cada año se cursan 12 asignaturas. Las
claves de cada asignatura está formada por el número del año a la que pertenece más un entero consecutivo.
Elegir un m y una función de dispersión adecuada.
Especificación del TDA Tabla de Dispersión
Nombre: TablaH
Constructoras:

crearTablaH: int

/* Crea y retorna una tabla vacía con una capacidad dada */
precondición: int m, siendo m un número primo
postcondición: TablaH t de tamaño m
TablaH
Modificadoras:

insertar: TablaH X tipoelemento

/* inserta un nuevo elemento en la tabla hash */
precondición: TablaH t; elemento e; e no pertenece a t
postcondición: t con el nuevo elemento e

eliminar: TablaH X tipoelemento

TablaH
/* elimina de la tabla hash el elemento cuya clave coincide con la dada*/
precondición: TablaH t; tipoelemento e con clave k; un elemento con clave k pertenece a t
postcondición: t sin el elemento con clave k
TablaH
Analizadoras:

recuperar: TablaH X tipoelemento

tipoelemento
/* recupera de la tabla hash el elemento cuya clave coincide con la del elemento dado */
precondición: TablaH t; tipoelemento e con clave k; k pertenece a t
postcondición: elemento de la tabla tal que la clave sea k

existe: TablaH X tipoelemento

boolean
/* verifica si existe en la tabla un elemento cuya clave coincida con la del elemento dado */
precondición: TablaH t; tipoelemento e con clave k;
postcondición: verdadero si existe un elemento cuya clave coincide con la dada; falso en caso
contrario

obtenerArBinBus: TablaH

ArBinBus
/* obtiene un árbol binario de búsqueda con los elementos de la tabla hash para su posterior
impresión */
precondición: TablaH t;
postcondición: ArBinBus a
Destructora:

destruirTablaH: TablaH

/* libera la memoria ocupada por una tabla hash */
precondición: TablaH t
postcondición: -
Alicia Gioia
9/23
-
Tablas de Dispersión
Estrategias De Resolución De Colisiones Y De Desbordamiento De Tabla
Antes de codificar las operaciones descritas en la especificación debemos resolver dos problemas:
 cómo tratar las colisiones
 qué hacer cuando el valor de m queda chico (desbordamiento de tabla).
Para el primer problema hay varias técnicas de resolución, éstas son:
1) Dispersión abierta
2) Dispersión cerrada
i) con exploración lineal
ii) con exploración cuadrática
iii) con dispersión doble
iv) con dispersión coalescente
Para el problema de desbordamiento de tabla consideraremos las siguientes técnicas:
1) Expansión de la tabla
2) Dispersión extensible.
Resolución de colisiones
Dispersión abierta
Esta estrategia consiste en construir la tabla de dispersión como un arreglo de listas, donde cada lista está
formada por los elementos que tienen claves equivalentes.
Así el ejemplo 1 da la siguiente tabla:
0
.
39, “Mario”, 1

1
.
79, “Silvia”, 4

2
.
54, “Juan”, 2

3
.
198, “Martin”, 3

4

5

6

7
.
46, “María”, 1

22, “José”, 1

8

9
.
10

11

12

Si agregamos los siguientes elementos: (14, “Oscar”,3), (41,”Roberto”,2) y (67,”Mariela”, 3) en ese orden, la
tabla queda:
Alicia Gioia
10/23
Tablas de Dispersión
0
.
39, “Mario”, 1

1
.
14, “Oscar”, 3
.
79, “Silvia”, 4

2
.
67, “Mariela”, 3
.
41,”Roberto”,2
.
3
.
198, “Martin”, 3

4

5

46, “María”, 1

22, “José”, 1

6

7
.
8

9
.
10

11

12

54, “Juan”, 2
Cuando busquemos una clave vamos a tener que recorrer la lista que le corresponda hasta encontrar nuestro
elemento. Si la función de dispersión distribuye uniformemente entonces el tamaño promedio de cada lista es
. Las listas pueden dejarse desordenadas o bien mantenerlas ordenadas. Lo más frecuente es usar listas
desordenadas porque son más eficientes para la implementación de la operación insertar y como generalmente
son listas pequeñas la búsqueda no se degrada. En el caso que utilicemos la tabla en su mayor parte haciendo
búsquedas va a ser útil mantener las listas ordenadas, con esto lograremos acelerar las búsquedas por un factor
de 2 a cambio de una inserción más lenta.
¿Cómo elegir el tamaño m de la tabla?
Deberíamos elegir un valor de m que sea suficientemente pequeño de manera que no estemos derrochando
una gran cantidad de memoria contigua con punteros a vacío, pero lo suficientemente grande de modo que las
búsquedas secuenciales sean eficientes. Como ya dijimos, la práctica indica que debemos elegir un m que sea
aproximadamente entre 1/5 y 1/10 mayor que la cantidad de claves que esperamos que se ingresen en la tabla.
Una de las mayores virtudes de la dispersión abierta es que esta decisión no es crítica: si se ingresan más
claves de las esperadas, simplemente las búsquedas van a ser un poco más lentas que con un m más grande.
Mientras que si se ingresan menos claves de las esperadas, tendremos búsqueda muy eficientes con tal vez
cierto desperdicio de espacio.
Implementación en Java
public class TablaHash {
// Implementacion de algunos metodos de
// Tabla Hash con resolucion dinamica de colisiones
// El objeto que se inserta debe implementar las interfaces Comparable y
Hashable.
// Autor Alicia Gioia
private ListaD t[];
private int capacidad;
public TablaHash(int M) {
if (!Primo.esPrimo(M))
M = Primo.proxPrimo(M);
capacidad = M;
t = new ListaD[M];
for(int i = 0; i < M ; i++)
t[i] = new ListaD();
Alicia Gioia
11/23

Tablas de Dispersión
}
public void insertar (Object x) {
int k =((Hashable) x).hash(capacidad);
t[k].insertar(x);
}
public Object buscar (Object x) {
int k = ((Hashable) x).hash(capacidad);
t[k].irPrimero();
int l = t[k].longitud();
for (int i = 0 ; i < l ; i ++ )
if (((Comparable) x).compareTo(t[k].recuperarActual())== 0)
return t[k].recuperarActual();
return x;
}
public ArBinBus obtenerArBinBus () {
ArBinBus a = new ArBinBus();
for (int i = 0; i < capacidad; i++ ) {
if (!t[i].esVacia()) {
t[i].irPrimero();
for (int j = 0; j < t[i].longitud() ; j++)
a.insertar(t[i].recuperarActual());
t[i].irSiguiente();
j++;
}
}
}
return a;
}
}
public interface Comparable {
int compareTo(Object x);
}
public interface Hashable {
int hash(int M);
}
public class Primo {
public static boolean esPrimo(int n) {
if (n == 1 || n == 2 || n == 3)
return true;
if (n % 2 == 0)
return false;
else {
int k = 3;
while (k <= Math.sqrt(n)) {
if (n % k == 0)
return false;
k = k +2;
}
}
return true;
}
public static int proxPrimo(int n) {
if (n % 2 == 0)
n++;
Alicia Gioia
12/23
{
Tablas de Dispersión
while (!esPrimo(n))
n = n + 2;
return n;
}
}
EJERCICIO 9
Dado el siguiente conjunto de claves {4371,, 1323, 6173, 4199, 4344, 9679, 1989} y la función de dispersión
h(k) = k % 10, mostrar cómo resulta la tabla de dispersión abierta.
EJERCICIO 10
Codificar el resto de las operaciones indicadas en la especificación.
Dispersión cerrada
En la dispersión cerrada todos los elementos se almacenan en la propia tabla. En este caso las colisiones se
resuelven calculando una secuencia de huecos en la tabla. Esta secuencia explora la tabla hasta que se
encuentra un hueco en el caso de insertar o se encuentra la clave en el caso de recuperar o eliminar.
Básicamente la dispersión cerrada consiste en aplicar la función de dispersión, si el lugar está libre colocamos
el elemento sino “exploramos” la tabla hasta encontrar un lugar libre.
La exploración se realiza calculando las posibles posiciones para una clave k con la siguiente función:
di(k) = (h(k) + f(i)) % m
para i = 1, 2, 3, ...
Exploración lineal
Se elige a f(i) = i, así la función de exploración queda:
di(k) = (h(k) + i) % m
para i = 1, 2, 3, ...
En el ejemplo anterior inicialmente tenemos:
Alicia Gioia
13/23
Tablas de Dispersión
0
39, “Mario”, 1
1
79, “Silvia”, 4
2
54, “Juan”, 2
3
198, “Martin”, 3
4
5
6
7
46, “María”, 1
8
9
22, “José”, 1
10
11
12
Al agregar el elemento (14, “Oscar”,3), resulta que h(14) = 14 % 13 = 1 y este lugar está ocupado.
Calculamos entonces
39, “Mario”, 1
0
d1(14) = (h(14) + 1) % 13 = 2 (también está ocupado)
79, “Silvia”, 4
Calculamos
1
54, “Juan”, 2
d2(14) = (h(14) + 2) % 13 = 3 (también está ocupado)
2
Calculamos
198, “Martin”, 3
3
d3(14) = (h(14) + 3) % 13 = 4 (está libre)
14, “Oscar”,3
4
5
6
7
46, “María”, 1
8
9
22, “José”, 1
10
11
12
Ahora insertamos (41,”Roberto”,2). Aplicamos h(42), resultando 3, que está ocupado. Calculamos d1(41) que
da 4, lugar ocupado y finalmente d2(41) da 5 y ahí lo insertamos. Finalmente al agregar el elemento
(67,”Mariela”, 3), procediendo de la misma manera, éste cae en la posición 6. Resultando la tabla:
Alicia Gioia
14/23
Tablas de Dispersión
0
39, “Mario”, 1
1
79, “Silvia”, 4
2
54, “Juan”, 2
3
198, “Martin”, 3
4
14, “Oscar”,3
5
41,”Roberto”,2
6
67,”Mariela”, 3
7
46, “María”, 1
8
9
22, “José”, 1
10
11
12
0.
9
0.
8
0.
6
0.
7
0.
3
0.
4
0.
5
100
90
80
70
60
50
40
30
20
10
0
0
0.
1
0.
2
probes
La ventaja de este enfoque es que evita el uso de punteros. La memoria que se ahorra al no almacenar
punteros puede utilizarse para construir una tabla más grande que probablemente conduzca a menos
colisiones.
Ahora vamos a necesitar indefectiblemente una tabla más grande, tal que el tamaño m sea mayor que la
cantidad n de elementos. El factor de carga es crítico, como se ve en la siguiente gráfica:
Factor de carga
En la exploración lineal se produce además un problema conocido como agrupamiento primario. Los
elementos, como vemos en el ejemplo, tienden a juntarse y en consecuencia la búsqueda se hace más lenta.
Por ejemplo para buscar el elemento de clave 67, debemos calcular
h(67) = 2, comparamos 67 con la clave guardada en 2, si coinciden ya está sino calculamos
d1(67) = 3, comparamos 67 con la clave guardada en 3, si coinciden ya está sino continuamos calculando
d2(67), d3(67), y así hasta encontrarla.
La razón por la cual se producen estos agrupamientos es la siguiente:
Supongamos que todas las posiciones de la tabla sean equiprobables y las i posiciones anteriores a una
posición p estén ocupadas.
Alicia Gioia
15/23
Tablas de Dispersión
p-i
ocupada
.
ocupada
p-2
ocupada
p-1
ocupada
P
libre
Todas las claves que deberían ir a p, p-1, p-2, ...,p-i van a ir a p, por lo tanto la probabilidad que una clave
vaya a p es (i+1)/m. En cambio si la celda anterior está desocupada la probabilidad que una clave vaya a p es
1/m.
EJERCICIO 11
Dado el siguiente espacio de claves {4371, 1323, 6173, 4199, 4344, 9679, 1989} y la función de dispersión
h(k) = k % 10, mostrar cómo resulta la tabla de dispersión cerrada con exploración lineal.
Exploración cuadrática
La exploración cuadrática es un método de resolución de colisiones que mejora el problema del
agrupamiento primario (Clustering) de la exploración lineal. El proceso es similar al anterior, pero mientras
que la función de inspección de la exploración lineal es f(i) = i ; ahora vamos a usar una función cuadrática
f(i) = i2, entonces, en una inserción cuando encontremos un lugar ocupado avanzaremos primero 1, luego 2 2,
luego 32 ,etc. hasta encontrar un lugar vacío. Si bien exploración cuadrática mejora sustancialmente el
problema del agrupamiento primario, tiene la desventaja de que pueden quedar celdas sin visitar (Clustering
Secundario), en particular, si la tabla esta llena en más de un 50% (>0.5) , no hay garantías de encontrar una
celda vacía si el tamaño de la tabla no es PRIMO.
¿Cómo borrar una clave de una tabla con exploración lineal o
cuadrática?
No podemos simplemente borrarla, porque los elementos que
fueron insertados después no van a ser encontrados ya que la
búsqueda va a ser interrumpida por el “agujero” que dejo el
borrado. Supongamos tener la siguiente tabla donde el valor –1
indica que esa posición está libre
Buscamos con exploración lineal la clave 32. Como h(32) da 6
comparamos 32 con la clave que está en la posición 6. Como ésta
es 67, calculamos d1(32), resultando 7, como este lugar está
ocupado por la clave 46, calculamos d2(32), dando 8, como este
lugar está libre concluimos que la clave 32 no está en la tabla, lo
que es correcto.
Alicia Gioia
16/23
0
39, “Mario”, 1
1
79, “Silvia”, 4
2
54, “Juan”, 2
3
198, “Martin”, 3
4
14, “Oscar”,3
5
41,”Roberto”,2
6
67,”Mariela”, 3
7
46, “María”, 1
8
-1
9
22, “José”, 1
10
-1
11
-1
12
-1
Tablas de Dispersión
Supongamos, ahora que eliminamos el 41. La tabla queda:
Buscamos ahora el 67, la secuencia va a ser 2, 3, 4 y 5. Como la
posición 5 está libre concluiremos que el 67 no está en la tabla lo
cual es erróneo. Eso se debe que la posición 5 antes estuvo ocupada
y ahora está libre.
Hay que diferenciar una posición que está vacía porque nunca fue
ocupada y otra que está vacía pero alguna vez estuvo ocupada. Le
podemos agregar a cada elemento una marca o centinela cuyos
posibles valores pueden ser:
Valor de marca
0
Significado
libre (nunca fue ocupado)
1
ocupado
2
libre (hubo un elemento que fue eliminado)
0
39, “Mario”, 1
1
79, “Silvia”, 4
2
54, “Juan”, 2
3
198, “Martin”, 3
4
14, “Oscar”,3
5
-1
6
67,”Mariela”, 3
7
46, “María”, 1
8
-1
9
22, “José”, 1
10
-1
11
-1
12
-1
EJERCICIO 12
Codificar las operaciones descritas en la especificación de tabla de dispersión utilizando la estrategia de
dispersión cerrada con exploración lineal.
Dispersión doble
Una solución para eliminar casi completamente el agrupamiento tanto primario como secundario de los
métodos citados anteriormente, es dispersión doble, en donde
di(k) = (h1(k) + ih2(k)) % m para i = 1, 2, .....
Tanto h1 como h2 son dos funciones de dispersión.
Dispersión coalescente
Esta forma es similar a la dispersión abierta excepto que todos los elementos se almacenan en la propia tabla.
Esto se consigue permitiendo que en cada celda se almacene un elemento y un “puntero”. Estos “punteros”
almacenan el valor –1 o bien la dirección de alguna celda.
Durante la inserción una colisión se resuelve insertando el elemento con el número más alto que esta libre y
haciendo el encadenamiento.
Ejemplo
Sea m = 7; h(k) = k % 7 y espacio de claves {4, 8, 9, 2, 13, 6}
Inicialmente la tabla es:
Alicia Gioia
17/23
Tablas de Dispersión
0
-1
1
-1
2
-1
3
-1
4
-1
5
-1
6
-1
0
-1
Luego de insertar el 4, el 8 y el 9,
1
8, 
-1
2
9, 
-1
-1
3
4
4, 
-1
5
-1
6
-1
Al aplicar la función hash a la clave de valor 2, resulta que su posición es 2, pero este lugar está ocupado por
la clave de valor 9, entonces guardamos el 2 en la posición más alta que está vacía (en este ejemplo es la
número 6) y el puntero de la clave 9 toma el valor 6.
-1
0
1
8, 
-1
2
9, 
6
-1
3
4
4, 
-1
-1
5
6
2, 
-1
La clave de valor 13 también le corresponde la posición 6, pero como está ocupada la guardamos en la
posición 5 (que es la más alta libre).
-1
0
1
8, 
-1
2
9, 
6
-1
3
4
4, 
-1
5
13, 
-1
6
2, 
5
Finalmente al insertar la clave de valor 6, la tabla resulta:
Alicia Gioia
18/23
Tablas de Dispersión
-1
0
1
8, 
-1
2
9, 
6
3
6, 
-1
4
4, 
-1
5
13, 
3
6
2, 
5
Veamos ahora cómo realizamos las búsquedas,
clave a buscar
h(k) = k % 7
6
h(6) = 6
15
h(15) = 1
20
h(20) = 6
6 es igual a 2 ? NO
6 es igual a 13? NO
6 es igual a 6? SI
prox. dirección: 5
prox. dirección: 3
BUSQUEDA EXITOSA
15 es igual a 8? NO
prox. dirección: -1
BUSQUEDA NO EXITOSA
20 es igual a 2? NO
20 es igual a 13? NO
20 es igual a 6? NO
prox. dirección: 5
prox. dirección: 3
prox. dirección: -1
BUSQUEDA NO EXITOSA
En la eliminación debemos actualizar los punteros. Por ejemplo, si eliminamos el 13 resulta:
-1
0
1
8, 
-1
2
9, 
6
3
6, 
-1
4
4, 
-1
-1
5
6
2, 
3
Una variante que mejora el manejo de los espacios libres es dividir la tabla en dos partes: la zona principal y
la zona de desbordamiento o excedentes. La función hash da resultados que caen sólo en la zona principal.
Alicia Gioia
19/23
Tablas de Dispersión
-1
0
1
8, 
-1
2
9, 
7
-1
3
4
4, 
-1
-1
5
6
13, 
8
7
2, 
-1
8
6, 
-1
9
10
}
Zona Principal
}
Zona
Excedentes
Estudios empíricos recomiendan alrededor de 15% a la zona de excedentes. Mejora el tiempo a costa de
espacio.
EJERCICIO 13
Dado el siguiente espacio de claves {4371, 1323, 6173, 4199, 4344, 9679, 1989} y la función de dispersión
h(k) = k % 10, mostrar cómo resulta la tabla de dispersión coalescente .
Desbordamiento de la tabla
Hasta este punto, hemos asumido que el tamaño m de la tabla de dispersión será siempre suficientemente
grande como para acomodar los conjuntos de datos con los que estamos trabajando. En la práctica, sin
embargo, debemos considerar la posibilidad de una inserción en una tabla llena (esto se denomina
desbordamiento de la tabla). Si se está utilizando dispersión abierta, como ya hemos dicho, éste no es
habitualmente un problema dado que el tamaño total de las cadenas sólo está limitado por la memoria
disponible. Obviamente en dispersión cerrada éste es un problema muy importante. Consideraremos dos
técnicas que ressuelven el problema del desbordamiento de la tabla asignando memoria adicional.
Redispersión (re-hashing)
Cuando las tablas se llenan demasiado, el tiempo de ejecución de algunas operaciones va a empezar a ser muy
largo. Sobre todo cuando combinamos muchas inserciones con borrados. Una solución es crear otra tabla que
sea el doble de grande (con una nueva función de dispersión asociada) y procesar la tabla de dispersión
original entera, computando el nuevo valor de dispersión para cada elemento e insertándolo en la nueva tabla.
Esta operación se denomina redispersión. Es una operación muy costosa (orden N), pero dado que no va a
pasar muy frecuentemente, no está nada mal y su efecto va a pasar prácticamente desapercibido. Solo el
desafortunado usuario cuya inserción provoque una redispersión va a sentir el efecto. En que momento aplicar
la redispersión queda a criterio del programador, algunas posibilidades son:
-
Aplicar redispersión cada vez que la tabla este llena en más de un 50%
Aplicar redispersión sólo cuando una inserción falla
-
Aplicar redispersión cuando el factor de carga  supera un valor estipulado (por ejemplo 0.8 ).
EJERCICIO 14
Aplicar redispersión a la tabla del ejercicio 11.
Alicia Gioia
20/23
Tablas de Dispersión
Dispersión Extensible
Una variante más efectiva de la redispersión es la dispersión extensible. En lugar de tener una única tabla de
dispersión se tienen varias. Supongamos que un valor adecuado para m sea 43, entonces en lugar de tener una
tabla de 43 posiciones tendríamos, por ejemplo, 4 de 11. Cuando la tabla grande esté ocupada en
aproximadamente 34 posiciones (  0.80) es el momento de aplicar la redispersión, mientras que en las
tablas de 11 elementos la redispersión habría que aplicarla cuando estén ocupados unos 9 lugares.
En la dispersión extensible el proceso de dispersión se realiza en dos partes. Primero se calculan los primeros
bits de la clave para ver que tabla le corresponde y luego se aplica la función residuo para calcular la posición
dentro de la tabla.
Más formalmente, si D es el número de bits usados para definir la tabla, la cual a veces se llama directorio, el
número de entradas en el directorio es 2D. Supongamos que nuestros datos consisten en enteros de seis bits y
D = 2 (el directorio tendrá como entradas 00, 01, 10 y 11).
Claves = {40, 11, 32, 4, 44, 57, 56, 8, 46, 20, 24, 10}
Como la representación en binario de 40 es 101000 le corresponde la entrada 10, y como h(40) = 40 % 11 =
7, ocupará la posición 7. Continuando de la misma manera con el resto de las claves, resulta.
Directorio
00 .

01 .

10 .

11 .

11
4
8
24
44
20
46
56
10
40
32
57
Si ahora insertamos las claves 36, 43, 39, 38, 45, la tabla queda de la siguiente manera:
00 .

01 .

10 .

11 .

11
4
8
24
44
43
46
56
57
10
20
36
45
39
38
40
32
Como la tabla de entrada 10 tiene un factor de carga de aproximadamente 0.8, ampliamos el tamaño del
directorio no el de la tabla. Así D pasa a valer 3.
Alicia Gioia
21/23
Tablas de Dispersión
000 . 
11
4
8
10
001
24
010 . 
20
011
36
100 . 
101

44
110 . 
45
46
56
57
38
39
32
40
43
111
Observar que las tablas que no fueron partidas están apuntadas por dos entradas de directorio adyacentes. Si
por ejemplo se llena la tabla que está apuntada por 000 y 001, no duplicamos el directorio sino que creamos
una nueva tabla que será apuntada por 001 y redispersamos sólo los elementos de esa tabla. La ventaja de
esta estrategia es que se redispersan tablas más pequeñas con el consecuente ahorro de tiempo.
EJERCICIOS 15
Mostrar el resultado de insertar las claves 10111101,00000010, 10011011, 10111110, 01111111, 01010001,
10010110, 00001011, 11001111, 10011110, 11011011, 00101011, 01100001, 11110000, 01101111 en una
estructura de datos de dispersión extensible inicialmente vacía con m = 4 y D =2.
Resumen
Las tablas de dispersión se pueden usar para implantar las operaciones insertar y buscar en tiempo medio
constante. Es de especial importancia cuidar detalles como el factor de carga cuando se emplean tablas de
dispersión, ya que de otra forma las cotas de tiempo no son válidas. También es importante escoger con
cuidado la función de dispersión.
Para la dispersión abierta, el factor de carga debe ser cercano a 1, aunque el rendimiento no se degrada
significativamente a menos que el factor de carga crezca mucho. Para dispersión cerrada, el factor de carga no
debe exceder 0.5. Si se usa exploración lineal, el rendimiento se degenera con rapidez conforme el factor de
carga se acerca a 1. Si se programa en un lenguaje (C, C++, Java) que permite la asignación de arreglos sin
conocer su tamaño en tiempo de compilación, entonces se puede implantar la redispersión. Esta permite que
la tabla crezca y se contraiga para mantener un factor de carga razonable.
En comparación con los árboles binarios de búsqueda, sabemos que en éstos las operaciones de insertar y
buscar son O(log2n) y permiten rutinas que permiten orden y por ello son más potentes. Con una tabla de
dispersión, no es posible encontrar el elemento mínimo ni máximo. Tampoco es posible buscar parte de la
clave. Un árbol binario de búsqueda podría encontrar rápidamente todos los elementos en un intervalo dado;
esto no es posible en las tablas de dispersión. además, la cota O(log2n) no es mucho más que O(1), pero si la
entrada de datos es más o menos ordenada puede provocar un rendimiento deficiente en los árboles. Es muy
costoso implantar árboles balanceados, así que si no se requiere información de ordenamiento y hay alguna
sospecha de que la entrada podría estar ordenada entonces la dispersión es la estructura ordenada.
Las aplicaciones de la dispersión son abundantes. Son utilizadas:

por los compiladores para seguir el rastro de las variables declaradas en el código fuente;

en problemas de teoría de grafos;

en los programas de juegos;

en revisores de ortografía en línea.
Bibliografía
Knuth, The art of computer programming
Alicia Gioia
22/23
Tablas de Dispersión
Weiss, Estructuras de datos y algoritmos
Wood, Data strucures, algorithms and perfomance
Heileman, Estructuras de datos, algoritmos y programación orientada a objetos.
Villalobos, Diseño y manejo de estructuras de datos en C
Alicia Gioia
23/23
Descargar