it-swarm.com.de

Warum sollten Hashfunktionen einen Primzahlmodul verwenden?

Vor langer Zeit kaufte ich ein Datenstruktur-Buch für 1,25 USD vom Schnäppchen-Tisch. Darin heißt es in der Erklärung für eine Hash-Funktion, dass sie wegen "der Natur der Mathematik" letztendlich um eine Primzahl modifiziert werden sollte.

Was erwarten Sie von einem 1,25-Dollar-Buch?

Auf jeden Fall hatte ich jahrelang über die Natur der Mathematik nachgedacht und kann es immer noch nicht herausfinden.

Ist die Verteilung von Zahlen wirklich mehr, wenn es eine Primzahl von Buckets gibt? Oder ist dies eine alte Programmierer-Geschichte, die jeder akzeptiert, weil jeder sonst es akzeptiert?

314
theschmitzer

Normalerweise funktioniert eine einfache Hash-Funktion, indem die "Komponententeile" der Eingabe (Zeichen im Fall einer Zeichenfolge) mit den Potenzen einer Konstanten multipliziert und zu einem Integer-Typ addiert werden. Ein typischer (wenn auch nicht besonders guter) Hash eines Strings könnte zum Beispiel so aussehen:

(first char) + k * (second char) + k^2 * (third char) + ...

Wenn dann ein Bündel Saiten mit der gleichen ersten Saite eingespeist wird, sind alle Ergebnisse das gleiche Modulo k, zumindest bis der Integer-Typ überläuft.

[Zum Beispiel ist der String-Hash-Code von Java sehr unähnlich - die Reihenfolge der Zeichen ist umgekehrt: k = 31. Sie erhalten also auffällige Beziehungen zwischen Strings, die auf die gleiche Weise enden, und auffällige Beziehungen zwischen Strings, die bis auf das Ende gleich sind. Dadurch wird das Hashtable-Verhalten nicht ernsthaft beeinträchtigt.]

Eine Hashtabelle funktioniert, indem der Modul des Hashs über die Anzahl der Buckets genommen wird.

In einer Hashtabelle ist es wichtig, in wahrscheinlichen Fällen keine Kollisionen zu erzeugen, da Kollisionen die Effizienz der Hashtabelle verringern.

Angenommen, jemand steckt eine ganze Reihe von Werten in eine Hashtabelle, die eine gewisse Beziehung zwischen den Elementen haben, wie alle, die den gleichen ersten Charakter haben. Dies ist ein ziemlich vorhersehbares Nutzungsmuster, würde ich sagen, wir möchten nicht, dass es zu viele Kollisionen erzeugt.

Es stellt sich heraus, dass "wegen der Natur der Mathematik", wenn die im Hash verwendete Konstante und die Anzahl der Buckets coprime sind, Kollisionen in einigen häufigen Fällen minimiert werden. Wenn sie nicht coprime sind, gibt es einige recht einfache Beziehungen zwischen Eingaben, bei denen Kollisionen nicht minimiert werden. Alle Hashes ergeben sich gleich dem gemeinsamen Faktor, was bedeutet, dass sie alle in das 1/n-te der Buckets fallen, die diesen Wert modulo als gemeinsamen Faktor haben. Sie erhalten n-mal so viele Kollisionen, wobei n der gemeinsame Faktor ist. Da n mindestens 2 ist, würde ich sagen, dass es für einen recht einfachen Anwendungsfall nicht akzeptabel ist, mindestens doppelt so viele Kollisionen wie normal zu erzeugen. Wenn ein Benutzer unsere Distribution in Buckets aufteilen möchte, möchten wir, dass es sich um einen ungewöhnlichen Unfall handelt und nicht um eine einfach vorhersehbare Verwendung.

Jetzt haben Hashtable-Implementierungen offensichtlich keine Kontrolle über die darin enthaltenen Elemente. Sie können nicht verhindern, dass sie miteinander verwandt sind. Also müssen Sie sicherstellen, dass die Konstante und die Anzahl der Bucket-Coprime übereinstimmen. Auf diese Weise verlassen Sie sich nicht allein auf die "letzte" Komponente, um den Modul des Buckets in Bezug auf einen kleinen gemeinsamen Faktor zu bestimmen. Soweit ich weiß, müssen sie nicht vorrangig sein, nur Coprime.

