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.
data-structures
hash
hashtable
modulo
Arec Barrwin
quelle
quelle
Antworten:
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 :)
quelle
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 KollisionenA{&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.Verwendung und Umgangssprache:
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!
quelle
100
Aufzeichnungen enthält (30.000 Aufzeichnungen / 300 Schränke = 100). Könnte eine Bearbeitung wert sein.TonyD
, den Sie in das Textfeld eingeben. Sie erhalten einen generierten Wert für etwas, das aussiehte5dc41578f88877b333c8b31634cf77e4911ed8c
. 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.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.
quelle
int
Schlü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 ....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.Ein paar Punkte:
[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 )."fred"
mit Hash42
) wird aus dem Bucket verknüpft,[hash % number_of_buckets]
z42 % 10 == [2]
.%
ist der Modulo-Operator - der Rest geteilt durch die Anzahl der Eimer42 % 10 == [2]
und9282 % 10 == [2]
), aber gelegentlich, weil die Hash-Werte gleich sind (z. B."fred"
und"jane"
beide42
oben mit Hash dargestellt).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 dasname
Feld betrachten (Alter ignorieren), und dann passiert etwas Wunderbares: Wir könnenValue
Datensä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-
double
S gehasht - Sie könnten 8 Tabellen mit jeweils 256 Zufallszahlen (Code unten) erstellen und dann mit jedem 8-Bit / 1-Byte-Slice derdouble
Speicherdarstellung 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 dendouble
Ergebnissen ändert, was dazu führt, dass eine andere Zufallszahl in einer der Tabellen nachgeschlagen wird und ein völlig unkorrelierter Endwert entsteht.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.
quelle
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:
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:
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. :) :)quelle
remainder
bezieht sich auf das Ergebnis der ursprünglichen Modulo-Berechnung, und wir addieren 1 dazu, um den nächsten verfügbaren Slot zu finden.long
Hash-Werten impliziert, dass Sie dies erreicht haben), aber sicherzustellen, dass sie nach der Mod /% -Operation nicht in der Hash-Tabelle kollidieren (im allgemeinen Fall) ).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.
quelle
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: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
internalArray
eines Arrays verknüpfter Listen zu codieren ist :Wenn ich also ein Element aus meiner Hash-Tabelle abrufen wollte, könnte ich schreiben:
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.
quelle
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.
quelle
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.
quelle
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.
quelle
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.
quelle
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.
quelle
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:
Die Einführung in Algorithmen durch CLRS bietet einen sehr guten Einblick in das Thema.
quelle
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.
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.
quelle
create_extra_space_for_bucket()
Schritt beim Einfügen neuer Schlüssel dokumentiert . Eimer können jedoch Zeiger sein.