it-swarm.com.de

Wie funktioniert eine Hash-Tabelle?

Ich suche nach einer Erklärung, wie eine Hash-Tabelle funktioniert - im Klartext für einen Simpleton wie mich!

Ich weiß zum Beispiel, dass es den Schlüssel benötigt, den Hash berechnet (ich suche nach einer Erklärung, wie) und dann eine Art Modulo durchführt, um herauszufinden, wo er in dem Array liegt, in dem der Wert gespeichert ist, aber dort hört mein Wissen auf .

Könnte jemand den Prozess klären?

Edit: Ich frage nicht speziell nach der Berechnung von Hash-Codes, sondern nach einem allgemeinen Überblick über die Funktionsweise einer Hash-Tabelle.

467
Arec Barrwin

Hier ist eine Erklärung in Laienbegriffen.

Nehmen wir an, Sie möchten eine Bibliothek mit Büchern füllen und diese nicht nur dort verstauen, sondern sie bei Bedarf einfach wiederfinden.

Sie entscheiden also, dass, wenn die Person, die ein Buch lesen möchte, den Titel des Buches und den genauen Titel kennt, das alles ist, was es brauchen sollte. Mit dem Titel soll die Person mit Hilfe des Bibliothekars das Buch leicht und schnell finden können.

Also, wie kannst du das machen? Nun, natürlich können Sie eine Liste darüber führen, wo Sie jedes Buch abgelegt haben, aber dann haben Sie das gleiche Problem wie beim Durchsuchen der Bibliothek. Sie müssen die Liste durchsuchen. Zugegeben, die Liste wäre kleiner und einfacher zu durchsuchen, dennoch möchten Sie nicht sequenziell von einem Ende der Bibliothek (oder Liste) zum anderen suchen.

Sie möchten etwas, das Ihnen mit dem Titel des Buches sofort den richtigen Platz bietet. Sie müssen also nur zum richtigen Regal gehen und das Buch in die Hand nehmen.

Aber wie geht das? Nun, mit ein bisschen Bedacht, wenn Sie die Bibliothek füllen, und viel Arbeit, wenn Sie die Bibliothek füllen.

Anstatt nur die Bibliothek von einem Ende zum anderen zu füllen, erfinden Sie eine clevere kleine Methode. Sie nehmen den Titel des Buches und führen es durch ein kleines Computerprogramm, das eine Regalnummer und eine Steckplatznummer in diesem Regal ausspuckt. Hier platzieren Sie das Buch.

Das Schöne an diesem Programm ist, dass Sie später, wenn eine Person zurückkommt, um das Buch zu lesen, den Titel erneut durch das Programm führen und dieselbe Regal- und Steckplatznummer zurückerhalten, die Sie ursprünglich erhalten haben, und dies ist wo sich das Buch befindet.

Das Programm wird, wie bereits erwähnt, als Hash-Algorithmus oder Hash-Berechnung bezeichnet und verwendet in der Regel die darin eingegebenen Daten (in diesem Fall den Buchtitel) und berechnet daraus eine Zahl.

Nehmen wir der Einfachheit halber an, es wandelt einfach jeden Buchstaben und jedes Symbol in eine Zahl um und summiert sie alle. In Wirklichkeit ist es viel komplizierter, aber lassen wir es jetzt.

Das Schöne an einem solchen Algorithmus ist, dass, wenn Sie immer wieder dieselbe Eingabe eingeben, jedes Mal dieselbe Zahl ausgegeben wird.

Ok, so funktioniert eine Hash-Tabelle im Prinzip.

Technische Sachen folgen.

Erstens gibt es die Größe der Zahl. Normalerweise liegt die Ausgabe eines solchen Hash-Algorithmus in einem Bereich mit einer großen Anzahl, die in der Regel viel größer ist als der Speicherplatz, den Sie in Ihrer Tabelle haben. Nehmen wir zum Beispiel an, wir haben Platz für genau eine Million Bücher in der Bibliothek. Die Ausgabe der Hash-Berechnung könnte im Bereich von 0 bis 1 Milliarde liegen, was viel höher ist.

Also, was machen wir? Wir verwenden eine sogenannte Modulberechnung, die grundsätzlich besagt, dass Sie, wenn Sie bis zu der gewünschten Zahl (dh der Zahl von einer Milliarde) gezählt haben, aber in einem viel kleineren Bereich bleiben möchten, jedes Mal, wenn Sie das Limit dieses kleineren Bereichs erreicht haben, bei dem Sie angefangen haben 0, aber Sie müssen verfolgen, wie weit Sie in der großen Sequenz gekommen sind.

Angenommen, die Ausgabe des Hash-Algorithmus liegt im Bereich von 0 bis 20 und Sie erhalten den Wert 17 aus einem bestimmten Titel. Wenn die Bibliothek nur aus 7 Büchern besteht, zählen Sie 1, 2, 3, 4, 5, 6, und wenn Sie 7 erreichen, beginnen Sie wieder bei 0. Da wir 17-mal zählen müssen, haben wir 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3 und die endgültige Zahl ist 3.

Natürlich wird die Modulberechnung nicht so durchgeführt, sondern mit Division und Rest. Der Rest des Teilens von 17 durch 7 ist 3 (7 geht bei 14 zweimal in 17 über und die Differenz zwischen 17 und 14 ist 3).

So setzen Sie das Buch in Steckplatz Nummer 3.

