Wie funktioniert eine Hash-Tabelle?

494

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

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

Könnte jemand den Prozess klären?

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

Arec Barrwin
quelle
4
Kürzlich habe ich diesen Artikel ( en.algoritmy.net/article/50101/Hash-table ) geschrieben, in dem verschiedene Möglichkeiten zum Speichern und Nachschlagen von Daten beschrieben werden, wobei der Schwerpunkt auf Hash-Tabellen und deren Strategien liegt (separate Verkettung, lineare Prüfung, doppeltes Hashing) )
malejpavouk
1
Sie können sich eine Hash-Tabelle als erweiterte Version eines Arrays vorstellen, die nicht nur auf aufeinanderfolgende Ganzzahlschlüssel beschränkt ist.
user253751
1
Hier ist noch eine: intelligentjava.wordpress.com/2016/10/19/…
Nesvarbu

Antworten:

913

Hier ist eine Erklärung für Laien.

Nehmen wir an, Sie möchten eine Bibliothek mit Büchern füllen und sie nicht nur hineinstecken, sondern sie auch bei Bedarf leicht wiederfinden können.

Sie entscheiden also, dass, wenn die Person, die ein Buch lesen möchte, den Titel des Buches und den genauen Titel zum Booten kennt, dies alles ist, was es braucht. Mit dem Titel sollte die Person mit Hilfe des Bibliothekars in der Lage sein, das Buch leicht und schnell zu finden.

Also, wie kannst du das machen? Natürlich können Sie eine Art 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, aber Sie möchten trotzdem nicht nacheinander 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 wenig Voraussicht, wenn Sie die Bibliothek füllen, und viel Arbeit, wenn Sie die Bibliothek füllen.

Anstatt nur damit zu beginnen, die Bibliothek von einem Ende zum anderen zu füllen, entwickeln 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 Regalnummer und Steckplatznummer zurückerhalten, die Sie ursprünglich erhalten haben wo sich das Buch befindet.

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

Nehmen wir zur Vereinfachung an, dass jeder Buchstabe und jedes Symbol in eine Zahl umgewandelt und alle zusammengefasst werden. In Wirklichkeit ist es viel komplizierter, aber lassen wir es vorerst dabei.

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

Ok, so funktioniert also im Grunde eine Hash-Tabelle.

Technisches folgt.

Erstens gibt es die Größe der Zahl. Normalerweise liegt die Ausgabe eines solchen Hash-Algorithmus in einem Bereich einer großen Anzahl, der normalerweise viel größer ist als der Platz, 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 im Grunde besagt, dass Sie jedes Mal, wenn Sie die gewünschte Zahl (dh die eine Milliarde) gezählt haben, aber in einem viel kleineren Bereich bleiben möchten, jedes Mal, wenn Sie die Grenze dieses kleineren Bereichs erreichen, bei dem Sie begonnen 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 Größe der Bibliothek nur 7 Bücher beträgt, 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 einem Rest. Der Rest der Division von 17 durch 7 ist 3 (7 geht 2 mal in 17 bei 14 und die Differenz zwischen 17 und 14 ist 3).

So legen 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), berechnet er ausnahmslos eine zuvor verwendete Zahl. Wenn Sie im Sinne der Bibliothek das Regal und die Steckplatznummer erreichen, in die Sie ein Buch legen möchten, befindet sich dort bereits ein Buch.

Es gibt verschiedene Methoden zur Behandlung von Kollisionen, darunter das Ausführen der Daten in eine weitere Berechnung, um einen weiteren Punkt in der Tabelle zu erhalten ( doppeltes Hashing ) oder einfach, um einen Platz in der Nähe des Platzes zu finden, den Sie erhalten haben (dh direkt neben dem vorherigen Buch, wobei der Slot angenommen wird war auch als lineare Sonde bekannt ). 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 möglicherweise irgendwann mehr Bücher in die Bibliothek aufnehmen, als die Bibliothek zulässt. Mit anderen Worten, Sie müssen eine größere Bibliothek erstellen. Da der genaue Platz in der Bibliothek anhand der exakten und aktuellen Größe der Bibliothek berechnet wurde, müssen Sie möglicherweise nach der Berechnung der Plätze neue Plätze für alle Bücher finden, wenn Sie die Größe der Bibliothek ändern hat sich verändert.

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