Wenn die Hash-Funktion und die Hash-Tabelle jedoch unabhängig voneinander geschrieben werden, weiß die Hash-Tabelle nicht, wie die Hash-Funktion funktioniert. Es könnte eine Konstante mit kleinen Faktoren verwendet werden. Wenn Sie Glück haben, funktioniert es möglicherweise völlig anders und ist nichtlinear. Wenn der Hash gut genug ist, ist jede Anzahl von Eimern in Ordnung. Eine paranoide Hashtabelle kann jedoch keine gute Hash-Funktion übernehmen, daher sollten Sie eine Primzahl von Buckets verwenden. Auf ähnliche Weise sollte eine paranoide Hash-Funktion eine große Prim-Konstante verwenden, um die Wahrscheinlichkeit zu verringern, dass jemand eine Anzahl von Buckets verwendet, die zufällig einen gemeinsamen Faktor mit der Konstante haben.

In der Praxis denke ich, dass es ziemlich normal ist, eine Potenz von 2 als Anzahl der Eimer zu verwenden. Dies ist praktisch und erspart das Suchen oder Vorwählen einer Primzahl der richtigen Größe. Sie verlassen sich also auf die Hash-Funktion, um nicht einmal Multiplikatoren zu verwenden, was im Allgemeinen eine sichere Annahme ist. Sie können jedoch immer noch gelegentlich schlechte Hash-Verhalten auf der Grundlage von Hash-Funktionen wie den oben genannten erhalten, und die Zählung der Prim-Bucket könnte weiter helfen.

Das Prinzip "Alles muss Primzahl sein" ist, soweit ich weiß, eine ausreichende, aber keine Voraussetzung für eine gute Verteilung über Hashtables. Es ermöglicht jedem die Interoperabilität, ohne davon auszugehen, dass die anderen dieselbe Regel befolgt haben.

[Bearbeiten: Es gibt einen weiteren, spezielleren Grund, eine Primzahl von Buckets zu verwenden. Dies ist der Fall, wenn Sie Kollisionen mit linearer Prüfung behandeln. Dann berechnen Sie einen Schritt aus dem Hashcode, und wenn dieser Schritt ein Faktor der Bucket-Zählung ist, können Sie nur (bucket_count/stride) -Prüfungen durchführen, bevor Sie wieder da sind, wo Sie angefangen haben. Der Fall, den Sie am meisten vermeiden möchten, ist natürlich stride = 0, der jedoch ein spezielles Gehäuse sein muss. Wenn Sie jedoch auch das spezielle Gehäuse bucket_count/stride gleich einer kleinen Ganzzahl vermeiden möchten, können Sie einfach den bucket_count als Priming definieren und sich nicht darum kümmern, was der Schritt ist vorgesehen, es ist nicht 0.]

228
Steve Jessop

Beim Einfügen/Auslesen aus der Hash-Tabelle müssen Sie zunächst den Hash-Code für den angegebenen Schlüssel berechnen und dann den richtigen Bucket finden, indem Sie den Hash-Code auf die Größe der Hash-Tabelle zuschneiden, indem Sie Hash-Code% table_length ausführen. Hier sind 2 'Aussagen', die Sie höchstwahrscheinlich irgendwo gelesen haben

  1. Wenn Sie eine Potenz von 2 für table_length verwenden, ist das Finden (hashCode (Schlüssel)% 2 ^ n) genauso einfach und schnell wie (hashCode (Schlüssel) & (2 ^ n -1)). Wenn Ihre Funktion zum Berechnen von hashCode für einen bestimmten Schlüssel jedoch nicht gut ist, werden Sie definitiv unter dem Clustering vieler Schlüssel in einigen Hash-Buckets leiden.
  2. Wenn Sie jedoch Primzahlen für table_length verwenden, können die berechneten Hash-Codes den verschiedenen Hash-Buckets zugeordnet werden, selbst wenn Sie eine etwas dumme Hash-Funktion haben.

Und hier ist der Beweis.