Dies führt zum nächsten Problem. Kollisionen. Da der Algorithmus keine Möglichkeit hat, die Bücher so zu platzieren, dass sie die Bibliothek genau füllen (oder die Hash-Tabelle, wenn Sie so wollen), wird er immer eine Zahl berechnen, die zuvor verwendet wurde. Im Sinne der Bibliothek gibt es dort bereits ein Buch, wenn Sie das Regal und die Steckplatznummer erreichen, in die Sie ein Buch legen möchten.

Es gibt verschiedene Methoden zur Behandlung von Kollisionen, einschließlich der Ausführung der Daten in einer weiteren Berechnung, um einen anderen Punkt in der Tabelle zu erhalten ( Doppel-Hashing ) oder einfach um einen Platz zu finden, der dem nahe kommt, den Sie erhalten haben (dh direkt nebenan) im vorherigen Buch unter der Annahme, dass der Schlitz auch als lineares Abtasten ) verfügbar war. Dies würde bedeuten, dass Sie etwas graben müssen, wenn Sie versuchen, das Buch später zu finden, aber es ist immer noch besser, als einfach an einem Ende der Bibliothek zu beginnen.

Schließlich möchten Sie vielleicht irgendwann mehr Bücher in die Bibliothek stellen, als die Bibliothek zulässt. Mit anderen Worten, Sie müssen eine größere Bibliothek erstellen. Da die exakte Position in der Bibliothek anhand der exakten und aktuellen Größe der Bibliothek berechnet wurde, muss beim Ändern der Bibliotheksgröße möglicherweise für alle Bücher eine neue Position gesucht werden, da die Berechnung zum Auffinden der Positionen durchgeführt wurde hat sich verändert.

Ich hoffe, diese Erklärung war ein bisschen bodenständiger als Eimer und Funktionen :)

Verwendung und Lingo:

  1. Hash-Tabellen werden zum schnellen Speichern und Abrufen von Daten (oder Datensätzen) verwendet.
  2. Datensätze werden in gespeichert eimer mit hash-Schlüssel
  3. Hash-Schlüssel werden berechnet, indem ein Hashalgorithmus auf einen ausgewählten Wert angewendet wird (der schlüssel Wert) im Datensatz enthalten. Dieser ausgewählte Wert muss allen Datensätzen gemeinsam sein.
  4. Jeder eimer kann mehrere Datensätze enthalten, die in einer bestimmten Reihenfolge angeordnet sind.

Beispiel aus der realen Welt:

Hash & Co., gegründet 1803 und ohne Computertechnologie, verfügte über insgesamt 300 Aktenschränke, um die detaillierten Informationen (Aufzeichnungen) für ihre rund 30.000 Kunden aufzubewahren. Jeder Dateiordner wurde eindeutig mit seiner Kundennummer identifiziert, einer eindeutigen Nummer von 0 bis 29.999.

Die damaligen Archivare mussten schnell Kundendaten für das Arbeitspersonal abrufen und speichern. Die Mitarbeiter hatten entschieden, dass es effizienter sein würde, eine Hash-Methode zum Speichern und Abrufen ihrer Aufzeichnungen zu verwenden.

Um einen Kundendatensatz einzureichen, verwenden die Sachbearbeiter die eindeutige Kundennummer, die in den Ordner geschrieben ist. Unter Verwendung dieser Kundennummer würden sie das modulieren hash-Schlüssel um 300, um den Aktenschrank zu identifizieren, in dem er enthalten ist. Wenn sie den Aktenschrank öffneten, stellten sie fest, dass er viele Ordner enthielt, die nach Kundennummer sortiert waren. Nachdem sie den richtigen Ort gefunden hatten, steckten sie ihn einfach ein.

Um einen Kundendatensatz abzurufen, erhalten die Sachbearbeiter eine Kundennummer auf einem Zettel. Verwenden Sie diese eindeutige Kundennummer (die hash-Schlüssel), modulieren sie es um 300, um festzustellen, in welchem ​​Aktenschrank sich der Kundenordner befindet. Als sie den Aktenschrank öffneten, stellten sie fest, dass er viele Ordner enthielt, die nach Kundennummer sortiert waren. Durchsuchen Sie die Datensätze, finden Sie schnell den Client-Ordner und rufen Sie ihn ab.

In unserem realen Beispiel ist unser eimer sind aktenschränke und unser aufzeichnungen sind dateiordner.


Es ist wichtig, sich daran zu erinnern, dass Computer (und ihre Algorithmen) mit Zahlen besser umgehen als mit Zeichenfolgen. Der Zugriff auf ein großes Array über einen Index ist also erheblich schneller als der sequentielle Zugriff.

Wie Simon schon erwähnt hat was ich glaube zu sein sehr wichtig Der Hashing-Teil besteht darin, einen großen Bereich (von beliebiger Länge, normalerweise Zeichenfolgen usw.) zu transformieren und ihn für die Indizierung einem kleinen Bereich (von bekannter Größe, normalerweise Zahlen) zuzuordnen. Dies ist sehr wichtig, um sich zu erinnern!

Im obigen Beispiel werden die 30.000 möglichen Clients auf einen kleineren Bereich abgebildet.


