Explicación detallada de hashcode() en HashMap

prefacio

Antes de hablar hashcode()sobre la función de , primero debemos entender qué es una tabla hash. Una tabla hash es una estructura de datos, su nombre en inglés se llama Hash Table, y también es lo que solemos llamar una 'tabla hash', o una 'tabla hash'. Una tabla hash es una estructura de datos que admite el acceso aleatorio mediante subíndices de matriz, y también se puede entender como una tabla hash es una extensión de una matriz.

Déjenme darles un ejemplo para analizar, habrá 100 jugadores participando en cierto deporte escolar, para registrar los resultados de cada jugador, los jugadores se colocarán con sus propios números por turno. Supongamos que están numerados secuencialmente del 1 al 100. Ahora necesitamos usar la programación, ¿cómo podemos obtener rápidamente los resultados de los jugadores numerados designados?

Por lo general, construiremos una matriz unidimensional y luego colocaremos la información del jugador con el número 1 en la posición de la matriz con el subíndice 0, colocaremos la información del jugador con el número 2 en la posición de la matriz con el subíndice 1, y así sucesivamente, coloque el número 100 El jugador la información en la matriz se coloca en la posición con el subíndice 99 en la matriz. Cuando ahora queremos obtener la información del jugador numerado x, solo necesitamos sacar la información del jugador en la posición del subíndice de matriz Y. La complejidad de tiempo de este proceso de obtener la información del jugador es O(1), Is la eficiencia de esta manera muy alta?

Extensión: ¿Por qué la complejidad temporal de un arreglo puede ser O(1) en función del acceso aleatorio del subíndice del arreglo?

De hecho, este ejemplo ya incluye la idea de hashing. En este ejemplo, el código de jugador x y el índice de matriz y pueden formar una expresión:

Entre ellos, el número del atleta se llama clave (key) o palabra clave. Lo usamos para identificar a un jugador. Luego, convertimos el número de entrada en un método de mapeo de subíndice de matriz que se llama función hash (o función hash, función hash), y el valor calculado por la función hash se llama valor hash (o valor hash, valor hash).

Del ejemplo anterior, podemos concluir la regla: la tabla hash usa la característica de que la complejidad del tiempo es O(1) cuando la matriz admite el acceso aleatorio de acuerdo con el subíndice. Usamos la función de tabla hash para mapear el valor clave del elemento en un subíndice y luego almacenamos los datos en la posición correspondiente al subíndice en la matriz. Cuando necesitamos consultar los elementos de la matriz, también debemos asignar el valor clave del elemento al subíndice de la matriz correspondiente a través de la función hash y luego encontrar el elemento correspondiente a través del subíndice de la matriz.

Cómo HashMap se da cuenta de la eficiencia del valor de consulta

Antes de hablar sobre cómo HashMap logra la eficiencia de consultar Valor, repasemos cómo consultar un elemento específico en una tabla lineal. Suponga que la longitud de esta tabla lineal es 1000. Si busca en orden, entonces la peor complejidad de tiempo es O( 1000), y usando búsqueda binaria, la peor complejidad de tiempo es O(500).

En HashMap, depende mucho de la validez del código hash. La estructura interna de HashMap se puede considerar como una estructura compuesta por una matriz (tabla Node<K,V>[]) y una lista enlazada. Y los datos colocados en HashMap y la matriz en la estructura interna de HashMap están relacionados entre sí a través de hash().

Tomemos el ejemplo de la reunión deportiva anterior, suponiendo que la expresión de hash() es x-1, que se transforma en y = f(x) en la fórmula matemática, donde f(x) = x - 1 (donde x representa el número del jugador, y representa el subíndice de la matriz). Cuando sabemos que el número de un determinado jugador es 1, entonces solo necesitamos convertir a través de la función hash() correspondiente, es decir, 1 - 1 = 0, luego el 0 obtenido es el subíndice de la matriz, entonces podemos Obtén rápidamente la información de la puntuación del jugador número 1. De manera similar, si necesitamos almacenar la información de puntaje del jugador con el número 50, entonces solo necesitamos sustituir 50 en hash(), es decir, 50 - 1 = 49, y el 49 obtenido es la posición donde el jugador con el número 50 puede como se muestra a continuación:

Por supuesto, en el HashMap real, hash() no es tan f(x) = x - 1simple como el anterior. Como se muestra a continuación (Java OpenJdk 11):

En la figura anterior se puede ver que la determinación del subíndice de la matriz de la tabla hash en HashMap está determinada por la longitud de la matriz de cubos ny hash()la función. En HashMap, cuando necesitamos almacenar un par de pares clave-valor, debemos colocar el valor clave correspondiente hash(Object key), convertirlo en el valor hash correspondiente y obtener la matriz de cubo correspondiente a través de la operación con la longitud del cubo. subíndice de matriz y, a continuación, almacene el valor correspondiente en la posición de subíndice especificada de la matriz. De manera similar, si obtenemos el valor correspondiente en HashMap de acuerdo con el valor clave, entonces debemos realizar la operación inversa de almacenamiento.

colisión de hash

Antes de implementar las operaciones de almacenamiento y recuperación de HashMap, debemos resolver la colisión de hash de HashMap. Sabemos que el algoritmo hash no puede lograr una colisión cero, es decir, hay una colisión hash en el algoritmo hash. Esto se basa en una teoría muy básica en combinatoria, el principio del casillero (también llamado principio del cajón). Este principio describe que si hay 10 casilleros y 11 palomas, entonces debe haber más de 1 paloma en un casillero, en otras palabras, debe haber 2 palomas en el mismo casillero.

