444 Stimmen

Warum sollten Hash-Funktionen einen Primzahlmodulus verwenden?

Vor langer Zeit kaufte ich mir ein Buch über Datenstrukturen für 1,25 Dollar vom Wühltisch. Darin hieß es in der Erklärung für eine Hashing-Funktion, dass sie aufgrund der "Natur der Mathematik" letztlich durch eine Primzahl modifiziert werden sollte.

Was erwarten Sie von einem Buch für 1,25 Dollar?

Jedenfalls hatte ich jahrelang Zeit, über die Natur der Mathematik nachzudenken, und bin immer noch nicht dahinter gekommen.

Ist die Verteilung der Zahlen wirklich gleichmäßiger, wenn es eine Primzahl von Eimern gibt?

Oder ist das ein altes Programmierermärchen, das jeder akzeptiert, weil jeder sonst es akzeptiert?

298voto

Steve Jessop Punkte 264569

Normalerweise funktioniert eine einfache Hash-Funktion, indem die "Bestandteile" der Eingabe (Zeichen im Falle einer Zeichenkette) mit den Potenzen einer Konstante multipliziert und in einem ganzzahligen Typ zusammengerechnet werden. Ein typischer (wenn auch nicht besonders guter) Hash-Wert für eine Zeichenkette könnte zum Beispiel so aussehen:

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

Wenn dann eine Reihe von Zeichenketten mit demselben ersten Zeichen eingegeben wird, sind die Ergebnisse alle gleich modulo k, zumindest bis der Ganzzahltyp überläuft.

[Die Java-Zeichenfolge hashCode ist diesem Beispiel unheimlich ähnlich - sie führt die Zeichen in umgekehrter Reihenfolge aus, mit k=31. So erhält man auffällige Beziehungen modulo 31 zwischen Zeichenketten, die auf die gleiche Weise enden, und auffällige Beziehungen modulo 2^32 zwischen Zeichenketten, die bis auf das Ende identisch sind. Das bringt das Verhalten von hashtable nicht ernsthaft durcheinander].

Eine Hashtabelle funktioniert, indem der Modulus des Hashes über die Anzahl der Eimer genommen wird.

Es ist wichtig, dass in einer Hashtabelle keine Kollisionen für wahrscheinliche Fälle entstehen, da Kollisionen die Effizienz der Hashtabelle verringern.

Nehmen wir nun an, jemand gibt eine ganze Reihe von Werten in eine Hashtabelle ein, die in irgendeiner Beziehung zueinander stehen, z. B. alle das gleiche erste Zeichen haben. Das ist ein ziemlich vorhersehbares Nutzungsmuster, würde ich sagen, also wollen wir nicht, dass es zu viele Kollisionen gibt.

Es stellt sich heraus, dass "wegen der Natur der Mathematik", wenn die Konstante, die in der Hash verwendet wird, und die Anzahl der Eimer, sind koprimieren werden Kollisionen in einigen häufigen Fällen auf ein Minimum reduziert. Wenn sie es nicht sind koprimieren dann gibt es einige recht einfache Beziehungen zwischen den Eingaben, bei denen die Kollisionen nicht minimiert werden. Alle Hashes kommen modulo des gemeinsamen Faktors gleich heraus, was bedeutet, dass sie alle in den 1/n-ten der Buckets fallen, die diesen Wert modulo des gemeinsamen Faktors haben. Man erhält 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 relativ einfachen Anwendungsfall inakzeptabel ist, mindestens doppelt so viele Kollisionen wie normal zu erzeugen. Wenn ein Benutzer unsere Verteilung in Eimer zerlegt, dann soll es ein Unfall sein und nicht eine einfache vorhersehbare Nutzung.

Hash-Table-Implementierungen haben natürlich keine Kontrolle über die darin enthaltenen Elemente. Sie können nicht verhindern, dass sie miteinander in Beziehung stehen. Es muss also sichergestellt werden, dass die Anzahl der Konstanten und der Eimer gleich groß ist. Auf diese Weise verlässt man sich nicht allein auf die "letzte" Komponente, um den Modulus des Buckets in Bezug auf einen kleinen gemeinsamen Faktor zu bestimmen. Soweit ich weiß, müssen sie dazu nicht primär sein, sondern nur koprimal.