Lasse V. Karlsen
quelle
Vielen Dank für eine so tolle Erklärung. Wissen Sie, wo ich weitere technische Details zur Implementierung in 4.x .Net Framework finden kann?
Johnny_D
Nein, es ist nur eine Zahl. Sie würden einfach jedes Regal und jeden Steckplatz beginnend bei 0 oder 1 nummerieren und für jeden Steckplatz in diesem Regal um 1 erhöhen und dann im nächsten Regal weiter nummerieren.
Lasse V. Karlsen
2
"Es gibt verschiedene Methoden zur Behandlung von Kollisionen, einschließlich der Ausführung der Daten in einer weiteren Berechnung, um einen weiteren Platz in der Tabelle zu erhalten" - was meinen Sie mit einer anderen Berechnung? Es ist nur ein anderer Algorithmus? Angenommen, wir verwenden einen anderen Algorithmus, der basierend auf dem Buchnamen eine andere Zahl ausgibt. Wenn ich später dieses Buch finden würde, wie würde ich dann wissen, welchen Algorithmus ich verwenden soll? Ich würde den ersten Algorithmus, den zweiten Algorithmus usw. verwenden, bis ich das Buch finde, dessen Titel der ist, nach dem ich suche.
user107986
1
@KyleDelaney: Nein für geschlossenes Hashing (bei dem Kollisionen durch Auffinden eines alternativen Buckets behandelt werden, was bedeutet, dass die Speichernutzung behoben ist, Sie jedoch mehr Zeit damit verbringen, über Buckets hinweg zu suchen). Für offenes Hashing, auch bekannt als Verkettung in einem pathologischen Fall (schreckliche Hash-Funktion oder Eingaben, die absichtlich so gestaltet sind, dass sie von einem Gegner / Hacker kollidieren), könnten die meisten Hash-Buckets leer sein, aber die gesamte Speichernutzung ist nicht schlechter - nur mehr Zeiger NULL statt Indizierung in die Daten sinnvoll.
Tony Delroy
3
@KyleDelaney: Benötige das "@Tony" -Ding, um über deine Kommentare informiert zu werden. Anscheinend wundern Sie sich über die Verkettung: Angenommen, wir haben drei Werteknoten A{ptrA, valueA}, B{ptrB, valueB}, C{ptrC, valueC}und eine Hash-Tabelle mit drei Buckets [ptr1, ptr2, ptr3]. Unabhängig davon, ob beim Einfügen Kollisionen auftreten, ist die Speichernutzung festgelegt. Möglicherweise haben Sie keine Kollisionen: A{NULL, valueA} B{NULL, valueB} C{NULL, valueC}und [&A, &B, &C]oder alle Kollisionen A{&B, valueA} B{&C, valueB}, C{NULL, valueC}und [NULL, &A, NULL]: Sind die NULL-Buckets "verschwendet"? Ein bisschen, ein bisschen nicht. Gleicher Gesamtspeicher verwendet.
Tony Delroy
104

Verwendung und Umgangssprache:

  1. Hash-Tabellen werden zum schnellen Speichern und Abrufen von Daten (oder Datensätzen) verwendet.
  2. Datensätze werden mithilfe von Hash-Schlüsseln in Buckets gespeichert
  3. Hash-Schlüssel werden berechnet, indem ein Hashing-Algorithmus auf einen ausgewählten Wert (den Schlüsselwert ) angewendet wird, der im Datensatz enthalten ist. Dieser ausgewählte Wert muss allen Datensätzen gemeinsam sein.
  4. Jeder Bucket 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 (die Aufzeichnungen) für ihre rund 30.000 Kunden aufzubewahren. Jeder Dateiordner wurde eindeutig mit seiner Client-Nummer identifiziert, einer eindeutigen Nummer von 0 bis 29.999.

Die damaligen Archivare mussten schnell Kundendatensätze für das Arbeitspersonal abrufen und speichern. Die Mitarbeiter hatten entschieden, dass es effizienter wäre, eine Hashing-Methode zum Speichern und Abrufen ihrer Aufzeichnungen zu verwenden.

Um einen Client-Datensatz einzureichen, verwenden Angestellte die eindeutige Client-Nummer, die in den Ordner geschrieben ist. Unter Verwendung dieser Client-Nummer modulierten sie den 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 nach Client-Nummer geordnete Ordner enthielt. Nachdem sie den richtigen Ort identifiziert hatten, schoben sie ihn einfach hinein.

Um einen Kundendatensatz abzurufen, erhalten die Archivierungsmitarbeiter eine Kundennummer auf einem Zettel. Unter Verwendung dieser eindeutigen Client-Nummer (des Hash-Schlüssels ) würden sie diese um 300 modulieren, um festzustellen, in welchem ​​Aktenschrank sich der Client-Ordner befand. Wenn sie den Aktenschrank öffneten, stellten sie fest, dass er viele Ordner enthielt, die nach Kundennummer sortiert waren. Beim Durchsuchen der Datensätze würden sie schnell den Client-Ordner finden und ihn abrufen.

In unserer realen Welt Beispiel unsere Eimer sind Aktenschränke und unsere Aufzeichnungen sind Aktenordner .


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

Wie Simon erwähnt hat , ist es meiner Meinung nach sehr wichtig, dass der Hashing-Teil darin besteht, einen großen Raum (beliebiger Länge, normalerweise Zeichenfolgen usw.) zu transformieren und ihn zur Indizierung einem kleinen Raum (bekannter Größe, normalerweise Zahlen) zuzuordnen. Dies ist sehr wichtig, um sich zu erinnern!

Im obigen Beispiel werden die etwa 30.000 möglichen Clients einem kleineren Bereich zugeordnet.


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