Die Hauptidee dabei ist, Ihren gesamten Datensatz in Segmente zu unterteilen, um die eigentliche Suche zu beschleunigen, die normalerweise zeitaufwändig ist. In unserem obigen Beispiel würde jeder der 300 Aktenschränke (statistisch) ungefähr 100 Datensätze enthalten. Das Durchsuchen von 100 Datensätzen (unabhängig von der Reihenfolge) ist viel schneller als das Durchsuchen von 30.000 Datensätzen.

Sie haben vielleicht bemerkt, dass einige dies tatsächlich bereits tun. Anstatt jedoch eine Hash-Methode zur Generierung eines Hash-Schlüssels zu entwickeln, wird in den meisten Fällen einfach der erste Buchstabe des Nachnamens verwendet. Wenn Sie also 26 Aktenschränke haben, die jeweils einen Buchstaben von A bis Z enthalten, haben Sie theoretisch nur Ihre Daten segmentiert und den Ablage- und Abrufprozess verbessert.

Hoffe das hilft,

Jeach!

95
Jeach

Es stellt sich heraus, dass dies ein ziemlich tiefes theoretisches Gebiet ist, aber die Grundzüge sind einfach.

Im Wesentlichen ist eine Hash-Funktion nur eine Funktion, die Dinge aus einem Leerzeichen (z. B. Zeichenfolgen beliebiger Länge) entnimmt und einem für die Indizierung nützlichen Leerzeichen (z. B. Ganzzahlen ohne Vorzeichen) zuordnet.

Wenn Sie nur eine kleine Menge von Dingen zu Hashing haben, können Sie diese Dinge einfach als ganze Zahlen interpretieren und sind fertig (z. B. 4-Byte-Strings).

Normalerweise haben Sie jedoch einen viel größeren Raum. Wenn der Bereich der Dinge, die Sie als Schlüssel zulassen, größer ist als der Bereich der Dinge, die Sie zum Indizieren verwenden (Ihre uint32 oder was auch immer), können Sie möglicherweise keinen eindeutigen Wert für jeden haben. Wenn zwei oder mehr Dinge zum gleichen Ergebnis führen, müssen Sie die Redundanz auf angemessene Weise handhaben (dies wird normalerweise als Kollision bezeichnet, und wie Sie damit umgehen oder nicht, hängt ein wenig davon ab, was Sie sind mit dem Hash für).

Dies impliziert, dass Sie möchten, dass es unwahrscheinlich ist, dass dasselbe Ergebnis erzielt wird, und Sie möchten wahrscheinlich auch, dass die Hash-Funktion schnell ist.

Das Ausbalancieren dieser beiden Eigenschaften (und einiger anderer) hat viele Menschen beschäftigt!

In der Praxis sollten Sie normalerweise in der Lage sein, eine Funktion zu finden, von der bekannt ist, dass sie für Ihre Anwendung gut funktioniert, und diese zu verwenden.

Um diese Funktion als Hash-Tabelle zu verwenden: Stellen Sie sich vor, Sie interessieren sich nicht für die Speichernutzung. Anschließend können Sie ein Array erstellen, solange Ihre Indizierung festgelegt ist (z. B. alle Uint32-Arrays). Wenn Sie der Tabelle etwas hinzufügen, kreuzen Sie den Schlüssel und sehen sich das Array an diesem Index an. Wenn dort nichts ist, setzen Sie Ihren Wert dort. Wenn es dort bereits etwas gibt, fügen Sie diesen neuen Eintrag zu einer Liste von Dingen an dieser Adresse hinzu, zusammen mit genügend Informationen (Ihr Originalschlüssel oder etwas Kluges), um herauszufinden, welcher Eintrag tatsächlich zu welchem ​​Schlüssel gehört.

So ist jeder Eintrag in Ihrer Hash-Tabelle (das Array) entweder leer oder enthält einen Eintrag oder eine Liste von Einträgen. Das Abrufen ist so einfach wie das Indizieren in das Array und entweder das Zurückgeben des Werts oder das Durchlaufen der Werteliste und das Zurückgeben des richtigen Werts.

In der Praxis ist dies normalerweise nicht möglich, da zu viel Speicher verschwendet wird. Sie tun also alles auf der Grundlage eines spärlichen Arrays (wobei die einzigen Einträge diejenigen sind, die Sie tatsächlich verwenden, alles andere implizit null ist).

Es gibt viele Schemata und Tricks, um diese Arbeit zu verbessern, aber das sind die Grundlagen.

64
simon

Viele Antworten, aber keine davon ist sehr visuell, und Hash-Tabellen können leicht "klicken", wenn sie visualisiert werden.

Hash-Tabellen werden häufig als Arrays von verknüpften Listen implementiert. Wenn wir uns eine Tabelle vorstellen, in der die Namen von Personen gespeichert sind, könnte sie nach einigen Einfügungen wie folgt im Speicher abgelegt werden, wobei () - eingeschlossene Zahlen Hash-Werte des Textes/Namens sind.

bucket#  bucket content / linked list

[0]      --> "sue"(780) --> null
[1]      null
[2]      --> "fred"(42) --> "bill"(9282) --> "jane"(42) --> null
[3]      --> "mary"(73) --> null
[4]      null
[5]      --> "masayuki"(75) --> "sarwar"(105) --> null
[6]      --> "margaret"(2626) --> null
[7]      null
[8]      --> "bob"(308) --> null
[9]      null