Wenn angenommen wird, dass Ihre hashCode-Funktion die folgenden hashCodes unter anderem {x, 2x, 3x, 4x, 5x, 6x ...} ergibt, dann werden alle diese in nur m Buckets zusammengefasst, wobei m = table_length/GreatestCommonFactor ist (table_length, x). (Es ist trivial, dies zu überprüfen/abzuleiten). Jetzt können Sie eine der folgenden Aktionen ausführen, um Clusterbildung zu vermeiden

Stellen Sie sicher, dass Sie nicht zu viele Hash-Codes generieren, die ein Vielfaches eines anderen Hash-Codes sind, wie in {x, 2x, 3x, 4x, 5x, 6x ...}. Dies kann jedoch schwierig sein, wenn Ihre hashTable dies haben soll Millionen von Einträgen . Oder machen Sie m einfach gleich der table_length, indem Sie GreatestCommonFactor (table_length, x) gleich 1 setzen, indem Sie table_length coprime mit x festlegen. Und wenn x nur eine beliebige Zahl sein kann, stellen Sie sicher, dass table_length eine Primzahl ist.

Von - http://srinvis.blogspot.com/2006/07/hash-table-lengths-and-prime-numbers.html

28
user177612

http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/

Ziemlich klare Erklärung, auch mit Bildern.

Bearbeiten: Zusammenfassend werden Primzahlen verwendet, da Sie die beste Chance haben, einen eindeutigen Wert zu erhalten, wenn Sie Werte mit der ausgewählten Primzahl multiplizieren und sie alle addieren. Wenn Sie beispielsweise eine Zeichenfolge angeben, multiplizieren Sie jeden Buchstabenwert mit der Primzahl und addieren Sie dann alle Werte, um den Hashwert zu erhalten.

Eine bessere Frage wäre, warum genau die Nummer 31? 

9
AlbertoPL

tl; dr

index[hash(input)%2] würde zu einer Kollision für die Hälfte aller möglichen Hashwerte und einen Bereich von Werten führen. index[hash(input)%prime] führt zu einer Kollision von <2 aller möglichen Hashes. Durch die Festlegung des Divisors auf die Tabellengröße wird auch sichergestellt, dass die Anzahl nicht größer als die Tabelle sein kann.

9
Indolering

Primes werden verwendet, weil Sie gute Chancen haben, einen eindeutigen Wert für eine typische Hash-Funktion zu erhalten, die Polynome modulo P. verwendet. Sie verwenden eine solche Hash-Funktion für Zeichenfolgen der Länge <= N und haben eine Kollision. Das bedeutet, dass 2 verschiedene Polynome denselben Modulo-Wert P erzeugen. Die Differenz dieser Polynome ist wiederum ein Polynom mit gleichem Grad N (oder weniger). Es hat nicht mehr als N Wurzeln (hier zeigt sich die Natur der Mathematik, da diese Behauptung nur für ein Polynom über ein Feld gilt => Primzahl). Wenn also N viel kleiner als P ist, haben Sie wahrscheinlich keine Kollision. Danach kann das Experiment wahrscheinlich zeigen, dass 37 groß genug ist, um Kollisionen für eine Hashtabelle von Zeichenketten der Länge 5-10 zu vermeiden, und klein genug, um sie für Berechnungen zu verwenden. 

8
TT_

Um einen alternativen Standpunkt anzugeben, gibt es diese Site: 

http://www.codexon.com/posts/hash-functions-the-modulo-prime-myth

Was bedeutet, dass Sie die größtmögliche Anzahl von Buckets verwenden sollten, statt auf eine Primzahl von Buckets abzurunden. Es scheint eine vernünftige Möglichkeit zu sein. Intuitiv kann ich sicherlich sehen, wie eine größere Anzahl von Buckets besser wäre, aber ich kann kein mathematisches Argument dafür aufstellen.

5
Falaina

Dies hängt von der Wahl der Hash-Funktion ab.

Viele Hash-Funktionen kombinieren die verschiedenen Elemente in den Daten, indem sie mit einigen Faktoren multipliziert werden, die der Word-Größe der Maschine entsprechen (Modulo ist frei, wenn die Berechnung nur überläuft).