Para resolver la colisión de hash, la gente ha propuesto dos soluciones principales, el método de direccionamiento de desarrollo y el método de lista enlazada. HashMap utiliza principalmente el método de lista enlazada para resolver conflictos de hash. El modelo de HashMap para resolver conflictos hash es el siguiente:

El tamaño de matriz de depósito inicial predeterminado de HashMap es 16, y cuando ocurre una colisión hash, el valor de colisión se agrega a la lista vinculada. En JDK1.8, cuando la longitud de la lista enlazada es superior a 8, la lista enlazada se convertirá en un árbol rojo-negro, y el árbol rojo-negro se puede usar para agregar, eliminar, verificar y modificar rápidamente, por lo tanto optimizando y mejorando la eficiencia de HashMap. Al mismo tiempo, cuando la longitud de la lista enlazada vuelva a ser inferior a 8, el árbol rojo-negro se convertirá de nuevo en una lista enlazada, porque el árbol rojo-negro necesita mantener el equilibrio. relativamente pequeño, la eficiencia de HashMap no es obvia.

Al reescribir hashcode(), ¿por qué necesita reescribir equals()?

Con el conocimiento básico anterior como un presagio, nos ayudará a comprender mejor por qué necesitamos reescribir el método equals() al reescribir hashcode().

Cuando usamos HashMap, necesitamos llamar al método público de HashMap put(K key, V value)para pasar el par clave-valor a la instancia de HashMap ( 在这篇文章里面约定,把传进 HashMap 的键值对,分别称为"键值"、Value值). Como se muestra abajo:

Como en el código anterior, hemos personalizado la IKeyclase de clave-valor, que es IKeymuy simple, a partir de la línea 17 del código anterior. Y en las líneas 4 y 5, respectivamente , cree IKeyel objeto de y llame a la función a través de la instancia de HashMap para pasar como el valor clave, y luego imprima el valor del valor con el valor clave llamando a la función. De acuerdo con nuestras necesidades, al llamar a la función para imprimir el Valor valor del valor clave debe ser el Valor del par clave-valor correspondiente, porque el valor al que pasamos es 5, entonces el valor que obtenemos también debe ser el igual que el valor pasado la misma. Sin embargo, el resultado impreso por la consola está completamente más allá de nuestras expectativas.¿Qué está impreso en la consola es por qué?mIkey_1mIkey_2put()mIkey_1get()mIkey_2get()mIkey_2I am Key_1IkeyidmIkey_2ValuemIkey_1null

Debido a que no reescribimos la función cuando personalizamos el HashMap, cuando hashcode()usamos mIkey_2el valor clave para obtener mIkey_1el valor pasado, llamamos directamente a la función en la clase Object . Sabemos que el retorno hashcode()en la clase Object es sobre el objeto IKey hashcode()La dirección de memoria de la instancia (puede ser el valor hash de la dirección de memoria, que es ligeramente diferente en diferentes versiones de JDK, por lo que no lo estudiaré aquí por el momento. Los lectores interesados ​​pueden leer el código fuente de Object por sí mismos. Piense que el método hashcode() en la clase Object devuelve la dirección de memoria del objeto).

Obviamente, las direcciones IKeyde las dos instancias del objeto mIkey_1en mIkey_2la memoria de la máquina virtual son diferentes, por lo que cuando se utiliza HashMap para mIkey_2consultar mIkey_1el valor de Value de , debido a que mIkey_2el valor clave de la dirección de memoria de no existe, el resultado devuelto por el la consola es null.

IKeyComo se muestra arriba, hemos reescrito hashcode()el método en el valor de clave personalizado , pero ejecutamos el método principal en IhashMap en este momento, entonces, ¿la consola imprimirá "Soy clave_1" en este momento?

La respuesta es no, lo mismo está impreso en la consola null. get()Porque cuando se llama a la función en HashMap , el subíndice de posición de la matriz del cubo mIkey_2se calculará primero por el valor hash de mIkey_2, y si existe el elemento de matriz del subíndice, se evaluará nuevamente si el valor hash del elemento es el igual que el valor hash de mIkey_2. Como mencionamos anteriormente, el algoritmo hash no puede lograr cero colisiones. Es decir, incluso dos instancias de objetos IKey diferentes pueden tener el mismo valor hash. Por lo tanto, bajo la premisa de que se cumplen las dos condiciones anteriores, es necesario llamar equals()al método de IKey para determinar si el interior ides el mismo. Si idlos valores de son los mismos, entonces el valor clave pasado mIkey_2se puede encontrar a través del valor clave . mIkey_1Pero no reescribimos equals()el método, por lo que HashMap llama equals()al método de la clase Object.

Sabemos que los métodos de la clase Object equals()comparan las direcciones de memoria de dos objetos. Obviamente, es diferente mIkey_1de la dirección de memoria de , por lo que hemos reescrito el método, pero sin reescribir en , podemos consultar el Valor de usándolo como el valor clave, y sigue siendo .mIkey_2hashcode()equals()mIkey_2mIkey_1null

Mire la imagen de arriba nuevamente, cuando comentamos lo ya comentado en IKey equals(), entonces el resultado que obtenemos en la consola es exactamente lo que queremos I am Key_1.

posdata

En este punto, es posible que desee preguntar, ¿por qué no necesito reescribir el método hashcode()y cuando generalmente uso Integer, String, etc. como valores clave en HashMap ? equals()Esto se debe a que los ingenieros de Java han reescrito e implementado estos dos métodos para nosotros. Los lectores interesados ​​pueden navegar a través de su código fuente para verlo.

referencias

Supongo que te gusta

Origin blog.csdn.net/HongHua_bai/article/details/102697799
Recomendado
Clasificación