Ein paar Punkte:

  • jeder der Array-Einträge (Indizes [0], [1] ...) wird als bucket und bezeichnet Startet eine - möglicherweise leere - verknüpfte Liste von Werten (auch bekannt als Elementen, in diesem Beispiel - Personen Namen)
  • jeder Wert (z. B. "fred" mit Hash 42) wird aus dem Bucket [hash % number_of_buckets] verknüpft, z. 42 % 10 == [2]; % Ist der Modulo-Operator - Rest dividiert durch die Anzahl der Buckets
  • mehrere Datenwerte können collide am selben Bucket sein und von diesem verknüpft werden, meistens, weil ihre Hash-Werte nach der Modulo-Operation kollidieren (z. B. 42 % 10 == [2] und 9282 % 10 == [2]), aber gelegentlich, weil die Hashwerte gleich sind (z. B. "fred" und "jane", beide mit dem obigen Hash 42)
    • die meisten Hash-Tabellen behandeln Kollisionen - mit leicht verringerter Leistung, aber ohne funktionale Verwirrung - indem sie den vollständigen Wert (hier Text) eines gesuchten Werts mit jedem Wert vergleichen, der sich bereits in der verknüpften Liste im Hash-to-Bucket befindet

Verknüpfte Listenlängen beziehen sich auf den Auslastungsfaktor und nicht auf die Anzahl der Werte

Wenn die Tabellengröße zunimmt, ändern sich die wie oben implementierten Hash-Tabellen in der Regel von selbst (dh erstellen Sie ein größeres Array von Buckets, erstellen Sie neue/aktualisierte verknüpfte Listen, löschen Sie das alte Array), um das Verhältnis von Werten zu Buckets (aka =) beizubehalten Lastfaktor) irgendwo im Bereich von 0,5 bis 1,0.

Die tatsächliche Formel für andere Auslastungsfaktoren gibt Hans in einem Kommentar weiter unten an, jedoch als Anhaltspunkt: Bei Auslastungsfaktor 1 und einer Hash-Funktion für die kryptografische Stärke ist 1/e (~ 36,8%) der Eimer in der Regel leer, weitere 1/e (~ 36,8%) haben ein Element, 1/(2e) oder ~ 18,4% zwei Elemente, 1/(3! E) ungefähr 6,1% drei Elemente, 1/(4! E) oder ~ 1,5% vier Elemente, 1/(5! E) ~ .3% haben fünf usw. - Die durchschnittliche Kettenlänge von nicht leeren Eimern beträgt ~ 1,58, unabhängig davon, wie viele Elemente in der Tabelle enthalten sind (dh ob es 100 Elemente und 100 Eimer oder 100 Millionen gibt Elemente und 100 Millionen Buckets), weshalb wir Nachschlagen/Einfügen/Löschen als O (1) konstante Zeitoperationen bezeichnen.

Wie eine Hash-Tabelle Schlüssel mit Werten verknüpfen kann

Bei einer oben beschriebenen Implementierung einer Hash-Tabelle können wir uns vorstellen, einen Wertetyp wie struct Value { string name; int age; }; Und Gleichheitsvergleichs- und Hash-Funktionen zu erstellen, die nur das Feld name betrachten (das Alter wird ignoriert), und dann Es passiert etwas Wunderbares: Wir können Value Datensätze wie {"sue", 63} in der Tabelle speichern und dann später nach "sue" suchen, ohne ihr Alter zu kennen, den gespeicherten Wert finden und ihr Alter wiederherstellen oder sogar aktualisieren
- Alles Gute zum Geburtstag Sue - was interessanterweise den Hash-Wert nicht ändert und es nicht erforderlich macht, dass wir Sues Datensatz in einen anderen Eimer verschieben.

Wenn wir dies tun, verwenden wir die Hash-Tabelle als assoziativer Container aka Map , und die darin gespeicherten Werte können als konsistent angesehen werden von einem Schlüssel (Name) und einem oder mehreren anderen Feldern, die - verwirrenderweise - immer noch als Wert ​​(in meinem Beispiel nur das Alter) bezeichnet werden. Eine als Map verwendete Hash-Tabellen-Implementierung wird als Hash-Map bezeichnet.

Dies steht im Gegensatz zu dem Beispiel weiter oben in dieser Antwort, in dem wir diskrete Werte wie "sue" gespeichert haben, die Sie sich als eigenen Schlüssel vorstellen können: Diese Art der Verwendung wird als Hash-Set ​​bezeichnet.

Es gibt andere Möglichkeiten, eine Hash-Tabelle zu implementieren

Nicht alle Hashtabellen verwenden verknüpfte Listen (bekannt als separate Verkettung ), aber die meisten Mehrzwecktabellen als Hauptalternative geschlossenes Hashing (aka open addressing) - insbesondere bei unterstützten Löschvorgängen - weist weniger stabile Leistungseigenschaften mit kollisionsanfälligen Schlüsseln/Hash-Funktionen auf.


Ein paar Worte zu Hash-Funktionen

Starkes Hasching ...

Eine allgemeine Aufgabe der kollisionsminimierenden Hash-Funktion im schlimmsten Fall besteht darin, die Schlüssel effektiv zufällig um die Eimer der Hash-Tabelle zu sprühen und dabei immer den gleichen Hash-Wert für denselben Schlüssel zu generieren. Selbst ein Bit, das sich irgendwo im Schlüssel ändert, würde im Idealfall - zufällig - etwa die Hälfte der Bits im resultierenden Hash-Wert umdrehen.