Sie möchten keinen gemeinsamen Faktor zwischen einem Multiplikator für ein Datenelement und der Größe der Hash-Tabelle, da es dann passieren kann, dass das Ändern des Datenelements die Daten nicht über die gesamte Tabelle verteilt. Wenn Sie für die Größe der Tabelle eine Primzahl auswählen, ist ein solcher gemeinsamer Faktor höchst unwahrscheinlich.

Auf der anderen Seite setzen sich diese Faktoren normalerweise aus ungeraden Primzahlen zusammen. Daher sollten Sie auch die Potenz von Zwei für Ihre Hashtabelle verwenden (z. B. verwendet Eclipse 31, wenn die Java-Methode hashCode () generiert wird).

3
starblue

Primes sind eindeutige Zahlen. Sie sind Einzigartig in dieser Hinsicht ist das Produkt einer Primzahl mit einer anderen Nummer hat das beste Chance, einzigartig zu sein (nicht so einzigartig wie die Primzahl selbst), bedingt durch. die Tatsache, dass eine Primzahl verwendet wird komponiere es. Diese Eigenschaft wird in .__ verwendet. Hash-Funktionen.

Bei einer Zeichenfolge „Samuel“ können Sie Erzeugen Sie einen eindeutigen Hash durch Multiplikation jede der konstituierenden Ziffern oder Buchstaben mit einer Primzahl und Hinzufügen von sie auf. Deshalb werden Primzahlen verwendet.

Primes zu verwenden ist jedoch ein alter Technik. Der Schlüssel hier zu verstehen solange Sie eine .__ generieren können. ausreichend eindeutiger Schlüssel, den Sie verschieben können auch zu anderen Hashtechniken. Gehen Weitere Informationen zu diesem Thema finden Sie hier http://www.azillionmonkeys.com/qed/hash.html

http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/

3
user105033

Angenommen, Ihre Tabellengröße (oder die Anzahl für Modulo) ist T = (B * C). Wenn der Hashwert für Ihre Eingabe wie (N * A * B) ist, wobei N eine beliebige Ganzzahl sein kann, wird Ihre Ausgabe nicht gut verteilt. Da jedes Mal, wenn n zu C, 2C, 3C usw. wird, beginnt die Ausgabe zu wiederholen. Ihre Ausgabe wird nur in C-Positionen verteilt. Beachten Sie, dass C hier ist (T/HCF (Tabellengröße, Hash)).

Dieses Problem kann durch die Herstellung von HCF 1 beseitigt werden. Primzahlen sind dafür sehr gut.

Eine andere interessante Sache ist, wenn T 2 ^ N ist. Diese geben die Ausgabe genauso aus wie alle unteren N Bits des Eingabe-Hashes. Da jede Zahl Potenzen von 2 darstellen kann, werden wir, wenn wir Modulo einer beliebigen Zahl mit T nehmen, alle Potenzen der Formnummer 2 abziehen, die> = N sind, und daher immer die Anzahl der spezifischen Muster angeben, abhängig von der Eingabe . Dies ist auch eine schlechte Wahl.

In ähnlicher Weise ist T wie 10 ^ N aus ähnlichen Gründen ebenfalls schlecht (Muster in Dezimalschreibweise von Zahlen statt binär).

Primzahlen liefern also tendenziell besser verteilte Ergebnisse und sind daher eine gute Wahl für die Tabellengröße.

2

Kopieren aus meiner anderen Antwort https://stackoverflow.com/a/43126969/917428 . Weitere Einzelheiten und Beispiele finden Sie hier.

Ich glaube, dass es nur damit zu tun hat, dass Computer in Basis 2 arbeiten. Denken Sie nur daran, wie das gleiche für Basis 10 funktioniert:

  • 8% 10 = 8
  • 18% 10 = 8
  • 87865378% 10 = 8

Es spielt keine Rolle, was die Zahl ist: Solange sie mit 8 endet, wird der Modulo 10 8 sein.

Wenn Sie eine ausreichend große Zahl auswählen, die nicht das Zweierpotential ist, wird sichergestellt, dass die Hash-Funktion wirklich eine Funktion aller Eingangsbits ist und keine Teilmenge von ihnen.