Wenn aber die Hash-Funktion und die Hashtabelle unabhängig voneinander geschrieben werden, dann weiß die Hashtabelle nicht, wie die Hash-Funktion funktioniert. Sie könnte eine Konstante mit kleinen Faktoren verwenden. Wenn man Glück hat, funktioniert sie vielleicht ganz anders und ist nicht linear. Wenn der Hash gut genug ist, dann ist jede beliebige Anzahl von Buckets in Ordnung. Eine paranoide Hashtabelle kann jedoch nicht von einer guten Hash-Funktion ausgehen und sollte daher eine Primzahl von Buckets verwenden. Ebenso sollte eine paranoide Hash-Funktion eine große Primzahlkonstante verwenden, um die Wahrscheinlichkeit zu verringern, dass jemand eine Anzahl von Buckets verwendet, die zufällig einen gemeinsamen Faktor mit der Konstante hat.

In der Praxis ist es meiner Meinung nach ziemlich normal, eine Potenz von 2 als Anzahl der Eimer zu verwenden. Das ist praktisch und erspart die Suche oder die Vorauswahl einer Primzahl der richtigen Größenordnung. Man verlässt sich also darauf, dass die Hash-Funktion keine geraden Multiplikatoren verwendet, was im Allgemeinen eine sichere Annahme ist. Dennoch kann es bei Hash-Funktionen wie der obigen gelegentlich zu fehlerhaftem Verhalten kommen, und die Anzahl der Primzahlen könnte hier weiterhelfen.

Das Prinzip "alles muss primär sein" ist meines Wissens eine ausreichende, aber keine notwendige Bedingung für eine gute Verteilung über Hashtabellen. Es erlaubt jedem, zu interagieren, ohne davon ausgehen zu müssen, dass die anderen dieselbe Regel befolgt haben.

[Edit: Es gibt noch einen anderen, spezielleren Grund, eine Primzahl von Buckets zu verwenden, nämlich wenn man Kollisionen mit linearer Sondierung behandelt. Dann berechnen Sie einen Stride aus dem Hashcode, und wenn dieser Stride ein Faktor der Bucket-Anzahl ist, dann können Sie nur (Bucket-Anzahl / Stride) Sondierungen durchführen, bevor Sie wieder da sind, wo Sie angefangen haben. Der Fall, den Sie am meisten vermeiden wollen, ist stride = 0, natürlich, die special-case werden muss, aber zu vermeiden, auch special-casing bucket_count / stride gleich eine kleine ganze Zahl, können Sie einfach die bucket_count Primzahl und nicht kümmern, was die stride ist, solange es nicht 0 ist].

38voto

Das erste, was Sie tun, wenn Sie eine Hash-Tabelle einfügen/auslesen, ist, den HashCode für den gegebenen Schlüssel zu berechnen und dann den richtigen Bucket zu finden, indem Sie den HashCode auf die Größe der Hash-Tabelle abschneiden, indem Sie hashCode % table_length tun. Hier sind 2 "Anweisungen", die Sie wahrscheinlich irgendwo gelesen haben

  1. Wenn Sie eine Potenz von 2 für table_length verwenden, ist die Suche nach (hashCode(key) % 2^n ) genauso einfach und schnell wie (hashCode(key) & (2^n -1)). Wenn Ihre Funktion zur Berechnung von hashCode für einen bestimmten Schlüssel jedoch nicht gut ist, werden Sie definitiv unter der Häufung vieler Schlüssel in einigen wenigen Hash-Eimern leiden.
  2. Wenn Sie jedoch Primzahlen für table_length verwenden, können die berechneten HashCodes den verschiedenen Hash-Buckets zugeordnet werden, selbst wenn Sie eine etwas dumme HashCode-Funktion haben.

Und hier ist der Beweis.

Angenommen, Ihre HashCode-Funktion ergibt unter anderem die folgenden HashCodes {x , 2x, 3x, 4x, 5x, 6x...}, dann werden alle diese in nur m Buckets geclustert, wobei m = table_length/GreatestCommonFactor(table_length, x). (Es ist trivial, dies zu überprüfen/abzuleiten). Sie können nun eine der folgenden Möglichkeiten nutzen, um die Clusterbildung zu vermeiden