Dies ist normalerweise mit Mathematik orchestriert, die zu kompliziert ist, als dass ich sie hätte anfassen können. Ich werde einen leicht verständlichen Weg erwähnen - nicht den skalierbarsten oder Cache-freundlichsten, aber von Natur aus elegant (wie die Verschlüsselung mit einem einmaligen Pad!) -, da ich denke, dass er dazu beiträgt, die oben genannten wünschenswerten Eigenschaften zu erreichen. Angenommen, Sie haben 64-Bit-doubles gehasht - Sie könnten 8 Tabellen mit jeweils 256 Zufallszahlen erstellen (Code unten) und dann jedes 8-Bit/1-Byte-Segment der Speicherdarstellung von double verwenden um in eine andere Tabelle zu indexieren, XOR-Verknüpfung der Zufallszahlen, die Sie nachschlagen. Bei diesem Ansatz ist leicht zu erkennen, dass ein Bit (im Sinne einer binären Ziffer), das sich irgendwo in double ändert, dazu führt, dass eine andere Zufallszahl in einer der Tabellen nachgeschlagen wird und ein völlig unkorrelierter Endwert vorliegt.

// note caveats above: cache unfriendly (SLOW) but strong hashing...
size_t random[8][256] = { ...random data... };
const char* p = (const char*)&my_double;
size_t hash = random[0][p[0]] ^ random[1][p[1]] ^ ... ^ random[7][p[7]];

Schwaches, aber oft schnelles Hashing ...

Die Hashing-Funktionen vieler Bibliotheken lassen ganze Zahlen unverändert durch (bekannt als trivial oder identity Hash-Funktion); Es ist das andere Extrem aus dem oben beschriebenen starken Hashing. Ein Identitäts-Hash ist im schlimmsten Fall extrem kollisionsanfällig, aber die Hoffnung ist, dass im relativ häufigen Fall von Integer-Schlüsseln, die dazu neigen, sich zu erhöhen (möglicherweise mit einigen Lücken), sie aufeinanderfolgend abgebildet werden Eimer, die weniger leer lassen als zufällige Hashing-Blätter (unsere ~ 36,8% bei Lastfaktor 1, die zuvor erwähnt wurden), haben dadurch weniger Kollisionen und weniger länger verknüpfte Listen von kollidierenden Elementen, als dies durch zufällige Zuordnungen erreicht wird. Es ist auch großartig, die Zeit zu sparen, die zum Generieren eines starken Hashs benötigt wird. Wenn Schlüssel nachgeschlagen werden, werden sie in Eimern in der Nähe im Speicher gefunden, wodurch die Cache-Treffer verbessert werden. Wenn die Tasten nicht ​​inkrementieren, ist die Hoffnung, dass sie zufällig genug sind und keine starke Hash-Funktion benötigen, um ihre Platzierung in Eimern vollständig zufällig zu machen.

42
Tony Delroy

Ihr seid sehr nahe daran, dies vollständig zu erklären, aber es fehlen ein paar Dinge. Die Hash-Tabelle ist nur ein Array. Das Array selbst enthält in jedem Steckplatz etwas. Sie werden mindestens den Hashwert oder den Wert selbst in diesem Slot speichern. Darüber hinaus können Sie auch eine verknüpfte/verkettete Liste von Werten speichern, die auf diesem Steckplatz kollidiert sind, oder die offene Adressierungsmethode verwenden. Sie können auch einen oder mehrere Zeiger auf andere Daten speichern, die Sie aus diesem Steckplatz abrufen möchten.

Es ist wichtig zu beachten, dass der Hashwert selbst im Allgemeinen nicht den Steckplatz angibt, in den der Wert eingefügt werden soll. Ein Hashwert kann beispielsweise ein negativer ganzzahliger Wert sein. Offensichtlich kann eine negative Zahl nicht auf eine Array-Position verweisen. Darüber hinaus sind Hash-Werte in der Regel um ein Vielfaches größer als die verfügbaren Slots. Daher muss eine weitere Berechnung von der Hash-Tabelle selbst durchgeführt werden, um herauszufinden, in welchen Slot der Wert eingefügt werden soll. Dies geschieht mit einer Modul-Mathematik-Operation wie:

uint slotIndex = hashValue % hashTableSize;

Dieser Wert ist der Steckplatz, in den der Wert eingefügt wird. Wenn bei der offenen Adressierung der Steckplatz bereits mit einem anderen Hashwert und/oder anderen Daten gefüllt ist, wird die Moduloperation erneut ausgeführt, um den nächsten Steckplatz zu finden:

slotIndex = (remainder + 1) % hashTableSize;

Ich nehme an, dass es andere fortgeschrittenere Methoden zur Bestimmung des Slot-Index gibt, aber dies ist die übliche, die ich gesehen habe ... würde mich für alle anderen interessieren, die eine bessere Leistung erbringen.

