Wie werden die Werte der Hash-Tabelle physisch im Speicher gespeichert?

7

Frage:

Wie werden die Werte der Hash-Tabelle so gespeichert, dass der Speicherplatz bei effizienter Nutzung nicht häufig verschoben werden muss?

Mein derzeitiges Verständnis (könnte falsch sein):

Angenommen, ich habe 3 Objekte in einer Hash-Tabelle gespeichert. Ihre Hash-Funktionen erzeugen folgende Werte:

  • 0
  • 10
  • 20

Ich würde annehmen, dass die Zeiger dieser Objekte nicht an den folgenden Speicheradressen gespeichert werden, da zwischen ihnen große Lücken bestehen würden:

  • startOfHashTable + 0
  • startOfHashTable + 10
  • startOfHashTable + 20

Der Wikipedia-Artikel über Hash-Tabellen besagt, dass der "Index" als solcher berechnet wird:

hash = hashfunc(key)
index = hash % array_size 

In meinem Beispiel wären die Indizes also:

  • 0% 3 = 0
  • 10% 3 = 1
  • 20% 3 = 2

Dadurch werden die großen Lücken beseitigt, die ich zuvor erwähnt habe. Selbst mit diesem Modulo-Schema treten Probleme auf, wenn Sie der Hash-Tabelle weitere Objekte hinzufügen. Wenn ich der Hash-Tabelle ein viertes Objekt hinzufüge, müsste ich% 4 anwenden, um den Index zu erhalten. Würde das nicht alle% 3 ungültig machen, die ich in der Vergangenheit gemacht habe? Müssten alle vorherigen% 3 an die% 4-Standorte verschoben werden?

Pwner
quelle

Antworten:

15

Die Einträge einer Hash-Tabelle werden in einem Array gespeichert. Sie haben jedoch die Anwendung des Modulo-Operators auf die Hash-Werte falsch verstanden. Wenn die Hash-Tabelle in einem Array der Größe gespeichert ist  , wird die Hash-Funktion modulo berechnet  , unabhängig davon, wie viele Elemente derzeit in der Tabelle gespeichert sind. Wenn Sie in Ihrem Beispiel die Elemente in einem Array der Größe 6 speichern, werden die drei Elemente mit den Hashwerten 0, 10 und 20 an den Positionen 0, 4 bzw. 2 gespeichert. Wenn Sie ein viertes Element mit einem Hashwert hinzufügen, z. B. 31, wird dieses an Position 1 gespeichert, ohne dass eines der ersten drei Elemente verschoben werden muss. Wenn Ihre Hash-Tabelle voll wurde und Sie sie in ein größeres Array verschieben wollten, dannnn Sie müssten die Positionen aller Elemente in der Tabelle neu berechnen und entsprechend verschieben.

David Richerby
quelle
1
Sie sagen also, dass Hash-Tabellen mit einer geschätzten potenziellen Größe erstellt werden und die Elemente nur verschoben werden, wenn Sie die Größe erhöhen müssen ... Es spielt also keine Rolle, ob eine Hash-Funktion eine gleichmäßige Verteilung aufweist. Zum Beispiel sind Hash-Werte von 0, 5 und 10 gleichmäßig verteilt, aber wenn sie in eine Hash-Tabelle der potenziellen Größe 5 eingefügt werden, kollidieren sie alle in Bucket 0. Es wäre besser zu sagen hash % table size, dass der Hash gleichmäßig verteilt sein sollte, nicht der Hash selbst.
Pwner
@Pwner Das alles ist richtig, ja.
David Richerby
1
Wie ist es möglich, eine gleichmäßig verteilte zu erstellen, hash % tableSizewenn sich tableSize ändern kann? Die Hash-Werte von 0, 5 und 10 erzeugen viele Kollisionen, wenn die Tabellengröße 5 ist, haben aber keine Kollisionen, wenn die Tabellengröße 20 ist.
Pwner
1
@Pwner Beachten Sie, dass Hashtabellen nur dann Operationen mit konstanter Zeit erwartet haben , wenn dies der Fall ist . Aber nur, wenn die Hash-Funktion (ungefähr) einheitlich ist.
Raphael
1
@Pwner Die Verteilung ist nicht buchstäblich einheitlich - aber Sie würden eine nahezu einheitliche Verteilung anstreben.
David Richerby
7

Hash-Tabellen verschwenden normalerweise Platz. Viele Algorithmen tun dies, da Zeit-Raum-Kompromisse üblich sind, aber sie verbergen sie normalerweise besser :) . Wie andere Algorithmen tun dies Hash-Tabellen, um eine bessere Zeitleistung zu erzielen.

Der erste Punkt ist, dass Sie versuchen, Kollisionen in Ihrer Hash-Tabelle zu vermeiden, da dies die Kosten für die Zugriffszeit konstant hält (Kollisionen sind jedoch normalerweise zulässig und können behandelt werden, sodass sich mehrere Elemente zu Zeitkosten im selben Eintrag befinden können ). Der zweite Punkt ist, dass Sie versuchen, große ungenutzte Lücken zu vermeiden, da dies Speicher kostet. Der dritte Punkt ist, dass Sie vermeiden, Ihre Hashing-Funktion (daher auch die Tabellengröße) zu ändern, da die gesamte Tabelle neu organisiert werden muss, was einen hohen Zeitaufwand bedeutet.

