2009-05-04 6 views
9

La documentación JDK para java.lang.String.hashCode()famously dice:Prueba: ¿por qué la implementación de java.lang.String.hashCode() coincide con su documentación?

El código hash para un objeto String se calcula como

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 

utilizando int aritmética, donde s[i] es el * i * º carácter de la cadena, n es la longitud de la cadena, y ^ indica exponenciación.

La implementación estándar de esta expresión es:

int hash = 0; 
for (int i = 0; i < length; i++) 
{ 
    hash = 31*hash + value[i]; 
} 
return hash; 

En cuanto a esto me hace sentir como si estuviera durmiendo a través de mi curso de algoritmos. ¿Cómo se traduce esa expresión matemática en el código de arriba?

Respuesta

12

No estoy seguro de si se olvidó de dónde dice "^ indica exponenciación" (no xor) en esa documentación.

Cada vez a través del bucle, el valor anterior de hash es multiplicada por 31 de nuevo antes de ser añadido al siguiente elemento de value.

pudo probar estas cosas son iguales por inducción, pero creo que un ejemplo podría ser más clara :

decir que estamos tratando con una cadena de 4-char.Vamos a desenrollar el bucle:

hash = 0; 
hash = 31 * hash + value[0]; 
hash = 31 * hash + value[1]; 
hash = 31 * hash + value[2]; 
hash = 31 * hash + value[3]; 

Ahora combinar estos en una declaración mediante la sustitución de cada valor de comprobación aleatoria, la siguiente declaración:

hash = 31 * (31 * (31 * (31 * 0 + value[0]) + value[1]) + value[2]) 
    + value[3]; 

31 * 0 es 0, por lo que simplifican:

hash = 31 * (31 * (31 * value[0] + value[1]) + value[2]) 
    + value[3]; 

Ahora multiplica los dos términos internos por ese segundo 31:

hash = 31 * (31 * 31 * value[0] + 31 * value[1] + value[2]) 
    + value[3]; 

Ahora multiplique los tres términos internos de ese primer 31:

hash = 31 * 31 * 31 * value[0] + 31 * 31 * value[1] + 31 * value[2] 
    + value[3]; 

y convertir a los exponentes (no es realmente más de Java):

hash = 31^3 * value[0] + 31^2 * value[1] + 31^1 * value[2] + value[3]; 
+0

RE su primera oración: ¿Vio alguna evidencia de que la pregunta o una respuesta en particular estaba asumiendo xor? –

+0

Ha expresado confusión acerca de cómo el código y la documentación podrían ser equivalentes. Dado que la documentación usaba "^" para la exponenciación, pero Java normalmente la usa para significar bit a bit xo, me pregunté si esa era la fuente de su confusión. (No hubo otras respuestas cuando comencé a escribir mi respuesta, por cierto) –

+0

Ahh, ya veo. No, era consciente de que era una exponenciación, pero no estaba claro cómo se siguió la implementación desde la expresión matemática. Tu respuesta aclara eso en gran medida, pero saber escribir ese código que solo da esa expresión sigue siendo un gran salto para mí. Para llegar a ese código, parece que tendrías que escribir un pequeño ejemplo, darte cuenta de que puedes "multiplicar por 0 de una manera inteligente" en el anidamiento más interno para completar el patrón, luego formar el ciclo. –

24

desenrollar el bucle. A continuación, se obtiene:

int hash = 0; 

hash = 31*hash + value[0]; 
hash = 31*hash + value[1]; 
hash = 31*hash + value[2]; 
hash = 31*hash + value[3]; 
... 
return hash; 

Ahora usted puede hacer alguna manipulación matemática, enchufe 0 para el valor hash inicial:

hash = 31*(31*(31*(31*0 + value[0]) + value[1]) + value[2]) + value[3])... 

simplificarlo un poco más:

hash = 31^3*value[0] + 31^2*value[1] + 31^1*value[2] + 31^0*value[3]... 

y que es esencialmente el algoritmo original dado.