Wenn Sie mit der Modul-Methode eine Tabelle mit der Größe 1000 haben, wird jeder Hash-Wert zwischen 1 und 1000 in den entsprechenden Slot verschoben. Alle negativen Werte und alle Werte größer als 1000 sind potenziell kollidierende Slot-Werte. Die Wahrscheinlichkeit, dass dies geschieht, hängt sowohl von Ihrer Hash-Methode als auch davon ab, wie viele Elemente Sie insgesamt zur Hash-Tabelle hinzufügen. Im Allgemeinen empfiehlt es sich, die Größe der Hashtabelle so festzulegen, dass die Gesamtzahl der hinzugefügten Werte nur etwa 70% der Größe entspricht. Wenn Ihre Hash-Funktion eine gute, gleichmäßige Verteilung leistet, treten im Allgemeinen nur sehr wenige bis keine Bucket-/Slot-Kollisionen auf, und sie wird sowohl für Such- als auch für Schreibvorgänge sehr schnell ausgeführt. Wenn die Gesamtzahl der hinzuzufügenden Werte nicht im Voraus bekannt ist, nehmen Sie mit einer beliebigen Methode eine gute Schätzung vor und ändern Sie dann die Größe Ihrer Hashtabelle, sobald die Anzahl der hinzugefügten Elemente 70% der Kapazität erreicht.

Ich hoffe das hat geholfen.

PS - In C # ist die GetHashCode() -Methode ziemlich langsam und führt unter vielen von mir getesteten Bedingungen zu Istwertkollisionen. Erstellen Sie Ihre eigene Hash-Funktion und versuchen Sie, sie NIEMALS dazu zu bringen, mit den von Ihnen gehashten Daten zu kollidieren. Führen Sie sie schneller als GetHashCode aus und haben Sie eine ziemlich gleichmäßige Verteilung. Ich habe dies mit langen statt int-Size-Hashcode-Werten gemacht und es hat ziemlich gut mit bis zu 32 Millionen gesamten Hashwerten in der Hashtabelle mit 0 Kollisionen funktioniert. Leider kann ich den Code nicht weitergeben, da er meinem Arbeitgeber gehört ... aber ich kann feststellen, dass er für bestimmte Datendomains möglich ist. Wenn Sie dies erreichen können, ist die Hash-Tabelle SEHR schnell. :)

24
Chris

So funktioniert es nach meinem Verständnis:

Hier ein Beispiel: Stellen Sie sich den gesamten Tisch als eine Reihe von Eimern vor. Angenommen, Sie haben eine Implementierung mit alphanumerischen Hash-Codes und einen Bucket für jeden Buchstaben des Alphabets. Diese Implementierung fügt jeden Artikel, dessen Hash-Code mit einem bestimmten Buchstaben beginnt, in den entsprechenden Bereich ein.

Angenommen, Sie haben 200 Objekte, aber nur 15 davon haben Hash-Codes, die mit dem Buchstaben "B" beginnen. Die Hash-Tabelle müsste nur die 15 Objekte im 'B'-Bucket und nicht alle 200 Objekte durchsuchen.

Was die Berechnung des Hash-Codes angeht, ist nichts Magisches daran. Das Ziel ist nur, dass unterschiedliche Objekte unterschiedliche Codes zurückgeben und dass gleiche Objekte gleiche Codes zurückgeben. Sie könnten eine Klasse schreiben, die für alle Instanzen immer dieselbe Ganzzahl wie ein Hash-Code zurückgibt, aber Sie würden im Wesentlichen die Nützlichkeit einer Hash-Tabelle zerstören, da sie nur ein riesiger Bucket werden würde.

17
AndreiM

Kurz und bündig:

Eine Hash-Tabelle fasst ein Array zusammen und nennt es internalArray. Elemente werden auf folgende Weise in das Array eingefügt:

let insert key value =
    internalArray[hash(key) % internalArray.Length] <- (key, value)
    //oversimplified for educational purposes

Manchmal werden zwei Schlüssel auf denselben Index im Array gehasht, und Sie möchten beide Werte beibehalten. Ich möchte beide Werte in demselben Index speichern, der einfach zu codieren ist, indem internalArray zu einem Array verknüpfter Listen gemacht wird:

let insert key value =
    internalArray[hash(key) % internalArray.Length].AddLast(key, value)

Wenn ich also einen Gegenstand aus meiner Hash-Tabelle holen wollte, könnte ich schreiben:

let get key =
    let linkedList = internalArray[hash(key) % internalArray.Length]
    for (testKey, value) in linkedList
        if (testKey = key) then return value
    return null

Löschvorgänge sind genauso einfach zu schreiben. Wie Sie sehen können, ist das Einfügen, Nachschlagen und Entfernen aus unserem Array verknüpfter Listen fast O (1).

Wenn unser internes Array zu voll wird, möglicherweise mit einer Kapazität von 85%, können wir die Größe des internen Arrays ändern und alle Elemente aus dem alten Array in das neue Array verschieben.

13
Juliet

Es ist noch einfacher.

Eine Hash-Tabelle ist nichts anderes als ein Array (normalerweise dünn eins) von Vektoren, die Schlüssel/Wert-Paare enthalten. Die maximale Größe dieses Arrays ist normalerweise kleiner als die Anzahl der Elemente in der Menge der möglichen Werte für den Datentyp, der in der Hash-Tabelle gespeichert wird.

Der Hash-Algorithmus wird verwendet, um basierend auf den Werten des Elements, das im Array gespeichert wird, einen Index für dieses Array zu generieren.

Hier werden Vektoren von Schlüssel/Wert-Paaren im Array gespeichert. Da die Menge der Werte, die Indizes im Array sein können, in der Regel kleiner ist als die Anzahl aller möglichen Werte, die der Typ haben kann, ist es möglich, dass Ihr Hash Der Algorithmus generiert denselben Wert für zwei separate Schlüssel. Ein guter Hash-Algorithmus verhindert dies so gut wie möglich (weshalb er normalerweise auf den Typ verwiesen wird, weil er spezifische Informationen hat, die ein allgemeiner Hash-Algorithmus enthält) kann unmöglich wissen), aber es ist unmöglich zu verhindern.

