Tablas de dispersión (hash tables)

Tablas de dispersión (hash tables) La dispersión es una técnica empleada para realizar inserciones, eliminaciones y búsquedas en un tiempo promedio co...
0 downloads 0 Views 44KB Size
Tablas de dispersión (hash tables) La dispersión es una técnica empleada para realizar inserciones, eliminaciones y búsquedas en un tiempo promedio constante. La estructura de datos ideal para la tabla de dispersión es simplemente un vector de tamaño fijo N que contiene las claves. • Cada clave se hace corresponder con algún número en el intervalo entre 0 y N − 1, y se coloca en la celda correcta. • A la correspondencia se le denomina función de dispersión que, idealmente, debe ser simple de calcular y debe asegurar que dos claves distintas cualesquiera caigan en celdas diferentes. ◦ Esto es imposible, así que se busca una función de dispersión que distribuya homogéneamente las llaves entre las celdas. • Resta elegir una función, decidir qué hacer cuando dos claves caen en el mismo valor (lo que se llama colisión) y decidir el tamaño de la tabla. Un uso común de las tablas hash lo representan los diccionarios. Un diccionario almacena objetos formados por una clave, por la cual se busca en el diccionario, y su definición, que es lo que se devuelve.

Función de dispersión Si las claves de entrada son enteros, la función clave mod N resulta una buena estrategia, a menos que clave tenga algunas propiedades indeseables. • Por ejemplo, si N es 10 y todas las claves terminan en cero, la función de dispersión estándar es una mala opción. ◦ Es buena idea asegurarse de que el tamaño de la tabla sea un número primo. Si las claves de entrada son enteros aleatorios, esta función es muy simple y distribuye las claves con uniformidad. Toda función de dispersión debe ser sencilla de computar, pero también ha de distribuir uniformemente las claves. Por lo regular, las claves son cadenas de caracteres.

• Una opción es sumar los valores ASCII de los caracteres de la cadena. función Dispersión1 (Clave, TamañoClave): Índice valor := ascii(Clave[1]); para i := 2 hasta TamañoClave hacer valor := valor + acii(Clave[i]) fin para devolver valor mod N fin función ◦ Es una función fácil de implementar, y se ejecuta con rapidez. ◦ No obstante, la función no distribuye bien las claves si el tamaño de la tabla es grande. Por ejemplo, si N = 10007 (un número primo), y todas las claves tienen una longitud de ocho o menos, la función de dispersión sólo toma valores entre 0 y 1016 que es 127 ∗ 8.

• En la siguiente función de dispersión se supone que la clave tiene al menos tres posiciones. función Dispersión2 (Clave, TamañoClave): Índice devolver (ascii(Clave[1]) + 27*ascii(Clave[2]) + 729*ascii(Clave[2])) mod N fin función ◦ Esta función examina sólo los primeros tres caracteres, pero si son aleatorios y el tamaño de la tabla es 10007, esperaríamos una distribución bastante homogénea. ◦ Desafortunadamente, los lenguajes naturales no son aleatorios. Aunque hay 273 = 17576 combinaciones posibles de tres caracteres, en un diccionario el número de combinaciones diferentes que nos encontramos es menor que 3000. Sólo un porcentaje bajo de la tabla puede ser aprovechada por la dispersión.

• En la función de dispersión que sigue intervienen todos los caracteres en la clave y se puede esperar una buena distribución. función Dispersión3 (Clave, TamañoClave): Índice valor := ascii(Clave[1]); para i := 2 hasta TamañoClave hacer valor := (32*valor + acii(Clave[i])) mod N fin para devolver valor fin función ◦ El código calcula una función polinómica con base en la noClave−1 32i · clave[Tama noClave − i] regla de Horner. ∑Tama i=0 ◦ La multiplicación por 32 no es realmente una multiplicación, sino el desplazamiento de cinco bits. ◦ Para lenguajes que permiten el desbordamiento el bucle se debería escribir sin operación mod; se aplicaría mod justo antes de volver. ◦ Si las claves son muy grandes, se acostumbra a no utilizar todos los caracteres. La longitud y las propiedades de las claves influirán en la elección de una buena función de distribución. Queda la resolución de colisiones. Si al insertar un elemento se dispersa en el mismo valor que un elemento ya insertado, tenemos una colisión y hay que resolverla.

Dispersión abierta La estrategia consiste en tener una lista de todos los elementos que se dispersan en el mismo valor. Para efectuar buscar, usamos la función de dispersión para determinar qué lista recorrer. Para efectuar un insertar, recorremos la lista adecuada para revisar si el elemento ya está en la lista. Si el elemento resulta ser nuevo, se inserta al frente o al final de la lista. Además de las listas enlazadas, se podría usar cualquier esquema para resolver colisiones, como un árbol binario de búsqueda. El factor de carga, λ, de una tabla de dispersión es la relación entre el número de elementos en la tabla y su tamaño. λ=

