Estamos acostumbrados a decir que las HashMap
get/put
operaciones son O (1). Sin embargo, depende de la implementación de hash. El hash de objeto predeterminado es en realidad la dirección interna en el montón JVM. ¿Estamos seguros de que es lo suficientemente bueno para afirmar que get/put
son O (1)?
La memoria disponible es otro problema. Según tengo entendido por los javadocs, el HashMap
load factor
debería ser 0.75. ¿Qué pasa si no tenemos suficiente memoria en JVM y load factor
excede el límite?
Entonces, parece que O (1) no está garantizado. ¿Tiene sentido o me estoy perdiendo algo?
Respuestas:
Depende de muchas cosas. Por lo general, es O (1), con un hash decente que en sí mismo es tiempo constante ... pero podría tener un hash que demore mucho en calcular, y si hay varios elementos en el mapa hash que devuelven el mismo código hash,
get
tendrá que iterar sobre ellos llamandoequals
a cada uno de ellos para encontrar una coincidencia.En el peor de los casos, a
HashMap
tiene una búsqueda O (n) debido a que recorre todas las entradas en el mismo depósito de hash (por ejemplo, si todas tienen el mismo código de hash). Afortunadamente, el peor de los casos no aparece muy a menudo en la vida real, en mi experiencia. Entonces, no, O (1) ciertamente no está garantizado, pero generalmente es lo que debe asumir al considerar qué algoritmos y estructuras de datos usar.En JDK 8,
HashMap
se ha ajustado de modo que si las claves se pueden comparar para ordenar, cualquier cubo densamente poblado se implementa como un árbol, de modo que incluso si hay muchas entradas con el mismo código hash, la complejidad es O (log norte). Eso puede causar problemas si tiene un tipo de clave donde la igualdad y el orden son diferentes, por supuesto.Y sí, si no tiene suficiente memoria para el mapa hash, tendrá problemas ... pero eso será cierto independientemente de la estructura de datos que utilice.
fuente
put
se "amortiza O (1)", generalmente O (1), ocasionalmente O (n), pero rara vez es suficiente para compensar.No estoy seguro de que el código hash predeterminado sea la dirección: leí la fuente de OpenJDK para la generación de código hash hace un tiempo, y recuerdo que era algo un poco más complicado. Todavía no es algo que garantice una buena distribución, tal vez. Sin embargo, eso es hasta cierto punto discutible, ya que pocas clases que usarías como claves en un hashmap usan el hashcode predeterminado: proporcionan sus propias implementaciones, lo que debería ser bueno.
Además de eso, lo que quizás no sepa (una vez más, esto se basa en la fuente de lectura, no está garantizado) es que HashMap agita el hash antes de usarlo, para mezclar entropía de toda la palabra en los bits inferiores, que es donde está necesario para todos menos los hashmaps más grandes. Eso ayuda a lidiar con hashes que específicamente no lo hacen ellos mismos, aunque no puedo pensar en ningún caso común en el que lo veas.
Finalmente, lo que sucede cuando la tabla se sobrecarga es que se degenera en un conjunto de listas vinculadas en paralelo: el rendimiento se convierte en O (n). Específicamente, el número de enlaces atravesados será en promedio la mitad del factor de carga.
fuente
La operación HashMap es un factor dependiente de la implementación de hashCode. Para el escenario ideal, digamos la buena implementación de hash que proporciona un código de hash único para cada objeto (sin colisión de hash), el mejor, el peor y el escenario promedio sería O (1). Consideremos un escenario donde una mala implementación de hashCode siempre devuelve 1 o un hash que tiene una colisión de hash. En este caso, la complejidad del tiempo sería O (n).
Ahora, llegando a la segunda parte de la pregunta sobre la memoria, entonces sí, JVM se ocuparía de la restricción de memoria.
fuente
Ya se ha mencionado que los hashmaps son
O(n/m)
en promedio, sin
es el número de elementos ym
el tamaño. También se ha mencionado que, en principio, todo podría colapsar en una lista vinculada individualmente con elO(n)
tiempo de consulta. (Todo esto supone que calcular el hash es tiempo constante).Sin embargo, lo que no se menciona a menudo es que, al menos con probabilidad
1-1/n
(por lo tanto, para 1000 artículos con una probabilidad del 99.9%) ¡el cubo más grande no se llenará más queO(logn)
! Por lo tanto, coincide con la complejidad promedio de los árboles de búsqueda binarios. (Y la constante es buena, un límite más estricto es(log n)*(m/n) + O(1)
).Todo lo que se requiere para este límite teórico es que use una función hash razonablemente buena (consulte Wikipedia: Universal Hashing . Puede ser tan simple como
a*x>>m
). Y, por supuesto, la persona que le da los valores al hash no sabe cómo ha elegido sus constantes aleatorias.TL; DR: con probabilidad muy alta, el peor de los casos es la complejidad get / put de un hashmap
O(logn)
.fuente
Estoy de acuerdo con:
hashCode()
implementación podría dar lugar a múltiples colisiones, lo que significa que, en el peor de los casos, cada objeto va al mismo depósito, por lo tanto, O ( N ) si cada depósito está respaldado por aList
.HashMap
reemplaza dinámicamente los nodos (lista vinculada) utilizados en cada depósito con TreeNodes (árbol rojo-negro cuando una lista se hace más grande que 8 elementos), lo que resulta en un peor rendimiento de O ( logN ).Pero, esto NO es toda la verdad si queremos ser 100% precisos. La implementación
hashCode()
y el tipo de claveObject
(inmutable / en caché o ser una Colección) también puede afectar la complejidad real en términos estrictos.Asumamos los siguientes tres casos:
HashMap<Integer, V>
HashMap<String, V>
HashMap<List<E>, V>
¿Tienen la misma complejidad? Bueno, la complejidad amortizada del primero es, como se esperaba, O (1). Pero, por lo demás, también necesitamos calcular
hashCode()
el elemento de búsqueda, lo que significa que podríamos tener que atravesar matrices y listas en nuestro algoritmo.Supongamos que el tamaño de todas las matrices / listas anteriores es k . Entonces,
HashMap<String, V>
yHashMap<List<E>, V>
tendrá O (k) complejidad amortizada y, de manera similar, O ( k + logN ) en el peor de los casos en Java8.* Tenga en cuenta que el uso de una
String
clave es un caso más complejo, ya que es inmutable y Java almacena el resultadohashCode()
en una variable privadahash
, por lo que solo se calcula una vez.Pero, lo anterior también tiene su propio peor caso, porque la
String.hashCode()
implementación de Java está verificando sihash == 0
antes de computarhashCode
. Pero bueno, hay cadenas no vacías quehashcode
generan un cero, como "f5a5a608", vea aquí , en cuyo caso la memorización podría no ser útil.fuente
En la práctica, es O (1), pero en realidad es una simplificación terrible y matemáticamente sin sentido. La notación O () dice cómo se comporta el algoritmo cuando el tamaño del problema tiende al infinito. Hashmap get / put funciona como un algoritmo O (1) para un tamaño limitado. El límite es bastante grande desde la memoria de la computadora y desde el punto de vista del direccionamiento, pero lejos del infinito.
Cuando uno dice que el hashmap get / put es O (1) realmente debería decir que el tiempo necesario para el get / put es más o menos constante y no depende del número de elementos en el hashmap hasta donde el hashmap puede ser presentado en el sistema informático real. Si el problema va más allá de ese tamaño y necesitamos mapas hash más grandes, luego de un tiempo, sin duda, el número de bits que describen un elemento también aumentará a medida que se agoten los posibles elementos diferentes que se pueden describir. Por ejemplo, si usamos un hashmap para almacenar números de 32 bits y luego aumentamos el tamaño del problema para que tengamos más de 2 ^ 32 elementos de bit en el hashmap, entonces los elementos individuales se describirán con más de 32 bits.
El número de bits necesarios para describir los elementos individuales es log (N), donde N es el número máximo de elementos, por lo tanto, get and put son realmente O (log N).
Si lo compara con un conjunto de árbol, que es O (log n), entonces el conjunto de hash es O (long (max (n)) y simplemente sentimos que esto es O (1), porque en cierta implementación max (n) es fijo, no cambia (el tamaño de los objetos que almacenamos se mide en bits) y el algoritmo que calcula el código hash es rápido.
Finalmente, si encontrar un elemento en cualquier estructura de datos fuera O (1), crearíamos información de la nada. Teniendo una estructura de datos de n elemento, puedo seleccionar un elemento de n manera diferente. Con eso, puedo codificar la información de bit de registro (n). Si puedo codificar eso en cero bits (eso es lo que significa O (1)), entonces creé un algoritmo ZIP de compresión infinita.
fuente
O(log(n) * log(max(n)))
, entonces? Si bien la comparación en cada nodo puede ser más inteligente, en el peor de los casos necesita inspeccionar todos losO(log(max(n))
bits, ¿verdad?