Aus diesem Grund können Sie mehrere Schlüssel haben, die denselben Hash-Code generieren. In diesem Fall werden die Elemente im Vektor durchlaufen, und es wird ein direkter Vergleich zwischen dem Schlüssel im Vektor und dem Schlüssel durchgeführt, nach dem gesucht wird. Wenn es gefunden wird, wird great und der mit dem Schlüssel verknüpfte Wert zurückgegeben, andernfalls wird nichts zurückgegeben.

10
casperOne

Sie nehmen eine Menge Dinge und eine Reihe.

Für jede Sache erstellen Sie einen Index, der als Hash bezeichnet wird. Das Wichtigste an dem Hash ist, dass es viel "verstreut"; Sie möchten nicht, dass zwei ähnliche Dinge ähnliche Hashes haben.

Sie legen Ihre Sachen in das Array an der durch den Hash angegebenen Position. Es kann mehr als eine Sache zu einem bestimmten Hash kommen, also speichern Sie die Dinge in Arrays oder etwas anderem Passendem, was wir im Allgemeinen einen Eimer nennen.

Wenn Sie Dinge im Hash nachschlagen, gehen Sie die gleichen Schritte durch, ermitteln den Hash-Wert, sehen dann, was sich an dieser Stelle im Eimer befindet, und prüfen, ob es das ist, wonach Sie suchen.

Wenn Ihr Hash gut funktioniert und Ihr Array groß genug ist, gibt es höchstens ein paar Dinge an einem bestimmten Index im Array, sodass Sie sich nicht viel ansehen müssen.

Wenn Sie Bonuspunkte erhalten möchten, stellen Sie sicher, dass beim Zugriff auf Ihre Hash-Tabelle das gefundene Objekt (falls vorhanden) an den Anfang des Buckets verschoben wird, sodass es beim nächsten Mal als erstes überprüft wird.

9
chaos

Hier ist eine andere Sichtweise.

Ich gehe davon aus, dass Sie das Konzept eines Arrays A verstehen. Dies unterstützt die Indizierungsoperation, bei der Sie in einem Schritt zum i-ten Element A [I] gelangen, unabhängig davon, wie groß A ist.

Wenn Sie beispielsweise Informationen über eine Gruppe von Personen mit unterschiedlichem Alter speichern möchten, besteht eine einfache Möglichkeit darin, ein Array zu erstellen, das groß genug ist, und das Alter jeder Person als Index für das Array zu verwenden. Auf diese Weise können Sie in einem Schritt auf die Informationen einer beliebigen Person zugreifen.

Aber natürlich kann es mehr als eine Person mit dem gleichen Alter geben. Sie fügen also bei jedem Eintrag eine Liste aller Personen ein, die dieses Alter haben. So können Sie in einem Schritt auf die Informationen einer einzelnen Person zugreifen und ein bisschen in dieser Liste suchen (als "Eimer" bezeichnet). Es wird nur langsamer, wenn so viele Leute da sind, dass die Eimer groß werden. Dann brauchen Sie ein größeres Array und eine andere Möglichkeit, mehr identifizierende Informationen über die Person zu erhalten, wie die ersten Buchstaben ihres Nachnamens, anstatt das Alter zu verwenden.

Das ist die Grundidee. Anstatt das Alter zu verwenden, kann jede Funktion der Person verwendet werden, die eine gute Werteverteilung erzeugt. Das ist die Hash-Funktion. Als ob Sie jedes dritte Bit der ASCII) - Darstellung des Namens der Person in einer bestimmten Reihenfolge nehmen könnten. Alles, was zählt, ist, dass Sie nicht möchten, dass zu viele Leute in denselben Eimer hacken, denn die geschwindigkeit hängt davon ab, ob die schaufeln klein bleiben.

3
Mike Dunlavey

Alle bisherigen Antworten sind gut und zeigen verschiedene Aspekte der Funktionsweise einer Hashtabelle auf. Hier ist ein einfaches Beispiel, das hilfreich sein könnte. Nehmen wir an, wir möchten einige Elemente mit Kleinbuchstaben als Schlüssel speichern.

Wie Simon erklärte, wird die Hash-Funktion verwendet, um einen großen Raum auf einen kleinen Raum abzubilden. Eine einfache, naive Implementierung einer Hash-Funktion für unser Beispiel könnte den ersten Buchstaben des Strings nehmen und ihn einer ganzen Zahl zuordnen, also hat "Alligator" den Hash-Code 0, "Bee" den Hash-Code 1. " Zebra "wäre 25, etc.

Als nächstes haben wir ein Array von 26 Buckets (in Java könnten es ArrayLists sein) und wir setzen das Element in den Bucket, der dem Hash-Code unseres Schlüssels entspricht. Wenn wir mehr als ein Element haben, dessen Schlüssel mit demselben Buchstaben beginnt, haben sie denselben Hash-Code. Würden also alle in den Bucket nach diesem Hash-Code suchen, müsste eine lineare Suche im Bucket nach erfolgen Finde einen bestimmten Gegenstand.