1
Ste_95

Ich möchte etwas für Steve Jessops Antwort hinzufügen (ich kann nicht dazu Stellung nehmen, da ich nicht genug Ansehen habe). Aber ich habe einiges hilfreiches Material gefunden. Seine Antwort ist sehr hilfreich, aber er hat einen Fehler gemacht: Die Eimergröße sollte keine Potenz von 2 sein. Ich zitiere nur das Buch "Introduction to Algorithm" von Thomas Cormen, Charles Leisersen ua auf Seite 263:

Bei der Divisionsmethode vermeiden wir normalerweise bestimmte Werte von m. Zum Beispiel sollte m keine Potenz von 2 sein, denn wenn m = 2 ^ p, dann ist h(k) nur die p niedrigsten Bits von k. Wenn wir nicht wissen, dass alle p-Bit-Muster niedriger Ordnung gleich wahrscheinlich sind, sollten wir die Hash-Funktion besser so gestalten, dass sie von allen Bits des Schlüssels abhängt. In Übung 11.3-3 werden Sie aufgefordert, zu zeigen, dass die Auswahl von m = 2 ^ p-1, wenn k eine in Radix 2 ^ p interpretierte Zeichenfolge ist, eine schlechte Wahl ist, da das Durchführen der Zeichen von k ihren Hashwert nicht ändert.

Ich hoffe es hilft.

1
iefgnoix

Ich habe die beliebte WordPress-Website in einigen der oben genannten Antworten oben gelesen. Nach dem, was ich verstanden habe, möchte ich eine einfache Beobachtung teilen, die ich gemacht habe.

Sie finden alle Details im Artikel hier , gehen jedoch davon aus, dass Folgendes gilt:

  • Die Verwendung einer Primzahl gibt uns die "beste Chance" eines einzigartigen Werts.

Eine allgemeine Hashmap-Implementierung möchte, dass zwei Dinge eindeutig sind. 

  • Eindeutiger Hashcode für die -Taste
  • Eindeutiger Index zum Speichern des tatsächlichen value

Wie erhalten wir den eindeutigen Index? Indem Sie die Anfangsgröße des internen Behälters ebenfalls zu einer Primzahl machen. Im Grunde handelt es sich dabei um prime, da es diese einzigartige Eigenschaft besitzt, eindeutige Zahlen zu erzeugen, die wir verwenden, um Objekte zu identifizieren und Indizes im internen Container zu finden.

Beispiel:

schlüssel = "Schlüssel"

value = "value" uniqueId = "k" * 31 ^ 2 + "e" * 31 ^ 1` + "y"

zuordnungen zu eindeutiger ID 

Jetzt wollen wir einen einzigartigen Standort für unseren Wert - also wir 

uniqueId % internalContainerSize == uniqueLocationForValue, vorausgesetzt, internalContainerSize ist auch eine Primzahl.

Ich weiß, dass dies vereinfacht ist, aber ich hoffe, die allgemeine Idee durchzubringen.

0
Ryan

Für eine Hash-Funktion ist es nicht nur wichtig, Kollisionen generell zu minimieren, sondern es auch unmöglich zu machen, bei demselben Hash zu bleiben, während einige Bytes geändert werden.

Angenommen, Sie haben eine Gleichung: (x + y*z) % key = x mit 0<x<key und 0<z<key. Wenn Schlüssel eine Primzahl ist, ist n * y = Schlüssel für jedes n in N und für jede andere Zahl falsch.

Ein Beispiel, bei dem key kein Hauptbeispiel ist: X = 1, z = 2 und key = 8 .__ Da key/z = 4 noch eine natürliche Zahl ist, wird 4 eine Lösung für unsere Gleichung und in dieser case (n/2) * y = Schlüssel ist für jedes n in N wahr. Die Anzahl der Lösungen für die Gleichung hat sich praktisch verdoppelt, da 8 keine Primzahl ist.

Wenn unser Angreifer bereits weiß, dass 8 eine mögliche Lösung für die Gleichung ist, kann er die Datei von 8 auf 4 umstellen und erhält immer noch denselben Hash.

0
Christian