Sie haben vielleicht bemerkt, dass einige dies tatsächlich bereits tun. Anstatt eine Hashing-Methode zum Generieren 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!

Jeach
quelle
2
Sie beschreiben eine bestimmte Art von Strategie zur Vermeidung von Kollisionen in Hash-Tabellen, die variabel als "offene Adressierung" oder "geschlossene Adressierung" (ja, traurig, aber wahr) oder "Verkettung" bezeichnet wird. Es gibt einen anderen Typ, der keine Listen-Buckets verwendet, sondern die Elemente "inline" speichert.
Konrad Rudolph
2
ausgezeichnete Beschreibung. mit der Ausnahme, dass jeder Aktenschrank durchschnittlich etwa 100Aufzeichnungen enthält (30.000 Aufzeichnungen / 300 Schränke = 100). Könnte eine Bearbeitung wert sein.
Ryan Tuck
@TonyD, gehen Sie online zu dieser Site sha-1 und generieren Sie einen SHA-1-Hash TonyD, den Sie in das Textfeld eingeben. Sie erhalten einen generierten Wert für etwas, das aussieht e5dc41578f88877b333c8b31634cf77e4911ed8c. Dies ist nichts weiter als eine große hexadezimale Anzahl von 160 Bit (20 Byte). Sie können dies dann verwenden, um zu bestimmen, welcher Eimer (eine begrenzte Menge) zum Speichern Ihrer Aufzeichnung verwendet wird.
Jeach
@ TonyD, ich bin nicht sicher, wo der Begriff "Hash-Schlüssel" in einer widersprüchlichen Angelegenheit bezeichnet wird? Wenn ja, weisen Sie bitte auf zwei oder mehr Standorte hin. Oder sagen Sie, dass "wir" den Begriff "Hash-Schlüssel" verwenden, während andere Websites wie Wikipedia "Hash-Werte, Hash-Codes, Hash-Summen oder einfach Hashes" verwenden? Wenn ja, wen interessiert es, solange der verwendete Begriff innerhalb einer Gruppe oder Organisation konsistent ist. Programmierer verwenden häufig den Begriff "Schlüssel". Ich würde persönlich argumentieren, dass eine andere gute Option "Hash-Wert" wäre. Aber ich würde ausschließen, "Hash-Code, Hash-Summe oder einfach Hashes" zu verwenden. Konzentrieren Sie sich auf den Algorithmus und nicht auf die Wörter!
Jeach
2
@ TonyD, ich habe den Text in "Sie würden den Hash-Schlüssel um 300 modulieren" geändert , in der Hoffnung, dass er für alle sauberer und klarer wird. Vielen Dank!
Jeach
64

Dies stellt sich als ziemlich tiefes Gebiet der Theorie heraus, aber die Grundzüge sind einfach.

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

Wenn Sie nur wenig Platz zum Hashing haben, können Sie diese Dinge möglicherweise nur als Ganzzahlen interpretieren, und schon sind Sie fertig (z. B. 4-Byte-Zeichenfolgen).