Número de claves en la tabla N

La longitud media de una lista es λ. Y la regla general de una dispersión abierta es hacer el tamaño de la tabla casi tan grande como el número de elementos esperados (λ = 1). El esfuerzo necesario para realizar una búsqueda es el tiempo constante necesario para evaluar la función de dispersión O(1) más el tiempo necesario para recorrer la lista. • En una búsqueda infructuosa el promedio de nodos por recorrer es O(λ) • En una búsqueda con éxito, O(λ/2)

Dispersión cerrada La dispersión abierta tiene la desventaja de que requiere apuntadores. En un sistema de dispersión cerrada, si ocurre una colisión, se intenta buscar celdas alternativas hasta encontrar una vacía. • Se busca en sucesión en las celdas d0(x), d1(x), d2(x) . . . donde di(x) = (dispersion(x) + f (i)) mod N, con f (0) = 0. • La función f es la estrategia de resolución de las colisiones. Como todos los datos se meten en la tabla, ésta tiene que ser más grande para la dispersión cerrada que para la abierta. En general, para la dispersión cerrada el factor de carga debe estar por debajo de λ = 0,5 La eliminación estándar no se puede realizar en una tabla de dispersión cerrada, porque la celda pudo haber causado una colisión en el pasado. • Las tablas de dispersión cerrada requieren eliminación perezosa, aunque en este caso no haya realmente “pereza” implicada.

Exploración lineal

En la exploración lineal, f (la estrategia de resolución de las colisiones) es una función lineal de i, por lo general f (i) = i. Esto equivale a buscar secuencialmente en el vector (con circularidad) hasta que encontremos una posición vacía. En tanto que la tabla sea suficientemente grande, siempre se puede encontrar una celda vacía, pero ello puede tomar demasiado tiempo. Si se supone independencia entre intentos, el número medio de celdas que se examinan en una inserción con exploración lineal es 1/(1 − λ) • En una tabla con factor de carga λ, la probabilidad de que una celda esté vacía es 1 − λ. Como consecuencia el número esperado de intentos independientes hasta encontrar una celda vacía es 1/(1 − λ). Desgraciadamente, la independencia asumida no se cumple. Lo peor, aún si la tabla está relativamente vacía, es que se empiezen a formar bloques de celdas ocupadas (agrupamiento primario). • Cualquier clave que se disperse en el agrupamiento necesitará varios intentos para resolver la colisión, y después se agregará al agrupamiento. • En la agrupación primaria, la falta de eficiencia no está sólo provocada por los elementos que colisionan por tener el

mismo valor hash, sino también por aquéllos que colisionan en las posiciones alternativas de otros. Teniendo todo esto en cuenta, el número medio de celdas examinadas en una inserción con exploración lineal es cercano a 1 1 (1 + ) 2 (1 − λ)2 λ Número medio de celdas examinadas 0,5 2,5 0,75 8,5 0,9 50,5 El coste de una búsqueda sin éxito es el mismo que el coste de una inserción. Y el coste de una búsqueda con éxito es la media de los costes de las inserciones en tablas con factores de carga más pequeños. 1 1 (1 + ) 2 1−λ Exploración cuadrática

Su resolución de colisiones elimina el problema del agrupamiento primario que padece la exploración lineal. La función de colisión es cuadrática. La elección común es f (i) = i2. Se examinan las celdas 1, 4, 9 y sucesivas a partir de la posición inicial. En la exploración lineal es mala idea dejar que la tabla de dispersión esté casi llena, porque se degrada el rendimiento. Para la exploración cuadrática, la situación es aún más drástica.

• No hay garantías de encontrar una celda vacía una vez que la tabla se llena a más de la mitad, o aún antes si el tamaño de la tabla no es primo. • Se demuestra que si la tabla está medio vacía y su tamaño es primo, podemos contar siempre con la seguridad de poder insertar un elemento nuevo. Aunque la exploración cuadrática elimina el agrupamiento primario, los elementos que se dispersan a la misma posición probarán en las mismas celdas alternas. Eso se llama agrupamiento secundario. • Éste es un pequeño defecto teórico. Los resultados prácticos indican que se producen, en general, menos de medio intento adicional por búsqueda. Exploración doble

En este método para la resolución de colisiones aplicamos una segunda función de dispersión. La elección común es f (i) = i · h2(x) La función nunca debe evaluarse a cero. Y es importante que todas las celdas puedan ser intentadas. Una función como h2(x) = R − (x mod R), con R un primo menor que el tamaño de la tabla, funcionará bien. Hay que asegurarse de que el tamaño de la tabla sea primo.