En esta respuesta, profundizaré un poco en cómo las tablas hash se convierten en O (n) en lugar de O (1).
Una tabla hash básica es:
- Una función hash que mapea un espacio grande en un espacio pequeño
- Una matriz del tamaño del espacio pequeño – llame a ese tamaño M – con el comportamiento de búsqueda O (1) porque usamos la salida del hash como índice de matriz
- M contenedor o elementos de matriz de lista (“cubos”) cada uno con el comportamiento de búsqueda O (K), donde K es el número de elementos en el cubo.
La búsqueda de la tabla hash es O (1) si y solo si O (K) == O (1). Esto sucede cuando se cumple lo siguiente: el hash es perfecto, distribuyendo datos de manera uniforme entre los cubos; y, hay más cubos M que elementos de datos n , por lo que a lo sumo un elemento por cubo. *
- He practicado más de 300 preguntas de algoritmos en LintCode y LeetCode. He estado desempleado durante casi 9 meses y obtuve 8 entrevistas y todas fallaron en la prueba de codificación. Todavía no puedo recibir ninguna oferta. ¿Qué tengo que hacer?
- ¿Qué es la representación de colas usando array?
- ¿Por qué es Forth el lenguaje de programación para practicar la escritura de algoritmos?
- Supongamos que eliminamos un borde de un árbol de expansión y luego agregamos un borde diferente para que permanezca conectado. ¿Seguirá siendo un árbol de expansión?
- En el algoritmo EM, ¿debería aumentar el valor de la función objetivo a través de cada M-STEP?
La violación de cualquiera de estas condiciones conduce a un comportamiento de búsqueda de O (n).
Puede violar una condición O (1) usted mismo especificando una función hash terrible. Ejemplo:
size_t hash (mydatatype X) {return 1; } // Este es un hash terrible
Con esta función hash, todo entra en el mismo depósito sin importar cuántos depósitos haya, y O (K) == O (n); su tabla hash ahora es O (n).
Algunas implementaciones de tablas hash intentan mitigar terribles funciones hash volviendo a aplicar hash a su hash, y muchas también cambian dinámicamente el tamaño de la matriz de cubos para mantener M > n usando su módulo de hash poderes sucesivos de dos como índice. En condiciones ideales, esto conduce a algo llamado comportamiento de búsqueda de O (1) amortizado . Puede pensarlo como un comportamiento O (1) la mayor parte del tiempo, con un pase ocasional de O (n) para duplicar el tamaño de la matriz y reequilibrar la tabla hash.
La búsqueda de la tabla hash se amortiza O (1) si y solo si el algoritmo de amortización puede mantener M > n para todos los n razonables. Si no, la tabla hash comenzará O (1) y se degradará a O (n) para valores grandes de n . Que es grande La respuesta podría ser 2 ^ 30, o podría ser 1.
Como programador, puede violar las condiciones de O (1) amortizadas con una función hash inadecuada. Los ejemplos incluyen funciones hash que no son “buenas” módulo 2 ^ b para cualquier b, y funciones hash que solo producen P valores únicos. El algoritmo de amortización podría mitigar el primero de estos si vuelve a ajustar el hash, pero no hay nada que pueda hacer sobre el segundo: una vez que n excede P , efectivamente tenemos M == P sin importar cuán grande sea n ; el número promedio de artículos en cada cubo se vuelve proporcional a n (es decir, K = n / P ); O (K) == O (n).
Tres cosas que usted como programador no controla puede violar una condición O (1) u O (1) amortizada a medida que n aumenta. Estos son:
- El código de la tabla hash. Puede fallar en la protección contra las malas funciones hash, y puede, por sí mismo, imponer un límite superior en M que es más pequeño que su conjunto de datos n . Una vez que M no puede crecer, O (K) == O (n).
- El sistema operativo en el que se ejecuta el código de la tabla hash. Una matriz asignada dinámicamente solo puede ser tan grande como la asignación de memoria contigua más grande admitida por el sistema operativo. Una vez que la matriz no puede ser más grande, M no puede ser más grande, O (K) == O (n). En los sistemas operativos de 16 bits con administración de memoria, la asignación máxima, es decir (size_t) (- 1) , es de solo 64 K, aunque el espacio de proceso podría ser de 16 MB. Si el tamaño de un puntero es de 3 a 4 bytes, M alcanza un máximo de 16,000 a 22,000. Una vez que n excede eso, O (K) == O (n).
- La arquitectura de memoria del hardware en el que se ejecuta el código de la tabla hash. La mayoría de los programadores están acostumbrados a trabajar en CPU de 32 bits o 64 bits con espacio de direcciones lineal. Los sistemas embebidos (incluyendo cualquier cosa que ejecute una CPU 80286) y consolas de juegos económicas (incluida cualquier cosa que ejecute una CPU 65816) pueden tener arquitecturas de memoria segmentadas, almacenadas o asignadas por página en las que la memoria contigua direccionable más grande es 32K o 64K. En dicho sistema, es difícil o ineficiente implementar una matriz más grande que esta, incluso a nivel de lenguaje ensamblador. Una implementación sencilla de un mapa hash en un sistema de este tipo alcanza un máximo de M entre 16,000 y 22,000. Una vez que n excede eso, O (K) == O (n).
Todo programador debe saber estas cosas sobre las tablas hash y las funciones hash para tomar decisiones informadas al escribir código. No asuma ciegamente que su tabla hash será O (1) o O (1) amortizada. Verifique sus casos de uso y su función de hashing, y si cumplen con los criterios O (1) u O (1) amortizado, ¡elimine el hash!
* Más precisamente, un número nominalmente constante de artículos por cubo; no debe crecer como log n o como n etc.