Normalerweise haben Sie jedoch einen viel größeren Raum. Wenn der Raum der Dinge, die Sie als Schlüssel zulassen, größer ist als der Raum der Dinge, die Sie zum Indizieren verwenden (Ihre uint32 oder was auch immer), können Sie möglicherweise nicht für jeden einen eindeutigen Wert haben. Wenn zwei oder mehr Dinge zum gleichen Ergebnis führen, müssen Sie die Redundanz in angemessener Weise behandeln (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 bedeutet, dass Sie wahrscheinlich nicht das gleiche Ergebnis erzielen möchten, 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 verwenden.

Damit dies als Hashtabelle funktioniert: Stellen Sie sich vor, Sie hätten sich nicht um die Speichernutzung gekümmert. Anschließend können Sie ein Array erstellen, solange Ihr Indexierungssatz festgelegt ist (z. B. alle uint32). Wenn Sie der Tabelle etwas hinzufügen, hashen Sie den Schlüssel und sehen sich das Array an diesem Index an. Wenn dort nichts ist, setzen Sie Ihren Wert dort. Wenn dort bereits etwas vorhanden ist, fügen Sie diesen neuen Eintrag zu einer Liste von Dingen an dieser Adresse hinzu, zusammen mit genügend Informationen (Ihrem ursprünglichen Schlüssel oder etwas Klugem), um herauszufinden, welcher Eintrag tatsächlich zu welchem ​​Schlüssel gehört.

Wenn Sie also lange unterwegs sind, ist jeder Eintrag in Ihrer Hashtabelle (das Array) entweder leer oder enthält einen Eintrag oder eine Liste von Einträgen. Das Abrufen ist einfach, indem Sie in das Array indizieren und entweder den Wert zurückgeben oder die Werteliste durchgehen und den richtigen zurückgeben.

In der Praxis ist dies natürlich normalerweise nicht möglich, da zu viel Speicher verschwendet wird. Sie machen also alles basierend auf einem spärlichen Array (wobei die einzigen Einträge diejenigen sind, die Sie tatsächlich verwenden, alles andere ist implizit null).

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

Simon
quelle
1
Entschuldigung, ich weiß, dass dies eine alte Frage / Antwort ist, aber ich habe versucht, diesen letzten Punkt zu verstehen, den Sie ansprechen. Eine Hash-Tabelle hat eine zeitliche Komplexität von O (1). Wenn Sie jedoch ein spärliches Array verwenden, müssen Sie dann keine binäre Suche mehr durchführen, um Ihren Wert zu finden? Zu diesem Zeitpunkt wird die zeitliche Komplexität nicht zu O (log n)?
Herbrandson
@herbrandson: nein ... ein spärliches Array bedeutet einfach, dass relativ wenige Indizes mit Werten gefüllt wurden. Sie können weiterhin direkt auf das spezifische Array-Element für den Hash-Wert indizieren, den Sie aus Ihrem Schlüssel berechnet haben. Dennoch ist die Implementierung eines spärlichen Arrays, die Simon beschreibt, nur unter sehr begrenzten Umständen vernünftig : Wenn die Bucket-Größen in der Größenordnung der Speicherseitengrößen liegen (im Vergleich zu etwa intSchlüsseln bei 1: 1000-Spärlichkeit und 4 KB-Seiten = die meisten berührten Seiten) und wann Das Betriebssystem behandelt alle 0-Seiten effizient (so dass alle nicht verwendeten Bucket-Seiten keinen Sicherungsspeicher benötigen), wenn genügend Adressraum vorhanden ist ....
Tony Delroy
@TonyDelroy - das stimmt, es ist zu einfach, aber die Idee war, einen Überblick darüber zu geben, was sie sind und warum, keine praktische Implementierung. Die Details des letzteren sind nuancierter, wenn Sie in Ihrer Erweiterung darauf nicken.
Simon
48

Viele Antworten, aber keine davon ist sehr visuell , und Hash-Tabellen können bei der Visualisierung leicht "klicken".

Hash-Tabellen werden häufig als Arrays verknüpfter Listen implementiert. Wenn wir uns eine Tabelle vorstellen, in der die Namen von Personen gespeichert sind, wird sie nach einigen Einfügungen möglicherweise wie ()folgt im Speicher angeordnet, 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 bezeichnet und startet eine - möglicherweise leere - verknüpfte Liste von Werten ( in diesem Beispiel auch als Elemente bezeichnet - Namen von Personen ).
  • Jeder Wert (z. B. "fred"mit Hash 42) wird aus dem Bucket verknüpft, [hash % number_of_buckets]z 42 % 10 == [2]. %ist der Modulo-Operator - der Rest geteilt durch die Anzahl der Eimer
  • Mehrere Datenwerte können mit demselben Bucket kollidieren und mit 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 Hash-Werte gleich sind (z. B. "fred"und "jane"beide 42oben mit Hash dargestellt).
    • Die meisten Hash-Tabellen behandeln Kollisionen - mit leicht reduzierter Leistung, aber ohne funktionale Verwirrung -, indem sie den vollständigen Wert (hier Text) eines gesuchten oder eingefügten Werts mit jedem Wert vergleichen, der bereits in der verknüpften Liste im Hash-to-Bucket enthalten ist

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

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

Hans gibt die tatsächliche Formel für andere Lastfaktoren in einem Kommentar unten an, jedoch für Richtwerte: Mit Lastfaktor 1 und einer Hash-Funktion für kryptografische Stärke ist 1 / e (~ 36,8%) der Eimer tendenziell leer, ein weiteres 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 100 Elemente und 100 Eimer vorhanden sind oder 100 Millionen Elemente und 100 Millionen Buckets), weshalb wir sagen, dass Nachschlagen / Einfügen / Löschen O (1) Operationen mit konstanter Zeit sind.

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

Bei einer Implementierung der Hash-Tabelle, wie oben beschrieben, können wir uns vorstellen, einen Werttyp wie struct Value { string name; int age; };und Gleichheitsvergleichs- und Hash-Funktionen zu erstellen , die nur das nameFeld betrachten (Alter ignorieren), und dann passiert etwas Wunderbares: Wir können ValueDatensätze wie {"sue", 63}in der Tabelle speichern , 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 daher nicht erfordert, dass wir Sues Datensatz in einen anderen verschieben Eimer.

Wenn wir dies tun, verwenden wir die Hash-Tabelle als assoziativen Container, auch bekannt als Map , und die darin gespeicherten Werte bestehen aus einem Schlüssel (dem Namen) und einem oder mehreren anderen Feldern, die - verwirrenderweise - immer noch als Wert ( in meinem Beispiel nur das Alter). Eine als Map verwendete Hash-Tabellenimplementierung 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 als eigenen Schlüssel betrachten können: Diese Art der Verwendung wird als Hash-Set bezeichnet .

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

