2009-05-04 2 views
9

Die JDK-Dokumentation für java.lang.String.hashCode()famously sagt:Beweis: Warum stimmt die Implementierung von java.lang.String.hashCode() mit ihrer Dokumentation überein?

Der Hash-Code für ein String-Objekt wird wie folgt berechnet

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

int Arithmetik, wo s[i] das ist * i * te Zeichen der Zeichenkette, n ist die Länge der Zeichenfolge und ^ zeigt die Potenzierung an.

Die Standardimplementierung dieses Ausdrucks ist:

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

Mit Blick auf das ich mich das Gefühl wie war meine Algorithmen natürlich schlafen durch. Wie übersetzt sich dieser mathematische Ausdruck in den obigen Code?

Antwort

12

Ich bin nicht sicher, wenn Sie verpasst haben, wo es heißt "^ zeigt Exponentiation" (nicht xor) in dieser Dokumentation.

Jedes Mal durch die Schleife, wird der vorherige Wert des Hash von 31 multipled wieder, bevor zum nächsten Element der value hinzugefügt wird.

Man könnte diese Dinge sind gleich durch Induktion beweisen, aber ich denke, ein Beispiel könnte mehr klar sein:

sagen wir mit einem 4-char-String zu tun hat.Lassen Sie uns die Schleife entrollen:

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

Und dies in einer Anweisung kombinieren, indem jeder Wert von Hash in die folgende Anweisung ersetzt:

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

31 * 0 0, so vereinfachen:

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

Jetzt multiplizieren Sie die beiden inneren Terme mit dieser Sekunde 31:

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

nun die drei inneren Bedingungen durch diesen ersten 31 multiplizieren:

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

und konvertieren Exponenten (nicht wirklich Java mehr):

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

RE dein erster Satz: Hast du einige Beweise gesehen, dass die Frage oder eine bestimmte Antwort xor angenommen hat? –

+0

Sie hatten sich darüber geärgert, wie der Code und die Dokumentation gleichwertig sein könnten. Da in der Dokumentation "^" für die Potenzierung verwendet wurde, wird in Java normalerweise bitweises x verwendet, oder ich fragte mich, ob dies die Ursache für Ihre Verwirrung war. (Es gab keine anderen Antworten, als ich anfing, meine Antwort zu schreiben, BTW) –

+0

Ahh, ich verstehe. Nein, mir war bewusst, dass es eine Potenzierung war, aber unklar, wie die Implementierung aus dem mathematischen Ausdruck folgte. Ihre Antwort verdeutlicht das sehr - aber zu wissen, diesen Code nur mit diesem Ausdruck zu schreiben, ist immer noch ein Sprung für mich. Um zu diesem Code zu kommen, müsste man ein kleines Beispiel schreiben, um zu erkennen, dass man in der innersten Verschachtelung "mit 0 auf intelligente Weise multiplizieren" kann, um das Muster zu vervollständigen und dann die Schleife zu bilden. –

24

die Schleife ausrollen. Dann erhalten Sie:

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; 

Jetzt können Sie einige mathematische Manipulation tun, stecken Sie in 0 für den Anfangs-Hash-Wert:

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

Vereinfachen es etwas mehr:

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

Und das ist im Wesentlichen der ursprüngliche Algorithmus gegeben.

+0

Sie möchten es vielleicht in Form von statischen Einzelzuweisung (SSA) Form erklären, die dann die Notwendigkeit beseitigt, darüber nachzudenken, welchen Wert "Hash" zu einem bestimmten Zeitpunkt hat. :-) –

+0

Sieht so aus, als ob der Originalalgorithmus sagt: 31^3 * Wert [0] + 31^2 * Wert [1] + 31^1 * Wert [2] + ... Oder ist es einfach mein gebratenes Gehirn fehlzündet? – Adnan

+0

Eigentlich hast du recht, ich werde den Schnitt machen. – CookieOfFortune

9

Werfen Sie einen Blick auf die ersten paar Iterationen und Sie werden das Musterstart sehen aufzutauchen:

 
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 Danke für (mehr oder weniger) Schreiben von CookieOfFortunes Antwort in SSA-Form. Sehr geschätzt! –

+0

Wie machst du tiefgestellte Zeichen? – CookieOfFortune

+0

Wäre noch besser, wenn Sie alle entsprechenden Begriffe vertikal ausrichten und die 31 (...) in der dritten Zeile verteilen könnten. –

10

Beweis durch Induktion:

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) 

Ich glaube, ich habe es, und einen Beweis wurde ersucht.

+1

oh schnappen! Induktion! –

0

Ist es nicht nutzlos an alle hashcode zählen der String aus aller Zeichen? Stellen Sie sich Dateinamen oder Klassennamen mit ihrem vollständigen Pfad in HashSet vor. Oder jemand, der HashSets von String-Dokumenten anstelle von Listen verwendet, weil "HashSet always beats Lists".

ich tun würde, so etwas wie:

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 

Am Ende hashcode ist nichts anderes als ein Hinweis.

+0

Das Ignorieren der Hälfte der Zeichen in der Zeichenfolge würde bedeuten, dass das Speichern einer Sequenz von "Zählzeichenfolgen" in einer Hash-Tabelle leicht dazu führen könnte, dass 100 Zeichenfolgen jedem Hashwert zugeordnet werden. Ignorieren mehr als die Hälfte der Charaktere würde die Dinge noch schlimmer machen.Das Ignorieren irgendeines Aspekts der Kette für Hash-Zwecke riskiert eine wirklich große Strafe im Austausch für eine ziemlich kleine Auszahlung. – supercat

+0

Das ist im Wesentlichen was die frühen Designer von Java obwohl. Anfänglich nahm die String-Hash-Funktion nur eine Stichprobe von Zeichen, wenn die Zeichenfolge länger als 15 Zeichen war. Schließlich musste es behoben werden, da es sich bei bestimmten Zeichenfolgen (z. B. bei URLs, die häufig ähnlich aussehen) als sehr schlecht erwiesen hat: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622 . Die Leistungssteigerungen, wenn die gesamte Zeichenfolge nicht verwendet wird, können die viel schlechtere Hash-Leistung nicht ausgleichen. –

+0

Um zu verdeutlichen: die zweite Art von Leistung, wenn Sie auf die "Hash-Tabelle" Leistung beziehen, nicht auf die rohe Geschwindigkeit der Berechnung des Hash. –

Verwandte Themen