kind: reference

Hash Tables

Eine Hash Table findet einen Schlüssel, ohne zu suchen: Eine Hashfunktion rechnet aus, in welchem Bucket er liegen muss. Weil sich zwei Schlüssel einen Bucket teilen können, braucht jede Hash Table einen Plan für Kollisionen, eine Liste je Bucket oder den nächsten freien Platz. Hier steht, wie beide arbeiten, was der Füllgrad damit zu tun hat und wann die Tabelle wächst.

1. Hashfunktion und Buckets

Ein Array findet den Wert an Index 5 sofort, eine Liste von Namen muss man durchsuchen. Eine Hash Table verbindet beides: Sie hat ein Array von \(m\) Buckets, und eine Hashfunktion rechnet aus jedem Schlüssel einen Index von 0 bis \(m - 1\) aus. Dort wird gespeichert, und dort wird später gesucht.

Für ganze Zahlen ist die einfachste Hashfunktion der Rest der Division:

$$ h(k) = k \bmod m $$

Bei 8 Buckets landet 13 in Bucket 5, 21 auch in Bucket 5, 6 in Bucket 6.

Was eine gute Hashfunktion kann

  • Gleiche Schlüssel, gleicher Bucket, jedes Mal. Sonst findet man nichts wieder.
  • Verschiedene Schlüssel möglichst gleichmäßig verteilt. Bei \(k \bmod 8\) landen alle geraden Zahlen in geraden Buckets, die Hälfte der Tabelle bleibt leer, wenn nur gerade Schlüssel kommen. Eine Primzahl als Bucketzahl, etwa 7 oder 11, verteilt solche Muster besser.
  • Schnell zu berechnen.

Für Strings rechnet man aus allen Zeichen eine Zahl, zum Beispiel so, dass jedes Zeichen den bisherigen Wert mal 31 plus seinen Code ergibt, und nimmt davon den Rest. Wer nur das erste Zeichen nimmt, bekommt für »Anna«, »Alex« und »Ali« denselben Bucket.

Negative Schlüssel. In Java, JavaScript und C ist der Rest einer negativen Zahl negativ: -7 % 8 ergibt −7, kein gültiger Index. Python und Ruby rechnen 1. Wer sicher gehen will, rechnet ((k % m) + m) % m.

2. Kollisionen: Chaining

Zwei Schlüssel im selben Bucket sind eine Kollision. Sie lassen sich nicht vermeiden: Es gibt mehr mögliche Schlüssel als Buckets. Beim Chaining (separate chaining) hält jeder Bucket eine kleine Liste, und alle Schlüssel, die dort landen, kommen in diese Liste.

Hans Peter Luhn (1896–1964), Ingenieur aus Barmen (heute Wuppertal), später bei IBM in den USA. 1953 schlug er in einer Notiz für IBM vor, Schlüssel mit einer Rechnung auf Buckets zu verteilen und Kollisionen in Listen zu sammeln: die erste Hash Table, mit Chaining.

Von ihm stammt auch die letzte Ziffer jeder Kreditkartennummer, die Prüfziffer nach dem Luhn-Algorithmus: Mit ihr fällt eine falsch getippte Ziffer sofort auf.

insert(k):
    b ← h(k)
    if k already in buckets[b]: done (oder Fehler, je nach Aufgabe)
    append k to the back of buckets[b]
    count ← count + 1

find(k):
    return whether k is in buckets[b], with b ← h(k)

remove(k):
    remove k from buckets[h(k)] if it is there

Gesucht wird nur in einem Bucket. Solange die Listen kurz sind, ist das fast so schnell wie ein Array. Sechs Schlüssel in acht Buckets, darunter drei, die alle in Bucket 5 landen:

Und im fertigen Bucket-Array zwei Suchen, eine erfolgreich und eine nicht:

3. Kollisionen: Linear Probing

Ohne Listen geht es auch: Jeder Bucket hält höchstens einen Schlüssel. Ist der Bucket \(h(k)\) belegt, probiert man den nächsten, dann den übernächsten, und am Ende des Arrays geht es vorn weiter. Das heißt Linear Probing (open addressing).