Nicht alle Hash-Tabellen verwenden verknüpfte Listen (als separate Verkettung bezeichnet ), aber die meisten allgemeinen Listen verwenden Listen , da die Hauptalternative für geschlossenes Hashing (auch als offene Adressierung bezeichnet ) - insbesondere bei unterstützten Löschvorgängen - weniger stabile Leistungseigenschaften mit kollisionsanfälligen Schlüsseln aufweist. Hash-Funktionen.


Ein paar Worte zu Hash-Funktionen

Starkes Hashing ...

Ein allgemeiner Zweck der Aufgabe der Kollisionsminimierung im schlimmsten Fall besteht darin, die Schlüssel effektiv zufällig um die Hash-Tabellen-Buckets 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 idealerweise - zufällig - etwa die Hälfte der Bits im resultierenden Hashwert umdrehen.

Dies wird normalerweise mit Mathe orchestriert, die zu kompliziert ist, als dass ich sie hätte üben können. Ich werde einen leicht verständlichen Weg erwähnen - nicht den skalierbarsten oder cachefreundlichsten, aber von Natur aus elegantesten (wie die Verschlüsselung mit einem einmaligen Pad!) -, da er meiner Meinung nach dazu beiträgt, die oben genannten wünschenswerten Eigenschaften nach Hause zu bringen. Angenommen, Sie haben 64-Bit- doubleS gehasht - Sie könnten 8 Tabellen mit jeweils 256 Zufallszahlen (Code unten) erstellen und dann mit jedem 8-Bit / 1-Byte-Slice der doubleSpeicherdarstellung des Index in eine andere Tabelle indizieren Zufallszahlen, die Sie nachschlagen. Bei diesem Ansatz ist leicht zu erkennen, dass ein Bit (im Sinne einer binären Ziffer) sich irgendwo in den doubleErgebnissen ändert, was dazu führt, dass eine andere Zufallszahl in einer der Tabellen nachgeschlagen wird und ein völlig unkorrelierter Endwert entsteht.

// 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 durchlaufen Ganzzahlen unverändert (als Trivial- oder Identitäts- Hash-Funktion bezeichnet). Es ist das andere Extrem von dem oben beschriebenen starken Hashing. Ein Identitäts-Hash ist extremKollision anfällig im schlimmsten Fall, aber die Hoffnung ist, dass im ziemlich häufigen Fall von Ganzzahlschlüsseln, die dazu neigen, sich zu erhöhen (möglicherweise mit einigen Lücken), sie in aufeinanderfolgende Eimer abgebildet werden, wobei weniger leer bleiben als zufällige Hashing-Blätter (unsere ~ 36,8) % bei Lastfaktor 1 (siehe oben), wodurch weniger Kollisionen und weniger längere verknüpfte Listen kollidierender Elemente auftreten, als dies durch zufällige Abbildungen erreicht wird. Es ist auch großartig, die Zeit zu sparen, die zum Generieren eines starken Hashs erforderlich ist. Wenn Schlüssel nachgeschlagen werden, werden sie in Eimern in der Nähe des Speichers gefunden, wodurch die Cache-Treffer verbessert werden. Wenn die Schlüssel nicht gut inkrementiert werden, besteht 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 bestimmen.