+0

Es posible que desee explicarlo en términos de formulario de asignación única estática (SSA), que elimina la necesidad de pensar sobre qué valor tiene el "hash" en un momento determinado. :-) –

+0

Parece que el algoritmo original dice que debería ser: 31^3 * valor [0] + 31^2 * valor [1] + 31^1 * valor [2] + ... ¿O es solo mi cerebro frito está fallando? – Adnan

+0

En realidad, tienes razón, haré la edición. – CookieOfFortune

9

Tome un vistazo a las primeras iteraciones y verá el inicio patrón a surgir:

 
hash0 = 0 + s0 = s0 
hash1 = 31(hash0) + s1 = 31(s0) + s1 
hash2 = 31(hash1) + s2 = 31(31(s0) + s1) + s2 = 312(s0) + 31(s1) + s2 
... 
+1

<3 Gracias por (más o menos) escribir la respuesta de CookieOfFortune en forma de SSA. ¡Muy apreciado! –

+0

¿Cómo se hacen los subíndices? – CookieOfFortune

+0

Sería aún mejor si pudieras alinear verticalmente todos los términos correspondientes, y distribuir el 31 (...) en la tercera línea. –

10

demostración por inducción:

T1(s) = 0 if |s| == 0, else s[|s|-1] + 31*T(s[0..|s|-1]) 
T2(s) = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 
P(n) = for all strings s s.t. |s| = n, T1(s) = T2(s) 

Let s be an arbitrary string, and n=|s| 
Base case: n = 0 
    0 (additive identity, T2(s)) = 0 (T1(s)) 
    P(0) 
Suppose n > 0 
    T1(s) = s[n-1] + 31*T1(s[0:n-1]) 
    T2(s) = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] = s[n-1] + 31*(s[0]*31^(n-2) + s[1]*31^(n-3) + ... + s[n-2]) = s[n-1] + 31*T2(s[0:n-1]) 
    By the induction hypothesis, (P(n-1)), T1(s[0:n-1]) = T2(s[0:n-1]) so 
     s[n-1] + 31*T1(s[0..n-1]) = s[n-1] + T2(s[0:n-1]) 
    P(n) 

creo que lo tengo, y una prueba fué solicitado.

+1

oh complemento! ¡Inducción! –

0

¿No es inútil en absoluto para contar el código hash de String out de todos los caracteres? Imagine nombres de archivos o nombres de clases con su ruta completa en HashSet. O alguien que utiliza HashSets de documentos String en lugar de Listas porque "HashSet always beats Lists".

Me gustaría hacer algo como:

int off = offset; 
char val[] = value; 
int len = count; 

int step = len <= 10 ? 1 : len/10; 

for (int i = 0; i < len; i+=step) { 
    h = 31*h + val[off+i]; 
} 
hash = h 

En el código hash final no es más que una pista.

+0

Ignorar la mitad de los caracteres en la cadena significaría que almacenar una secuencia de "conteo de cadenas" en una tabla hash fácilmente podría provocar que 100 cadenas se correlacionen con cada valor hash. Ignorar a más de la mitad de los personajes empeoraría las cosas.Ignorar cualquier aspecto de la cadena con fines de hashing corre el riesgo de una penalización realmente enorme a cambio de una pequeña recompensa. – supercat

+0

Eso es esencialmente lo que los primeros diseñadores de Java sin embargo. Inicialmente, la función hash de cadena solo tomaba una muestra de caracteres cuando la cadena tenía más de 15 caracteres. Finalmente, tuvo que ser reparado porque resultó tener muy mal rendimiento de hash con ciertas cadenas (por ejemplo, con un conjunto de URL que a menudo se ven similares): http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622 . Las ganancias de rendimiento por no usar toda la cadena no pueden compensar el rendimiento de hash mucho peor. –

+0

Para aclarar: el segundo tipo de rendimiento se refiere al rendimiento de la "tabla hash", no a la velocidad bruta del cálculo del hash. –

Cuestiones relacionadas