En informática , una función hash perfecta h para un conjunto S es una función hash que asigna elementos distintos de S a un conjunto de m enteros, sin colisiones . En términos matemáticos, es una función inyectiva .
Las funciones hash perfectas se pueden utilizar para implementar una tabla de búsqueda con un tiempo de acceso constante en el peor de los casos. Una función hash perfecta se puede utilizar , como cualquier función hash , para implementar tablas hash , con la ventaja de que no se debe implementar ninguna resolución de colisiones . Además, si las claves no están en los datos y se sabe que las claves consultadas serán válidas, no es necesario almacenarlas en la tabla de búsqueda, lo que ahorra espacio.
Las desventajas de las funciones hash perfectas son que se debe conocer S para la construcción de la función hash perfecta. Las funciones hash perfectas no dinámicas deben reconstruirse si S cambia. Para S que cambia con frecuencia, se pueden usar funciones hash perfectas dinámicas a costa de espacio adicional. [1] El requisito de espacio para almacenar la función hash perfecta es en O ( n ) donde n es el número de claves en la estructura.
Los parámetros de rendimiento importantes para las funciones hash perfectas son el tiempo de evaluación, que debe ser constante, el tiempo de construcción y el tamaño de representación.
Una función hash perfecta con valores en un rango limitado puede utilizarse para operaciones de búsqueda eficientes, colocando claves de S (u otros valores asociados) en una tabla de búsqueda indexada por la salida de la función. Luego, se puede comprobar si una clave está presente en S , o buscar un valor asociado con esa clave, buscándolo en su celda de la tabla. Cada una de esas búsquedas lleva un tiempo constante en el peor de los casos . [2] Con un hash perfecto, los datos asociados se pueden leer o escribir con un solo acceso a la tabla. [3]
Los parámetros de rendimiento importantes para un hash perfecto son el tamaño de la representación, el tiempo de evaluación, el tiempo de construcción y, además, el requisito de rango (número promedio de contenedores por clave en la tabla hash). [4] El tiempo de evaluación puede ser tan rápido como O ( 1 ) , lo cual es óptimo. [2] [4] El tiempo de construcción debe ser al menos O ( n ) , porque cada elemento en S necesita ser considerado, y S contiene n elementos. Este límite inferior se puede lograr en la práctica. [4]
El límite inferior para el tamaño de la representación depende de m y n . Sea m = (1+ε) n y h una función hash perfecta. Una buena aproximación para el límite inferior es Bits por elemento. Para un hash perfecto mínimo, ε = 0 , el límite inferior es log e ≈ 1,44 bits por elemento. [4]
Una función hash perfecta para un conjunto específico S que se puede evaluar en tiempo constante, y con valores en un rango pequeño, se puede encontrar mediante un algoritmo aleatorio en un número de operaciones que es proporcional al tamaño de S. La construcción original de Fredman, Komlós y Szemerédi (1984) utiliza un esquema de dos niveles para mapear un conjunto S de n elementos a un rango de índices O ( n ) y luego mapear cada índice a un rango de valores hash. El primer nivel de su construcción elige un primo grande p (más grande que el tamaño del universo del cual se extrae S ), y un parámetro k , y mapea cada elemento x de S al índice
Si k se elige aleatoriamente, es probable que este paso tenga colisiones, pero es probable que el número de elementos n i que se asignan simultáneamente al mismo índice i sea pequeño. El segundo nivel de su construcción asigna rangos disjuntos de O ( n i 2 ) enteros a cada índice i . Utiliza un segundo conjunto de funciones modulares lineales, una para cada índice i , para asignar cada miembro x de S al rango asociado con g ( x ) . [2]
Como muestran Fredman, Komlós y Szemerédi (1984), existe una elección del parámetro k tal que la suma de las longitudes de los rangos para los n valores diferentes de g ( x ) sea O ( n ) . Además, para cada valor de g ( x ) , existe una función modular lineal que mapea el subconjunto correspondiente de S en el rango asociado con ese valor. Tanto k como las funciones de segundo nivel para cada valor de g ( x ) , se pueden encontrar en tiempo polinomial eligiendo valores aleatoriamente hasta encontrar uno que funcione. [2]
La función hash en sí misma requiere espacio de almacenamiento O ( n ) para almacenar k , p y todas las funciones modulares lineales de segundo nivel. El cálculo del valor hash de una clave dada x se puede realizar en tiempo constante calculando g ( x ) , buscando la función de segundo nivel asociada con g ( x ) y aplicando esta función a x . Se puede utilizar una versión modificada de este esquema de dos niveles con una mayor cantidad de valores en el nivel superior para construir una función hash perfecta que mapee S en un rango más pequeño de longitud n + o ( n ) . [2]
Belazzougui, Botelho y Dietzfelbinger (2009) describen un método más reciente para construir una función hash perfecta como "hash, desplazamiento y compresión". Aquí también se utiliza una función hash de primer nivel g para mapear elementos en un rango de números enteros r . Un elemento x ∈ S se almacena en el contenedor B g(x) . [4]
Luego, en orden descendente de tamaño, los elementos de cada contenedor se codifican mediante una función hash de una secuencia de funciones hash independientes completamente aleatorias (Φ 1 , Φ 2 , Φ 3 , ...) , comenzando con Φ 1 . Si la función hash no produce ninguna colisión para el contenedor, y los valores resultantes aún no están ocupados por otros elementos de otros contenedores, se elige la función para ese contenedor. Si no, se prueba la siguiente función hash en la secuencia. [4]
Para evaluar la función hash perfecta h ( x ) solo hay que guardar la asignación σ del índice de cubo g ( x ) en la función hash correcta en la secuencia, lo que da como resultado h(x) = Φ σ(g(x)) . [4]
Finalmente, para reducir el tamaño de la representación, los ( σ(i)) 0 ≤ i < r se comprimen en una forma que aún permite la evaluación en O ( 1 ) . [4]
Este enfoque necesita un tiempo lineal en n para la construcción y un tiempo de evaluación constante. El tamaño de la representación está en O ( n ) y depende del rango alcanzado. Por ejemplo, con m = 1,23 n, Belazzougui, Botelho y Dietzfelbinger (2009) lograron un tamaño de representación entre 3,03 bits/clave y 1,40 bits/clave para su conjunto de ejemplo dado de 10 millones de entradas, y los valores más bajos necesitan un mayor tiempo de cálculo. El límite inferior del espacio en este escenario es 0,88 bits/clave. [4]
El algoritmo hash, desplazar y comprimir es (1) Dividir S en grupos B i := g −1 ({i})∩S,0 ≤ i < r (2) Ordenar los grupos B i en orden descendente según el tamaño |B i |(3) Inicializar la matriz T[0...m-1] con 0(4) para todo i ∈[r], en el orden de (2), haga (5) para l ← 1,2,...(6) repetir la formación de K i ← { Φ l (x)|x ∈ B i }(6) hasta que |K i |=|B i | y K i ∩{j|T[j]=1}= ∅(7) sea σ(i):= el l exitoso(8) para todo j ∈ K i sea T[j]:= 1(9) Transforme (σ i ) 0≤i<r en forma comprimida, conservando el acceso O ( 1 ) .
El uso de O ( n ) palabras de información para almacenar la función de Fredman, Komlós y Szemerédi (1984) es casi óptimo: cualquier función hash perfecta que pueda calcularse en tiempo constante requiere al menos un número de bits que sea proporcional al tamaño de S. [5 ]
Para funciones hash mínimas perfectas, el límite inferior del espacio teórico de la información es
bits/llave. [4]
Para funciones hash perfectas, primero se supone que el rango de h está acotado por n como m = (1+ε) n . Con la fórmula dada por Belazzougui, Botelho y Dietzfelbinger (2009) y para un universo cuyo tamaño | U | = u tiende hacia el infinito, los límites inferiores del espacio son
bits/clave, menos log( n ) bits en total. [4]
Un ejemplo trivial pero generalizado de hash perfecto está implícito en el espacio de direcciones de memoria (virtual) de una computadora. Dado que cada byte de memoria virtual es una ubicación de almacenamiento distinta, única y directamente direccionable, el valor de la dirección inicial donde se almacena cualquier objeto en la memoria puede considerarse un hash perfecto de facto de ese objeto en todo el rango de direcciones de memoria. [6]
El uso de una función hash perfecta es mejor en situaciones en las que hay un conjunto grande, S , que se consulta con frecuencia y que rara vez se actualiza. Esto se debe a que cualquier modificación del conjunto S puede hacer que la función hash ya no sea perfecta para el conjunto modificado. Las soluciones que actualizan la función hash cada vez que se modifica el conjunto se conocen como hash perfecto dinámico [1] , pero estos métodos son relativamente complicados de implementar.
Una función hash mínima perfecta es una función hash perfecta que asigna n claves a n enteros consecutivos, generalmente los números de 0 a n − 1 o de 1 a n . Una forma más formal de expresar esto es: Sean j y k elementos de algún conjunto finito S . Entonces h es una función hash mínima perfecta si y solo si h ( j ) = h ( k ) implica j = k ( inyectividad ) y existe un entero a tal que el rango de h es a .. a + | S | − 1 . Se ha demostrado que un esquema hash mínimo perfecto de propósito general requiere al menos bits/clave. [4] Suponiendo que es un conjunto de tamaño que contiene enteros en el rango , se sabe cómo construir eficientemente una función hash mínima perfecta explícita de a que usa bits espaciales y que admite un tiempo de evaluación constante. [7] En la práctica, hay esquemas hash mínimos perfectos que usan aproximadamente 1,56 bits/clave si se les da suficiente tiempo. [8]
Una función hash es k -perfecta si, como máximo, k elementos de S se asignan al mismo valor en el rango. El algoritmo "hash, desplazamiento y compresión" se puede utilizar para construir funciones hash k -perfectas permitiendo hasta k colisiones. Los cambios necesarios para lograr esto son mínimos y se subrayan en el pseudocódigo adaptado que aparece a continuación:
(4) para todo i ∈[r], en el orden de (2), haga (5) para l ← 1,2,...(6) repetir la formación de K i ← { Φ l (x)|x ∈ B i }(6) hasta que |K i |=|B i | y K i ∩{j| T[j]=k }= ∅(7) sea σ(i):= el l exitoso(8) para todo j ∈ K i establece T[j]←T[j]+1
Una función hash mínima perfecta F preserva el orden si las claves se dan en algún orden a 1 , a 2 , ..., a n y para cualquier clave a j y a k , j < k implica F ( a j ) < F( a k ) . [9] En este caso, el valor de la función es simplemente la posición de cada clave en el orden ordenado de todas las claves. Una implementación simple de funciones hash mínimas perfectas que preservan el orden con un tiempo de acceso constante es usar una función hash perfecta (ordinaria) para almacenar una tabla de búsqueda de las posiciones de cada clave. Esta solución usa bits, lo cual es óptimo en el entorno donde la función de comparación para las claves puede ser arbitraria. [10] Sin embargo, si las claves a 1 , a 2 , ..., a n son números enteros extraídos de un universo , entonces es posible construir una función hash que preserve el orden usando solo bits de espacio. [11] Además, se sabe que este límite es óptimo. [12]
Si bien las tablas hash bien dimensionadas tienen un tiempo O(1) promedio amortizado (tiempo constante promedio amortizado) para búsquedas, inserciones y eliminación, la mayoría de los algoritmos de tablas hash sufren posibles tiempos en el peor de los casos que toman mucho más tiempo. Un tiempo O(1) en el peor de los casos (tiempo constante incluso en el peor de los casos) sería mejor para muchas aplicaciones (incluidos los enrutadores de red y las memorias caché ). [13] : 41
Pocos algoritmos de tabla hash admiten un tiempo de búsqueda O(1) en el peor de los casos (tiempo de búsqueda constante incluso en el peor de los casos). Los pocos que lo hacen incluyen: hash perfecto; hash perfecto dinámico ; hash cuckoo ; hash hopscotch ; y hash extensible . [13] : 42–69
Una alternativa sencilla al hash perfecto, que también permite actualizaciones dinámicas, es el hash cuckoo . Este esquema asigna claves a dos o más ubicaciones dentro de un rango (a diferencia del hash perfecto que asigna cada clave a una sola ubicación), pero lo hace de tal manera que las claves se pueden asignar una a una a las ubicaciones a las que han sido asignadas. Las búsquedas con este esquema son más lentas, porque se deben verificar múltiples ubicaciones, pero sin embargo toman un tiempo constante en el peor de los casos. [14]