Tony Delroy
quelle
6
Lassen Sie mich nur sagen: fantastische Antwort.
CRThaze
@ Tony Delroy Danke für die tolle Antwort. Ich habe jedoch noch einen offenen Punkt im Kopf. Sie sagen, selbst wenn es 100 Millionen Buckets gibt, wäre die Suchzeit O (1) mit Lastfaktor 1 und einer Hash-Funktion für kryptografische Stärke. Aber was ist mit dem richtigen Eimer in 100 Millionen? Selbst wenn wir alle Eimer sortiert haben, ist es nicht O (log100.000.000)? Wie kann das Finden des Eimers O (1) sein?
Selman
@selman: Ihre Frage enthält nicht viele Details, um zu erklären, warum Sie glauben, dass es O sein könnte (log100.000.000), aber Sie sagen "auch wenn wir alle Buckets sortiert haben" - denken Sie daran, dass die Werte in Hash-Tabellen-Buckets sind werden niemals im üblichen Sinne "sortiert": Welcher Wert in welchem ​​Bucket erscheint, wird durch Anwenden der Hash-Funktion auf den Schlüssel bestimmt. Wenn Sie glauben, die Komplexität sei O (log 100.000.000), können Sie sich vorstellen, eine binäre Suche in sortierten Buckets durchzuführen, aber so funktioniert Hashing nicht. Vielleicht lesen Sie ein paar der anderen Antworten und sehen Sie, ob es sinnvoller wird.
Tony Delroy
@ TonyDelroy In der Tat sind "sortierte Eimer" das beste Szenario, das ich mir vorstelle. Daher O (log 100.000.000). Aber wenn dies nicht der Fall ist, wie kann die Anwendung einen verwandten Bereich unter Millionen finden? Erzeugt die Hash-Funktion irgendwie einen Speicherort?
Selman
1
@selman: da der Computerspeicher eine konstante Zeit "Direktzugriff" ermöglicht: Wenn Sie eine Speicheradresse berechnen können, können Sie den Speicherinhalt abrufen, ohne auf den Speicher in anderen Teilen des Arrays zugreifen zu müssen. Unabhängig davon, ob Sie auf den ersten Bucket, den letzten Bucket oder einen dazwischen liegenden Bucket zugreifen, weist dieser die gleichen Leistungsmerkmale auf (nehmen Sie locker die gleiche Zeit in Anspruch, obwohl dies Auswirkungen auf das Caching des CPU L1 / L2 / L3-Speichers hat, aber Sie dienen nur dazu, Ihnen den schnellen Zugriff auf kürzlich aufgerufene oder zufällig in der Nähe befindliche Buckets zu erleichtern und können für Big-O-Analysen ignoriert werden.
Tony Delroy
24

Ihr seid sehr nahe daran, dies vollständig zu erklären, aber es fehlen ein paar Dinge. Die Hashtabelle ist nur ein Array. Das Array selbst enthält in jedem Steckplatz etwas. In diesem Slot speichern Sie mindestens den Hashwert oder den Wert selbst. Darüber hinaus können Sie auch eine verknüpfte / verkettete Liste von Werten speichern, die in diesem Steckplatz kollidiert sind, oder Sie können die offene Adressierungsmethode verwenden. Sie können auch einen Zeiger oder 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 dem der Wert platziert 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 Hashtabelle selbst durchgeführt werden, um herauszufinden, in welchen Slot der Wert gehen soll. Dies geschieht mit einer Modul-Mathematik-Operation wie:

uint slotIndex = hashValue % hashTableSize;

Dieser Wert ist der Steckplatz, in den der Wert eingegeben 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, es gibt andere fortgeschrittenere Methoden zur Bestimmung des Slot-Index, aber dies ist die übliche, die ich gesehen habe ... würde mich für andere interessieren, die eine bessere Leistung erbringen.

Wenn Sie bei der Modulmethode eine Tabelle mit der Größe 1000 haben, wird jeder Hashwert zwischen 1 und 1000 in den entsprechenden Steckplatz verschoben. Alle negativen Werte und alle Werte größer als 1000 kollidieren möglicherweise mit den Slot-Werten. Die Wahrscheinlichkeit, dass dies geschieht, hängt sowohl von Ihrer Hashing-Methode als auch von der Anzahl der Elemente ab, die Sie insgesamt zur Hash-Tabelle hinzufügen. Im Allgemeinen empfiehlt es sich, die Größe der Hashtabelle so zu gestalten, dass die Gesamtzahl der hinzugefügten Werte nur etwa 70% ihrer Größe entspricht. Wenn Ihre Hash-Funktion eine gleichmäßige Verteilung leistet, treten im Allgemeinen nur sehr wenige bis gar 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, geben Sie mit allen Mitteln eine gute Schätzung ab.

Ich hoffe das hat geholfen.

PS - In C # ist die GetHashCode()Methode ziemlich langsam und führt unter vielen von mir getesteten Bedingungen zu Kollisionen mit dem tatsächlichen Wert. Erstellen Sie für echten Spaß Ihre eigene Hash-Funktion und versuchen Sie, sie NIEMALS auf die spezifischen Daten zu kollidieren, die Sie haschen, schneller als GetHashCode ausführen und eine ziemlich gleichmäßige Verteilung haben. Ich habe dies mit langen statt int-Hashcode-Werten gemacht und es hat ziemlich gut mit bis zu 32 Millionen vollständigen Hashwerten in der Hashtabelle mit 0 Kollisionen funktioniert. Leider kann ich den Code nicht teilen, da er meinem Arbeitgeber gehört ... aber ich kann feststellen, dass er für bestimmte Datendomänen möglich ist. Wenn Sie dies erreichen können, ist die Hashtabelle SEHR schnell. :) :)

Chris
quelle
Ich weiß, dass der Beitrag ziemlich alt ist, aber kann jemand erklären, was (Rest + 1) hier bedeutet
Hari
3
@Hari remainderbezieht sich auf das Ergebnis der ursprünglichen Modulo-Berechnung, und wir addieren 1 dazu, um den nächsten verfügbaren Slot zu finden.
x4nd3r
"Das Array selbst enthält in jedem Slot etwas. Zumindest speichern Sie den Hashwert oder den Wert selbst in diesem Slot." - Es ist üblich, dass "Slots" (Eimer) überhaupt keinen Wert speichern. Bei Implementierungen mit offener Adressierung wird häufig entweder NULL oder ein Zeiger auf den ersten Knoten in einer verknüpften Liste gespeichert - ohne Wert direkt im Steckplatz / Bucket. "würde sich für andere interessieren" - die von Ihnen dargestellte "+1" wird als lineare Prüfung bezeichnet , die häufig eine bessere Leistung erbringt: quadratische Prüfung . "Im Allgemeinen treten nur sehr wenige bis gar keine Bucket / Slot-Kollisionen auf" - bei 70% Kapazität, ~ 12% Slots mit 2 Werten, ~ 3% 3 ....
Tony Delroy
"Ich habe dies mit langen Hashcode-Werten anstelle von Int-Size-Hashcode-Werten gemacht und es hat bei bis zu 32 Millionen Hash-Werten in der Hashtabelle mit 0 Kollisionen recht gut funktioniert." - Dies ist im allgemeinen Fall einfach nicht möglich, wenn die Werte von Schlüsseln in einem viel größeren Bereich als der Anzahl der Buckets effektiv zufällig sind. Beachten Sie, dass es oft einfach genug ist, unterschiedliche Hash-Werte zu haben (und Ihre Rede von longHash-Werten impliziert, dass Sie dies erreicht haben), aber sicherzustellen, dass sie nach der Mod /% -Operation nicht in der Hash-Tabelle kollidieren (im allgemeinen Fall) ).
Tony Delroy
(Das Vermeiden aller Kollisionen wird als perfektes Hashing bezeichnet . Im Allgemeinen ist es praktisch, wenn einige hundert oder tausend Schlüssel im Voraus bekannt sind. Gperf ist ein Beispiel für ein Tool zur Berechnung einer solchen Hash-Funktion. Sie können auch Ihre eigenen in sehr begrenztem Umfang schreiben Umstände - z. B. wenn Ihre Schlüssel Zeiger auf Objekte aus Ihrem eigenen Speicherpool sind, der ziemlich voll gehalten wird, wobei jeder Zeiger einen festen Abstand voneinander hat, können Sie die Zeiger durch diesen Abstand teilen und effektiv einen Index in ein leicht spärliches Array einfügen, um dies zu vermeiden Kollisionen.)
Tony Delroy
17

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 jedes Element, dessen Hash-Code mit einem bestimmten Buchstaben beginnt, in den entsprechenden Bucket ein.

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