Je weniger Lücken Sie haben, desto wahrscheinlicher ist es, dass ein neuer Hash-Eintrag eine Kollision verursacht. Eine gute Hash-Funktion für einen bestimmten Datensatz begrenzt die Wahrscheinlichkeit einer Kollision, selbst wenn der verfügbare Indexraum besser genutzt wird.

Eigentlich sollten Sie berücksichtigen, dass es zwei Arten von Hash-Tabellen gibt: statische und dynamische.

Bei statischen Daten ändern sich die zu hashenden Daten nicht. Sie können daher versuchen, eine Hash-Funktion ohne Kollision für diesen Datensatz zu finden. Das nennt man einen perfekten Hash . Das Beste ist jedoch ein minimaler perfekter Hash , der das Ergebnis ohne Lücken erzielt.

Dies ist jedoch nicht möglich, wenn sich die zu hashenden Daten innerhalb einer Vielzahl von Möglichkeiten dynamisch ändern. Dann können Sie Kollisionen nicht vermeiden, aber Sie versuchen, sie zu begrenzen, indem Sie genügend Lücken haben.

Es gibt eine Vielzahl von Techniken, um dies unterschiedlich zu verwalten: Anpassen der Tabellengröße an die Anzahl der gehashten Werte, Vergrößern der Tabelle bei vielen Kollisionen oder Verringern bei zu großen Lücken. Dies muss jedoch sehr sorgfältig behandelt werden, indem exponentielle Tabellenvariationen verwendet werden, um die Auswirkungen der Tabellenreorganisation auf die Gesamtkosten für die Verwendung der Hash-Tabelle zu begrenzen.

Dies ist als intuitive Einführung gedacht. Weitere technische Details und Referenzen finden Sie in den Antworten auf diese Frage: (Wann) ist die Hash-Tabellensuche O (1)? . Hash-Tabellen und Hashing sind ein wichtiges Thema mit vielen Variationen.

babou
quelle
3

Eine gute Möglichkeit, Hash-Tabellen anzuzeigen, ist wie eine Nachschlagetabelle mit einem unendlichen Indexbereich (nicht wirklich unendlich, Sie sind immer noch durch die Wertbegrenzung des von Ihnen verwendeten Schlüssels eingeschränkt).

Nehmen wir an, Sie versuchen, bestimmte Werte von sqrt (x) in einer Nachschlagetabelle zu speichern, in der X eine Ganzzahl ist. Das würde ungefähr so ​​aussehen:

[1] = 1
[3] = 1.732
[10000] = 100

Dies führt zu einer sehr günstigen Quadratwurzelung, da Sie anstelle der teuren Berechnung einfach den Wert aus dem Array abrufen können. Es ist jedoch eine sehr ineffiziente Speichernutzung, da [2] und [4 - 9999] leer sind.

Zur Rettung kommt die Hash-Funktion. Der Zweck einer Hash-Funktion in diesem Zusammenhang besteht darin, den Index in etwas umzuwandeln, das tatsächlich in ein Array mit angemessener Größe passt. So könnte es beispielsweise Folgendes tun:

(1) = [5] = 1
(3) = [2] = 1.732
(10000) = [3] = 100

Jetzt passen alle 3 Werte in ein Array mit der Größe 6.

Wie erreicht die Hash-Funktion dies? Die grundlegendste Hash-Funktion ist (Index% ArraySize). Der Modulo-Operator dividiert den von Ihnen ausgewählten Index durch die Größe des Arrays und gibt Ihnen den Rest, der immer kleiner als die Array-Größe ist.

Was aber, wenn mehrere Indizes zum gleichen Ergebnis führen? Dies wird als Hash-Kollision bezeichnet und es gibt verschiedene Möglichkeiten, damit umzugehen. Am einfachsten ist es, jeden Wert zusammen mit seinem ursprünglichen Index im Array zu speichern. Wenn dieser Array-Slot belegt ist, gehen Sie um 1 vorwärts, bis ein leerer Slot gefunden wird. Gehen Sie beim Abrufen des Werts zu der durch die Hash-Funktion angegebenen Position und durchlaufen Sie die Elemente, bis die mit dem geeigneten Originalindex gefunden wird.

Aus diesem Grund eignet sich eine gute Hash-Funktion auch hervorragend zum Verteilen der Daten, sodass das Hash-Ergebnis unabhängig davon, ob die eingehenden Indizes sequentiell oder zufällig sind, so weit wie möglich verteilt werden sollte, um die Kosten für den Zugriff auf Daten relativ konstant zu halten.

Je größer das zugrunde liegende Array ist, desto weniger Kollisionen treten auf, sodass ein Kompromiss zwischen Geschwindigkeit und Größeneffizienz besteht. Moderne Hash-Tabellen füllen normalerweise bis zu ~ 70%, während weniger als 10 Kollisionen pro Zugriff auftreten. Zusammen mit der Hash-Funktion bedeutet dies, dass jeder Datenabruf ~ 20 Zyklen kostet, was (für einige Zwecke) ein guter Kompromiss zwischen Geschwindigkeit (Nachschlagetabelle) und Effizienz (Liste) ist.

user29075
quelle