insert(k):
    b ← h(k)
    while buckets[b] occupied:
        if buckets[b] == k: done
        b ← (b + 1) mod m
    buckets[b] ← k
    count ← count + 1

find(k):
    b ← h(k)
    while buckets[b] occupied:
        if buckets[b] == k: found
        b ← (b + 1) mod m
    not found

Die Suche hört erst an einem leeren Bucket auf, denn bis dahin könnte \(k\) beim Einfügen weitergerutscht sein. Dieselben sechs Schlüssel wie oben:

Und im fertigen Bucket-Array zwei Suchen, eine erfolgreich und eine nicht:

Die Kollisionen in Bucket 5 schieben andere Schlüssel weiter, die selbst gar nicht kollidiert hätten: 6 gehört in Bucket 6, doch dort steht schon 21. So wachsen lange belegte Strecken, Cluster, und die Suche nach 37 muss am Ende fast die ganze Tabelle ansehen.

Entfernen ist bei Linear Probing heikel: Wird ein Bucket einfach geleert, endet die Suche nach einem Schlüssel, der hinter ihm liegt, zu früh. Man markiert ihn deshalb als gelöscht (einen »Grabstein«): Beim Suchen läuft man darüber hinweg, beim Einfügen darf er wieder belegt werden.

4. Füllgrad und Rehashing

Der Füllgrad (load factor) ist die Zahl der Schlüssel geteilt durch die Zahl der Buckets, \(\alpha = n / m\). Er sagt, wie schnell die Tabelle ist:

  • Beim Chaining ist eine Liste im Mittel \(\alpha\) lang. Bei \(\alpha = 1\) steht in jedem Bucket im Mittel ein Schlüssel, bei \(\alpha = 10\) sucht man in Listen mit zehn.
  • Bei Linear Probing muss \(\alpha\) unter 1 bleiben, sonst passt nichts mehr hinein, und schon ab etwa 0,7 werden die Cluster lang.

Deshalb wächst eine Hash Table, wenn der Füllgrad eine Grenze überschreitet, meist 0,75: Sie legt ein Array mit doppelt so vielen Buckets an und fügt jeden Schlüssel neu ein. Kopieren genügt nicht, denn mit \(m\) ändert sich \(h(k) = k \bmod m\), und jeder Schlüssel kommt in einen anderen Bucket. Das heißt Rehashing. Wie beim Vergrößern einer Array List ist es teuer, aber selten.

insert(k):
    … as above …
    if count / m > 0.75:
        old ← buckets
        m ← 2 · m
        buckets ← m empty buckets
        count ← 0
        for each key s in old: insert(s)

Wofür

Hash Tables stecken hinter jedem Hash in Ruby, jedem dict und set in Python, jedem Objekt in JavaScript und jeder HashMap in Java: überall, wo man zu einem Schlüssel schnell einen Wert finden will. Ein Suchbaum kann dasselbe, langsamer, aber er hält die Schlüssel dafür sortiert. Eine Hash Table weiß nichts über die Reihenfolge, nach dem kleinsten Schlüssel muss sie alle Buckets durchsuchen.

5. Zum Nachschlagen

Hash in Ruby, dict und set in Python sind fertige Hash Tables, mit Hashfunktion, Kollisionen und Rehashing, ohne dass du davon etwas merkst. Sollen zwei eigene Objekte mit gleichem Inhalt derselbe Schlüssel sein, brauchen sie eine Hashfunktion, die zu ihrer Gleichheit passt: gleiche Schlüssel, gleicher Bucket.

  • Ruby: Hash: alle Methoden; der Abschnitt User-Defined Hash Keys zeigt, wie eigene Objekte mit hash und eql? zu Schlüsseln werden.
  • Python: dict und set: die beiden Hash Tables von Python.
  • Python: __hash__: was eine eigene Klasse zusammen mit __eq__ braucht, damit ihre Objekte Schlüssel sein können.
  • VisuAlgo: Hash Table: Chaining und Linear Probing als Animation, dazu Quadratic Probing und Double Hashing, auf Englisch.