Was die Berechnung des Hash-Codes angeht, ist nichts Magisches daran. Das Ziel ist nur, dass verschiedene Objekte unterschiedliche Codes zurückgeben und 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.

AndreiM
quelle
13

Kurz und bündig:

Eine Hash-Tabelle schließt ein Array ab 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 durch Erstellen internalArrayeines Arrays verknüpfter Listen zu codieren ist :

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

Wenn ich also ein Element aus meiner Hash-Tabelle abrufen 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 etwa 85%, können wir die Größe des internen Arrays ändern und alle Elemente aus dem alten Array in das neue Array verschieben.

Julia
quelle
11

Es ist noch einfacher als das.

Eine Hashtabelle ist nichts anderes als ein Array (normalerweise ein spärliches Array ) von Vektoren, die Schlüssel / Wert-Paare enthalten. Die maximale Größe dieses Arrays ist normalerweise kleiner als die Anzahl der Elemente im Satz möglicher Werte für den Datentyp, der in der Hashtabelle gespeichert wird.

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

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

Aus diesem Grund können Sie mehrere Schlüssel haben, die denselben Hashcode 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, der nachgeschlagen wird. Wenn es gefunden wird, ist großartig und der dem Schlüssel zugeordnete Wert wird zurückgegeben, andernfalls wird nichts zurückgegeben.

casperOne
quelle
10

Sie nehmen eine Menge Dinge und ein Array.

Für jede Sache erstellen Sie einen Index, der als Hash bezeichnet wird. Das Wichtige am Hash ist, dass er viel "zerstreut"; Sie möchten nicht, dass zwei ähnliche Dinge ähnliche Hashes haben.

Sie legen Ihre Sachen an der durch den Hash angegebenen Position in das Array. Bei einem bestimmten Hash kann mehr als eine Sache auftauchen. Sie speichern die Dinge also in Arrays oder in einem anderen geeigneten Element, das wir im Allgemeinen als Bucket bezeichnen.

Wenn Sie im Hash nachschlagen, gehen Sie dieselben 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 Hashing 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.

Stellen Sie für Bonuspunkte sicher, dass beim Zugriff auf Ihre Hash-Tabelle das gefundene Objekt (falls vorhanden) an den Anfang des Buckets verschoben wird, damit es beim nächsten Mal als erstes überprüft wird.

Chaos
quelle
1
Vielen Dank für den letzten Punkt, den alle anderen nicht erwähnt haben
Sandeep Raju Prabhakar
4

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

Wie Simon erklärte, wird die Hash-Funktion verwendet, um von einem 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 Ganzzahl zuordnen, sodass "Alligator" einen Hash-Code von 0 hat, "Biene" einen Hash-Code von 1 hat. " Zebra "wäre 25 usw.

Als nächstes haben wir ein Array von 26 Buckets (könnten ArrayLists in Java sein) und wir legen das Element in den Bucket, das 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, sodass alle für diesen Hash-Code in den Bucket gehen würden, sodass im Bucket eine lineare Suche durchgeführt werden müsste finde einen bestimmten Gegenstand.

In unserem Beispiel würde es sehr gut funktionieren, wenn wir nur ein paar Dutzend Elemente mit Schlüsseln hätten, die das Alphabet überspannen. 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.