Stellen Sie sicher, dass Sie nicht zu viele HashCodes generieren, die ein Vielfaches eines anderen HashCodes sind, wie z.B. {x, 2x, 3x, 4x, 5x, 6x...}. Dies kann jedoch etwas schwierig sein, wenn Ihre HashTable Millionen von Einträgen haben soll. Oder machen Sie einfach m gleich der table_length, indem Sie GreatestCommonFactor(table_length, x) gleich 1 machen, d.h. indem Sie table_length coprime mit x machen. Und wenn x so ziemlich jede Zahl sein kann, dann stellen Sie sicher, dass table_length eine Primzahl ist.

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

28voto

dz902 Punkte 3201

Ich möchte nur einige Gedanken aus den Antworten wiedergeben.

  • Hashing verwendet Modulus, so dass jeder Wert in einen bestimmten Bereich passen kann
  • Wir wollen Kollisionen zufällig verteilen
  • Zufällige Kollisionen, d.h. es gibt keine Muster, wie Kollisionen zustande kommen, oder die Änderung eines kleinen Teils der Eingabe würde einen völlig anderen Hash-Wert ergeben
  • Um eine zufällige Kollision zu vermeiden, sollten Sie die Basis ( 10 in Dezimalzahlen, 16 in hex) als Modul, denn 11 % 10 -> 1 , 21 % 10 -> 1 , 31 % 10 -> 1 zeigt es ein klares Muster der Hashwert-Verteilung: Werte mit gleichen Endziffern kollidieren
  • Vermeiden Sie die Verwendung von Potenzen der Basis ( 10^2 , 10^3 , 10^n ) als Modulus, weil es auch ein Muster erzeugt: Wert mit gleichem letzten n Ziffernsachen werden kollidieren
  • Vermeiden Sie es, irgendetwas zu verwenden, das andere Faktoren als sich selbst hat und 1 weil es ein Muster erzeugt: Vielfache eines Faktors werden zu ausgewählten Werten gehasht
  • Zum Beispiel, 9 hat 3 als Faktor, also 3 , 6 , 9 , ... 999213 wird immer in 0 , 3 , 6
  • 12 hat 3 y 2 als Faktor, also 2n wird immer in 0 , 2 , 4 , 6 , 8 , 10 y 3n wird immer in 0 , 3 , 6 , 9
  • Dies ist ein Problem, wenn die Eingaben nicht gleichmäßig verteilt sind, z. B. wenn viele Werte von 3n dann erhalten wir nur 1/3 aller möglichen Hash-Werte und hohe Kollisionsrate
  • Wenn man also eine Primzahl als Modulus verwendet, ist das einzige Muster, dass ein Vielfaches des Modulus immer zu einem Hashwert wird 0 ansonsten sind die Hashwerte gleichmäßig verteilt

15voto

AlbertoPL Punkte 11396

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

Ziemlich klare Erklärung, auch mit Bildern.

Edit: Zusammenfassend lässt sich sagen, dass Primzahlen verwendet werden, weil man die besten Chancen hat, einen eindeutigen Wert zu erhalten, wenn man die Werte mit der gewählten Primzahl multipliziert und sie alle addiert. Multipliziert man zum Beispiel bei einer Zeichenkette jeden einzelnen Buchstaben mit der Primzahl und addiert sie dann, erhält man den Hash-Wert.

Eine bessere Frage wäre, warum gerade die Zahl 31?

11voto

TT_ Punkte 1599

Primzahlen werden verwendet, weil man gute Chancen hat, einen eindeutigen Wert für eine typische Hash-Funktion zu erhalten, die Polynome modulo P verwendet. Angenommen, Sie verwenden eine solche Hash-Funktion für Zeichenketten der Länge <= N, und es kommt zu einer Kollision. Das bedeutet, dass 2 verschiedene Polynome den gleichen Wert modulo P erzeugen. Die Differenz dieser Polynome ist wiederum ein Polynom des gleichen Grades N (oder weniger). Es hat nicht mehr als N Wurzeln (hier zeigt sich die Natur der Mathematik, denn diese Behauptung gilt nur für ein Polynom über einem Feld => Primzahl). Wenn also N viel kleiner als P ist, ist es wahrscheinlich, dass es nicht zu einer Kollision kommt. Danach kann ein Experiment wahrscheinlich zeigen, dass 37 groß genug ist, um Kollisionen für eine Hash-Tabelle mit Zeichenketten der Länge 5-10 zu vermeiden, und klein genug, um sie für Berechnungen zu verwenden.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X