In unserem Beispiel würde es sehr gut funktionieren, wenn wir nur ein paar Dutzend Elemente hätten, deren Tasten sich über das Alphabet erstrecken. Wenn wir jedoch eine Million Elemente hätten oder alle Schlüssel mit 'a' oder 'b' beginnen würden, wäre unsere Hash-Tabelle nicht ideal. Um eine bessere Leistung zu erzielen, benötigen wir eine andere Hash-Funktion und/oder mehr Buckets.

3
Greg Graham

Wie der Hash berechnet wird, hängt normalerweise nicht von der Hash-Tabelle ab, sondern von den hinzugefügten Elementen. In Frameworks/Basisklassenbibliotheken wie .net und Java verfügt jedes Objekt über eine GetHashCode () - (oder ähnliche) Methode, die einen Hashcode für dieses Objekt zurückgibt. Der ideale Hash-Code-Algorithmus und die genaue Implementierung hängen von den im Objekt dargestellten Daten ab.

2
Lucero

Eine Hash-Tabelle arbeitet vollständig mit der Tatsache, dass die praktische Berechnung dem Modell einer Maschine mit wahlfreiem Zugriff folgt, d. H. Auf den Wert an einer beliebigen Adresse im Speicher kann in O(1) Zeit oder konstanter Zeit zugegriffen werden.

Wenn ich also ein Universum von Schlüsseln habe (Satz aller möglichen Schlüssel, die ich in einer Anwendung verwenden kann, z. B. Rollennummer für Schüler, wenn es 4-stellig ist, dann ist dieses Universum ein Satz von Zahlen von 1 bis 9999) und a So ordnen Sie sie einem endlichen Satz von Größen zu Ich kann Speicher in meinem System zuweisen, theoretisch ist meine Hash-Tabelle bereit.

Im Allgemeinen ist in Anwendungen die Größe des Schlüsseluniversums sehr groß als die Anzahl der Elemente, die ich zur Hash-Tabelle hinzufügen möchte (ich möchte keinen 1-GB-Speicher für Hash-Werte wie 10000 oder 100000 Ganzzahlen verschwenden, da diese 32 sind bit long in binary reprsentaion). Also benutzen wir dieses Hashing. Es ist eine Art "mathematische" Mischoperation, die mein großes Universum auf eine kleine Menge von Werten abbildet, die ich im Speicher unterbringen kann. In der Praxis hat der Platz einer Hash-Tabelle häufig die gleiche "Ordnung" (Big-O) wie die (Anzahl der Elemente * Größe jedes Elements). Wir verschwenden also nicht viel Speicher.

Wenn eine große Menge einer kleinen Menge zugeordnet ist, muss die Zuordnung mehrere Male erfolgen. So werden verschiedene Schlüssel dem gleichen Raum zugewiesen (?? nicht angemessen). Es gibt ein paar Möglichkeiten damit umzugehen, ich kenne nur die populären zwei von ihnen:

  • Verwenden Sie den Bereich, der dem Wert zugewiesen werden soll, als Referenz auf eine verknüpfte Liste. In dieser verknüpften Liste werden ein oder mehrere Werte gespeichert, die sich in mehreren Zuordnungen in demselben Slot befinden. Die verknüpfte Liste enthält auch Schlüssel, die jemandem bei der Suche helfen. Es ist wie bei vielen Leuten in derselben Wohnung, wenn ein Lieferbote kommt, er ins Zimmer geht und speziell nach dem Typen fragt.
  • Verwenden Sie eine Double-Hash-Funktion in einem Array, die jedes Mal dieselbe Folge von Werten liefert, anstatt nur einen einzigen Wert. Wenn ich einen Wert speichere, sehe ich, ob der benötigte Speicherplatz frei oder belegt ist. Wenn es frei ist, kann ich meinen Wert dort speichern. Wenn es belegt ist, nehme ich den nächsten Wert aus der Sequenz und so weiter, bis ich einen freien Ort finde und meinen Wert dort speichere. Wenn ich nach dem Wert suche oder ihn wieder erhalte, gehe ich auf den gleichen Pfad zurück, wie er in der Sequenz angegeben ist, und frage an jedem Ort nach dem Wert, bis ich ihn gefunden habe, oder suche nach allen möglichen Orten im Array.

Die Einführung in Algorithmen von CLRS bietet einen sehr guten Einblick in das Thema.

2
div

Für alle, die Programmiersprache suchen, ist hier, wie es funktioniert. Die interne Implementierung von erweiterten Hashtabellen weist viele Schwierigkeiten und Optimierungen bei der Speicherzuweisung/Freigabe und Suche auf, aber die Idee auf oberster Ebene wird sehr ähnlich sein.

(void) addValue : (object) value
{
   int bucket = calculate_bucket_from_val(value);
   if (bucket) 
   {
       //do nothing, just overwrite
   }
   else   //create bucket
   {
      create_extra_space_for_bucket();
   }
   put_value_into_bucket(bucket,value);
}

(bool) exists : (object) value
{
   int bucket = calculate_bucket_from_val(value);
   return bucket;
}

dabei ist calculate_bucket_from_val() die Hashing-Funktion, bei der die gesamte Eindeutigkeitsmagie auftreten muss.

Die Faustregel lautet: Damit ein bestimmter Wert eingefügt werden kann, muss der Bucket EINZIGARTIG UND VON DEM WERT ABLEITBAR sein, den er SPEICHERN soll.

Bucket ist ein beliebiger Bereich, in dem die Werte gespeichert werden - hier habe ich ihn als Array-Index beibehalten, aber möglicherweise auch als Speicherort.

0
Nirav Bhatt