Greg Graham
quelle
3

Hier ist eine andere Sichtweise.

Ich gehe davon aus, dass Sie das Konzept eines Arrays A verstehen. Dies unterstützt die Indizierung, 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 speichern möchten, die alle unterschiedlich alt sind, besteht eine einfache Möglichkeit darin, ein Array zu haben, 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 demselben Alter geben. Was Sie also bei jedem Eintrag in das Array einfügen, ist eine Liste aller Personen, die dieses Alter haben. So können Sie in einem Schritt zu den Informationen einer einzelnen Person gelangen und ein wenig in dieser Liste suchen (als "Eimer" bezeichnet). Es wird nur langsamer, wenn so viele Leute da sind, dass die Eimer groß werden. Dann benötigen Sie ein größeres Array und eine andere Möglichkeit, um mehr identifizierende Informationen über die Person zu erhalten, z. B. die ersten Buchstaben ihres Nachnamens, anstatt das Alter zu verwenden.

Das ist die Grundidee. Anstelle des Alters 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 verschlüsseln könnten. Alles, was zählt, ist, dass Sie nicht möchten, dass zu viele Leute zum selben Eimer hashen, da die Geschwindigkeit davon abhängt, dass die Eimer klein bleiben.

Mike Dunlavey
quelle
2

Wie der Hash berechnet wird, hängt normalerweise nicht von der Hashtabelle 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.

Lucero
quelle
2

Eine Hash-Tabelle arbeitet vollständig mit der Tatsache, dass die praktische Berechnung dem Maschinenmodell mit wahlfreiem Zugriff folgt, dh auf den Wert an jeder 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 Um sie einer endlichen Anzahl von Größen zuzuordnen, kann ich Speicher in meinem System zuweisen. Theoretisch ist meine Hash-Tabelle fertig.

Im Allgemeinen ist in Anwendungen das Universum der Schlüssel 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 verschwenden, z. B. 10000 oder 100000 Ganzzahlwerte, da diese 32 sind etwas lang in binärer Darstellung). Also verwenden wir dieses Hashing. Es ist eine Art mischende "mathematische" Operation, die mein großes Universum einem kleinen Satz von Werten zuordnet, die ich im Speicher aufnehmen kann. In praktischen Fällen hat der Speicherplatz einer Hash-Tabelle häufig die gleiche "Reihenfolge" (big-O) wie die (Anzahl der Elemente * Größe jedes Elements). Wir verschwenden also nicht viel Speicher.

Nun, eine große Menge, die einer kleinen Menge zugeordnet ist, muss die Zuordnung viele zu eins sein. Also, verschiedenen Schlüsseln wird der gleiche Platz zugewiesen (?? nicht fair). Es gibt einige Möglichkeiten, damit umzugehen. Ich kenne nur die beiden beliebtesten:

  • Verwenden Sie den Platz, der dem Wert zugewiesen werden sollte, als Referenz auf eine verknüpfte Liste. Diese verknüpfte Liste speichert einen oder mehrere Werte, die sich in vielen zu einer Zuordnung im selben Slot befinden. Die verknüpfte Liste enthält auch Schlüssel, die jemandem bei der Suche helfen. Es ist wie bei vielen Menschen in derselben Wohnung, wenn ein Lieferbote kommt, geht er ins Zimmer und fragt speziell nach dem Mann.
  • Verwenden Sie eine doppelte Hash-Funktion in einem Array, die jedes Mal dieselbe Wertesequenz anstelle eines einzelnen Werts angibt. Wenn ich einen Wert speichere, sehe ich, ob der erforderliche Speicherplatz frei oder belegt ist. Wenn es kostenlos 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 Speicherort finde und meinen Wert dort speichere. Wenn ich den Wert suche oder 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, ob er dort ist, bis ich ihn finde, oder suche alle möglichen Orte im Array.

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

div
quelle
0

Für alle, die Programmiersprache suchen, ist hier, wie es funktioniert. Die interne Implementierung erweiterter Hashtabellen weist viele Feinheiten und Optimierungen für die 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;
}

Wo calculate_bucket_from_val()ist die Hashing-Funktion, wo all die Magie der Einzigartigkeit geschehen 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.

Nirav Bhatt
quelle
1
"Als Faustregel gilt: Damit ein bestimmter Wert eingefügt werden kann, muss der Bucket EINZIGARTIG UND VON DEM WERT ABLEITBAR sein, den er SPEICHERN soll." - Dies beschreibt eine perfekte Hash-Funktion , die normalerweise nur für einige hundert oder tausend Werte möglich ist, die zur Kompilierungszeit bekannt sind. Die meisten Hash-Tabellen müssen mit Kollisionen umgehen . Außerdem weisen Hash-Tabellen in der Regel Speicherplatz für alle Buckets zu, unabhängig davon, ob sie leer sind oder nicht, während Ihr Pseudocode einen create_extra_space_for_bucket()Schritt beim Einfügen neuer Schlüssel dokumentiert . Eimer können jedoch Zeiger sein.
Tony Delroy