La documentación lo explica bastante bien:
Una instancia de HashMap tiene dos parámetros que afectan su rendimiento: capacidad inicial y factor de carga. La capacidad es el número de cubos en la tabla hash, y la capacidad inicial es simplemente la capacidad en el momento en que se crea la tabla hash. El factor de carga es una medida de cuán llena se permite que llegue la tabla hash antes de que su capacidad aumente automáticamente. Cuando el número de entradas en la tabla hash excede el producto del factor de carga y la capacidad actual, la tabla hash se vuelve a aplicar (es decir, se reconstruyen las estructuras de datos internas) para que la tabla hash tenga aproximadamente el doble de la cantidad de depósitos.
Como regla general, el factor de carga predeterminado (.75) ofrece una buena compensación entre los costos de tiempo y espacio. Los valores más altos disminuyen la sobrecarga de espacio pero aumentan el costo de búsqueda (reflejado en la mayoría de las operaciones de la clase HashMap, incluidas get y put). El número esperado de entradas en el mapa y su factor de carga deben tenerse en cuenta al establecer su capacidad inicial, para minimizar el número de operaciones de repetición. Si la capacidad inicial es mayor que el número máximo de entradas dividido por el factor de carga, nunca se realizarán operaciones de repetición.
Al igual que con todas las optimizaciones de rendimiento, es una buena idea evitar optimizar las cosas prematuramente (es decir, sin datos precisos sobre dónde están los cuellos de botella).
capacity = N/0.75
para evitar repetir, pero mi pensamiento inicial se acaba de establecerload factor = 1
. ¿Habría inconvenientes en ese enfoque? ¿Por qué afectaría el factor de cargaget()
y losput()
costos de operación?La capacidad inicial predeterminada de las
HashMap
tomas es de 16 y el factor de carga es de 0,75 f (es decir, el 75% del tamaño actual del mapa). El factor de carga representa a qué nivel seHashMap
debe duplicar la capacidad.Por ejemplo, producto de capacidad y factor de carga como
16 * 0.75 = 12
. Esto representa que después de almacenar el duodécimo par clave-valor enHashMap
, su capacidad se convierte en 32.fuente
En realidad, según mis cálculos, el factor de carga "perfecto" está más cerca del log 2 (~ 0.7). Aunque cualquier factor de carga inferior a este producirá un mejor rendimiento. Creo que .75 probablemente fue sacado de un sombrero.
Prueba:
Se puede evitar el encadenamiento y se puede explotar la predicción de rama al predecir si un cubo está vacío o no. Un cubo probablemente esté vacío si la probabilidad de que esté vacío exceda de .5.
Vamos a representar el tamaño yn el número de claves agregadas. Usando el teorema binomial, la probabilidad de que un cubo esté vacío es:
Por lo tanto, un cubo probablemente esté vacío si hay menos de
Cuando s alcanza el infinito y si el número de claves agregadas es tal que P (0) = .5, entonces n / s se acerca rápidamente a log (2):
fuente
.75
se redondeó a la fracción más fácil de entenderlog(2)
, y parece menos un número mágico. Me encantaría ver una actualización del valor predeterminado de JDK, con dicho comentario sobre su implementación: D¿Qué es el factor de carga?
¿La cantidad de capacidad que se debe agotar para que HashMap aumente su capacidad?
¿Por qué factor de carga?
El factor de carga es por defecto 0.75 de la capacidad inicial (16), por lo tanto, el 25% de los cubos estarán libres antes de que haya un aumento en la capacidad y esto hace que muchos nuevos cubos con nuevos códigos hash apunten a que existan justo después del aumento en el Número de cubos.
Ahora, ¿por qué debería mantener muchos cubos libres y cuál es el impacto de mantener cubos libres en el rendimiento?
Si configura el factor de carga para que diga 1.0, puede suceder algo muy interesante.
Supongamos que está agregando un objeto x a su mapa hash cuyo código hash es 888 y en su mapa hash el depósito que representa el código hash es libre, por lo que el objeto x se agrega al depósito, pero ahora diga nuevamente si está agregando otro objeto y cuyo código hash es también 888, entonces su objeto y se agregará con seguridad PERO al final del depósito ( porque los depósitos no son más que enlaces Implementación de lista de almacenamiento de clave, valor y siguiente ) ahora esto tiene un impacto en el rendimiento. Dado que su objeto y ya no está presente en la cabeza del cubo si realiza una búsqueda, el tiempo necesario no será O (1)esta vez depende de cuántos elementos hay en el mismo cubo. Por cierto, esto se llama colisión hash y esto incluso ocurre cuando su factor de carga es inferior a 1.
¿Correlación entre rendimiento, colisión hash y factor de carga?
Factor de carga más bajo = más cucharones libres = menos posibilidades de colisión = alto rendimiento = alto requisito de espacio.
Corrígeme si estoy equivocado en alguna parte.
fuente
LinkedList
que se conoce comoAmortized Constant Execution Time
y denota con un+
tanO(1)+
De la documentación :
Realmente depende de sus requisitos particulares, no existe una "regla general" para especificar un factor de carga inicial.
fuente
Para HashMap DEFAULT_INITIAL_CAPACITY = 16 y DEFAULT_LOAD_FACTOR = 0.75f significa que el número MÁXIMO de TODAS las entradas en HashMap = 16 * 0.75 = 12 . Cuando se agregue el decimotercer elemento, ¡la capacidad (tamaño de matriz) de HashMap se duplicará! La ilustración perfecta respondió a esta pregunta: la imagen se toma desde aquí:
https://javabypatel.blogspot.com/2015/10/what-is-load-factor-and-rehashing-in-hashmap.html
fuente
Si los cubos se llenan demasiado, entonces tenemos que mirar a través de
Una larga lista vinculada.
Y eso es como derrotar el punto.
Así que aquí hay un ejemplo donde tengo cuatro cubos.
Tengo elefante y tejón en mi HashSet hasta ahora.
Esta es una muy buena situación, ¿verdad?
Cada elemento tiene cero o uno elementos.
Ahora ponemos dos elementos más en nuestro HashSet.
Esto tampoco es tan malo.
Cada cubo solo tiene un elemento. Entonces, si quiero saber, ¿esto contiene panda?
Puedo ver rápidamente el cubo número 1 y no es
alli y
Sabía que no está en nuestra colección.
Si quiero saber si contiene gato, miro el cubo
numero 3,
Encuentro gato, sé muy rápidamente si está en nuestro
colección.
¿Qué pasa si agrego koala? Bueno, eso no es tan malo.
Tal vez ahora en lugar de en el cubo número 1 solo mirando
un elemento
Necesito mirar a las dos.
Pero al menos no tengo que mirar elefante, tejón y
gato.
Si nuevamente estoy buscando panda, solo puede estar en el cubo
número 1 y
No tengo que mirar nada más que nutria y
coala.
Pero ahora pongo cocodrilo en el cubo número 1 y puedes
mira quizás a dónde va esto.
Que si el cubo número 1 sigue creciendo y creciendo
más grande, entonces básicamente tengo que mirar a través de todos
esos elementos para encontrar
algo que debería estar en el cubo número 1.
Si empiezo a agregar cadenas a otros cubos,
bien, el problema se hace cada vez más grande en cada
solo cubo
¿Cómo evitamos que nuestros cubos se llenen demasiado?
La solución aquí es que
Ahí está el HashSet se da cuenta de que los cubos se están poniendo
muy lleno.
Está perdiendo esta ventaja de esta búsqueda completa de
elementos.
Y solo creará más cubos (generalmente el doble que antes) y
luego coloque los elementos en el cubo correcto.
Así que aquí está nuestra implementación básica de HashSet con separador
encadenamiento Ahora voy a crear un "HashSet auto-redimensionable".
Este HashSet se dará cuenta de que los cubos son
llenándose demasiado y
Necesita más cubos.
loadFactor es otro campo en nuestra clase HashSet.
loadFactor representa el número promedio de elementos por
Cubeta,
por encima del cual queremos cambiar el tamaño.
loadFactor es un equilibrio entre espacio y tiempo.
Si los cubos se llenan demasiado, cambiaremos el tamaño.
Eso lleva tiempo, por supuesto, pero
puede ahorrarnos tiempo en el futuro si los cubos son un
Un poco más vacío.
Veamos un ejemplo.
Aquí hay un HashSet, hasta ahora hemos agregado cuatro elementos.
Elefante, perro, gato y pez.
En este punto, he decidido que loadFactor, el
límite,
el número promedio de elementos por cubo que estoy bien
con, es 0.75.
El número de cubos es buckets.length, que es 6, y
en este punto nuestro HashSet tiene cuatro elementos, por lo que el
El tamaño actual es 4.
Redimensionaremos nuestro HashSet, es decir, agregaremos más cubos,
cuando el número promedio de elementos por cubo excede
el loadFactor.
Es entonces cuando el tamaño actual dividido por cubos. Longitud es
mayor que loadFactor.
En este punto, el número promedio de elementos por cubo
es 4 dividido por 6.
4 elementos, 6 cubos, eso es 0.67.
Eso es menos que el umbral que establecí de 0,75, así que estamos
bueno.
No necesitamos cambiar el tamaño.
Pero ahora digamos que agregamos marmota.
La marmota terminaría en el cubo número 3.
En este punto, el tamaño actual es 5.
Y ahora el número promedio de elementos por cubo
es el tamaño actual dividido por buckets.length.
Eso es 5 elementos divididos por 6 cubos es 0.83.
Y esto excede el loadFactor que era 0.75.
Para abordar este problema, para hacer que el
cubos tal vez un poco
más vacío para que las operaciones como determinar si un
el cubo contiene
un elemento será un poco menos complejo, quiero cambiar el tamaño
mi HashSet.
Cambiar el tamaño del HashSet tiene dos pasos.
Primero doblaré el número de cubos, tuve 6 cubos,
ahora voy a tener 12 cubos.
Tenga en cuenta que el loadFactor que configuré en 0.75 permanece igual.
Pero el número de cubos cambiados es 12,
el número de elementos se mantuvo igual, es 5.
5 dividido por 12 es alrededor de 0,42, eso está muy por debajo de nuestro
factor de carga,
así que estamos bien ahora.
Pero no hemos terminado porque algunos de estos elementos están en
El cubo equivocado ahora.
Por ejemplo, elefante.
Elefante estaba en el cubo número 2 porque el número de
personajes en elefante
fue 8.
Tenemos 6 cubos, 8 menos 6 es 2.
Por eso terminó en el número 2.
Pero ahora que tenemos 12 cubos, 8 mod 12 es 8, entonces
el elefante ya no pertenece al cubo número 2.
El elefante pertenece al cubo número 8.
¿Qué hay de la marmota?
La marmota fue la que inició todo este problema.
La marmota terminó en el cubo número 3.
Porque 9 mod 6 es 3.
Pero ahora hacemos 9 mod 12.
9 mod 12 es 9, la marmota va al cubo número 9.
Y ves la ventaja de todo esto.
Ahora el cubo número 3 solo tiene dos elementos, mientras que antes tenía 3.
Así que aquí está nuestro código,
donde teníamos nuestro HashSet con encadenamiento separado que
no hizo ningún cambio de tamaño.
Ahora, aquí hay una nueva implementación donde usamos el cambio de tamaño.
La mayor parte de este código es el mismo,
todavía vamos a determinar si contiene el
valor ya.
Si no es así, entonces descubriremos qué cubeta
debería entrar y
luego agréguelo a ese cubo, agréguelo a esa LinkedList.
Pero ahora incrementamos el campo currentSize.
currentSize fue el campo que realizó un seguimiento del número
de elementos en nuestro HashSet.
Vamos a aumentarlo y luego vamos a mirar
a la carga promedio,
El número promedio de elementos por cubo.
Haremos esa división aquí abajo.
Tenemos que hacer un poco de casting aquí para asegurarnos
Que tenemos un doble.
Y luego, compararemos esa carga promedio con el campo
que he establecido como
0.75 cuando creé este HashSet, por ejemplo, que era
el loadFactor.
Si la carga promedio es mayor que el factor de carga,
eso significa que hay demasiados elementos por cubo en
promedio, y necesito reinsertarme.
Así que aquí está nuestra implementación del método para reinsertar
Todos los elementos.
Primero, crearé una variable local llamada oldBuckets.
Que se refiere a los cubos tal como están actualmente
antes de comenzar a cambiar el tamaño de todo.
Tenga en cuenta que todavía no estoy creando una nueva matriz de listas vinculadas.
Solo estoy cambiando el nombre de los cubos como oldBuckets.
Ahora recuerda que los cubos eran un campo en nuestra clase, voy
para crear ahora una nueva matriz
de listas enlazadas pero esto tendrá el doble de elementos
como lo hizo la primera vez.
Ahora necesito hacer la reinserción,
Voy a recorrer todos los viejos cubos.
Cada elemento en oldBuckets es un LinkedList de cadenas
Eso es un balde.
Revisaré ese cubo y obtendré cada elemento en ese
Cubeta.
Y ahora lo voy a reinsertar en los nuevos Buckets.
Obtendré su hashCode.
Descubriré qué índice es.
Y ahora obtengo el nuevo cubo, la nueva LinkedList de
cuerdas y
Lo agregaré a ese nuevo cubo.
En resumen, los HashSets, como hemos visto, son matrices de Linked
Listas o cubos.
Un HashSet auto-redimensionable puede darse cuenta usando alguna relación o
fuente
Elegiría un tamaño de tabla de n * 1.5 o n + (n >> 1), esto daría un factor de carga de .66666 ~ sin división, que es lento en la mayoría de los sistemas, especialmente en sistemas portátiles donde no hay división en El hardware.
fuente