Programación. Tema 8: Tablas Hash 16/Mayo/2004 Apuntes elaborados por: Eduardo Quevedo, Aaron Asencio y Raquel López Revisado por: Javier Miranda el ???? Tema 8: Tabla Hash Las tabla hash aparece para conseguir una búsqueda e inserción muy rápidas; para ello se hace uso de un array, con lo que volvemos a la estructura básica de almacenamiento. Sin embargo, hay una diferencia importante que es lo que hace que sea mejor que el array en cuanto a rapidez: a cada dato se le asigna, mediante una fórmula matemática denominada función hash, una posición única en la tabla, con lo que la búsqueda, la inserción y el borrado son inmediatos: O(1). Como ocurre siempre, lo que ganamos por un lado, lo perdemos por otro; en el caso de la tabla hash los inconvenientes son: ⇒ Al tratarse de un array, el tamaño de la tabla está limitado y debe fijarse desde el principio. ⇒ Como las posiciones ocupadas no tienen por qué ser consecutivas, no se puede recorrer el contenido de una tabla hash. ⇒ Como la posición de una palabra se calcula de forma matemática, los datos no pueden almacenarse ordenados. ⇒ Si permitimos datos duplicados se produce lo que se denomina “colisión”, que consiste en que a dos palabras se les asigna la misma posición en el array. Este es un problema que trataremos de resolver. Función hash: Hasta ahora hemos dicho que a cada palabra se le asigna una determinada posición de la tabla por medio de la función hash, pero todavía no hemos definido esta función. Veamos una primera solución y sus inconvenientes, y las soluciones que nacen a partir de ésta: Solución 1: Proponemos como una primera función hash la suma del código ASCII de cada una de las letras de la palabra: F ( Palabra) = Palabra ' Last ∑ Character' Pos( Palabra(i)) i = Palabra ' First Por ejemplo: F (" alfredo" ) = 97 + 108 + 102 + 114 + 101 + 100 + 111 = 733 Problemas: Programación. Tema 8: Tablas Hash 16/Mayo/2004 Colisión 1 : Palabras idénticas. Colisión 2 : Palabras formadas por las mismas letras pero en distinto orden. Colisión 3 : Palabras distintas cuyas letras dan la misma suma. La suma da números muy pequeños. Solución 2: Suma ponderada o polinómica. Cambiamos la función hash para que el resultado sea mayor; para conseguir esto multiplicamos la posición de cada letra de la palabra por el número de letras del alfabeto (27) elevado al peso que tiene la letra en la palabra: F ( Palabra ) = Polinomio( Palabra ) = n.27 n + (n − 1).27 n −1 + ... + 2.27 2 + 1.271 Por ejemplo: F (" alfredo" ) = 7.27 7 + 6.27 6 + 5.27 5 + 4.27 4 + 3.27 3 + 2.27 2 + 1.271 Problemas: Los resultados de la función hash salen muy diferentes para cada palabra, con lo que éstas se guardan en posiciones muy separadas de la tabla, desperdiciando de esta forma una gran cantidad de memoria. Si introducimos una palabra muy grande, la posición resultante es demasiado elevada. Por ejemplo, si queremos insertar una palabra de 10 letras, la posición que le corresponde viene dada por 10.2710 + 9.27 9 + ... + 2.27 2 + 1.271 . Solución 3: Para evitar que se produzcan resultados demasiado elevados, dividimos el resultado de la función de la solución anterior entre el tamaño de la tabla hash y tomamos como resultado de la nueva función hash el resto de esta división. F ( Palabra) = Polinomio( Palabra) mod Tamaño _ Tabla Problemas: Se producen muchas colisiones debido a la reducción del resultado de la función hash con el “mod”. Solución 4: Programación. Tema 8: Tablas Hash 16/Mayo/2004 Para arreglar el problema de las colisiones hay dos posibles opciones: 9 Usar memoria dinámica: Cada posición de la tabla hash contiene una lista o un árbol. De esta forma, si a dos palabras les corresponde la misma posición de la tabla, lo único que hay que hacer es insertar ambas en la lista o en el árbol, según sea el caso. 9 Si la solución anterior no es viable, hay que buscar otra manera de arreglar las colisiones. Esta solución consistiría en buscar, mediante saltos, otra posición de inserción de la palabra a partir de la posición que le corresponde según la función hash, sólo en el caso de que esta posición esté ocupada, es decir, sólo si se produce una colisión. Hay varias implementaciones de esta solución en función del salto que se utilice: 1. Salto = Posición + n, con n = 1, 2, ... De esta forma insertaremos la nueva palabra en la posición no ocupada más próxima a partir de la posición que le correspondería en realidad. Por ejemplo: Queremos insertar “72”, pero la posición que le corresponde (posición 2) ya está ocupada, 0 1 2 52 3 4 5 6 7 67 8 28 por lo que insertaremos “72” en la posición vacía más próxima a 2 a partir de ésta 0 1 2 52 3 72 4 102 5 83 6 7 67 8 28 9 Problemas: 0 1 2 52 3 72 De la misma forma que antes, vamos a insertar “102” 1 2 52 3 72 4 4 102 5 5 6 6 7 67 7 67 8 28 8 28 9 9 Ahora queremos insertar “83”, pero la posición que le corresponde (posición 3) ya está ocupada. 0 9 Programación. Tema 8: Tablas Hash 16/Mayo/2004 Las palabras tienden a concentrarse en ciertas zonas de la tabla, dejando grandes espacios en blanco entre cada grupo de posiciones ocupadas. 2. Salto = Posición + n2, con n = 1, 2, ... De esta forma, evitamos el apelotonamiento de datos. Problemas: Si queremos insertar una palabra y justo las posiciones x + 12, x + 22, x + 32, ... están ocupadas, mientras que las que quedan en medio están vacías, va a tardar demasiado en encontrar un hueco libre en la tabla cuando en realidad se ha pasado por alto unos cuantos. Queremos insertar “12”, pero la posición que le corresponde (posición 2) ya está ocupada Æ buscamos la posición de inserción: 0 1 2 52 3 72 4 5 6 7 8 102 x + 1 = 3 Æ Ya está ocupada. x + 4 = 6 Æ Ya está ocupada. x + 9 = 11 Æ Se sale de la tabla, con lo que contamos a partir del principio: 11 – 9 = 2 Æ Ya está ocupada. ... Así entraríamos en un bucle sin fin, pues las posiciones se van a repetir y, sin embargo, todavía nos quedan 7 posiciones libres en la tabla. 9 3. Salto variable: el valor de este salto no puede ser aleatorio, ya que de ser así, una vez insertada una palabra no seríamos capaces de encontrarla en la tabla, pues no sabríamos cuánto hay que saltar a partir de su posición resultante de la función hash. Una posible solución es calcular este salto por medio de otra función hash que resulte un número más pequeño: Salto = k − (( Polinomio( Palabra )) mod k ) donde k es el máximo salto que permitimos (lo elegimos nosotros). Algoritmos en Ada: function Posición_Hash ( Palabra : in String; Tamaño_Tabla : in Positive) return Natural is Programación. Tema 8: Tablas Hash 16/Mayo/2004 Posición : Natural := 0; begin for I in Palabra’Range loop Posición := (Posición + (I * Character’Pos(Palabra(I) **I))) mod Tamaño_Tabla; end loop; return Posición; end Posición_Hash; function salto_Hash (Palabra : in String) return Integer is begin return Character’Pos (Palabra(I)) mod 15; end Salto_Hash; Observaciones importantes: ª Se recomienda fijar el tamaño de la tabla hash como el doble del necesario para almacenar todos los elementos. ª Si queremos ampliar el tamaño de la tabla, como la función hash depende del tamaño de ésta, hay que recalcular las posiciones de todos lo elementos e insertarlos en estas nuevas posiciones (NO se puede copiar directamente el contenido de una tabla a otra). ª El tamaño de la tabla debe ser un número primo para evitar bucles infinitos como ocurría en el ejemplo del segundo tipo de salto. ª Se recomienda no permitir datos duplicados a no ser que vayamos a implementar la tabla hash con listas, ya que si se almacena todo en el array la gestión de la información para insertar y buscar se complica. ª En caso de que permitiéramos datos duplicados en una tabla implementada con saltos, necesitaríamos introducir un nuevo campo “Borrado” de tipo boolean en cada posición que nos diga si el dato contenido en esa posición ha sido borrado o todavía no se ha insertado nada en ella. Observación. Esto es así porque, al haber datos duplicados, el segundo elemento que insertemos se irá a colocar en una posición de la tabla que no se corresponde con el resultado de la primera función hash. En caso de que no usásemos la variable boolean, si borrásemos el elemento que había en la primera posición, cuando intentásemos buscar el segundo elemento se elevaría la excepción No_Encontrado pues la posición que le corresponde está vacía. Sin embargo, con el boolean, como al borrar el primer elemento habríamos puesto esta variable a True, al buscar el segundo elemento basta con seguir calculando saltos hasta Programación. Tema 8: Tablas Hash 16/Mayo/2004 que lo encontremos, hasta que en una posición en la que debería estar el elemento el campo “Borrado” esté puesto a False o hasta que la posición calculada con saltos sea de nuevo igual a la posición de la que partimos. Lo vemos mejor con un ejemplo: Supongamos un array implementado con el primer tipo de salto, es decir, saltos a posiciones consecutivas: 0 1 2 3 4 5 6 7 8 9 Dato :0 Borrado : False Dato : 31 Borrado : False Dato : 12 Borrado : False Dato : 32 Borrado : False Dato : 44 Borrado : False Dato : 15 Borrado : False Dato : 42 Borrado : False Dato : 12 Borrado : False Dato :0 Borrado : False Dato :0 Borrado : False 0 1 Borramos “12” 2 3 4 5 6 7 8 9 Dato :0 Borrado : False Dato : 31 Borrado : False Dato :0 Borrado : True Dato : 32 Borrado : False Dato : 44 Borrado : False Dato : 15 Borrado : False Dato : 42 Borrado : False Dato : 12 Borrado : False Dato :0 Borrado : False Dato :0 Borrado : False Si ahora quisiéramos buscar el elemento “12” los pasos que seguiríamos serían: 1. Posición (12) = 12 mod 10 = 2 2. Comprobamos el campo “Borrado” de la posición 2 de la tabla. Como está puesto a True, seguimos buscando. 3. Comprobamos el “Borrado” de la posición 3. Como está a False, miramos a ver si el dato coincide con el que estamos buscando. Como no es así, seguimos buscando. .... 7. Comprobamos el “Borrado” de la posición 7. Como está a False, miramos a ver si el dato coincide con el que estamos buscando. Como esto último se cumple, retornamos True. Programación. Tema 8: Tablas Hash 16/Mayo/2004