Suchen

Hashing mit linearer Sondierung (Algorithmus 3.6)

Die Hashtabelle mit linearer Sondierung aus Sedgewick/Wayne, Algorithmus 3.6 (Listing 3.20) – jede Sondierung einzeln, Cluster, Löschen mit Neu-Einfügen des Clusters und Verdoppeln der Tabelle, mit dem Testclient aus Abbildung 3.59.

Lesestand

So geht’s: Beim Öffnen ist der Testclient aus Abbildung 3.59 geladen: 16 Plätze, SEARCHEXAMPLE, Wert = Position im Wort. Jede Operation beginnt bei h = hash(key), dann zeigt jeder Schritt eine Sondierung: belegter Platz, Vergleich, weiter nach rechts – am Ende des Arrays vorn weiter. Oben steht neben Füllgrad und längstem Cluster, wie viele Sondierungen bei diesem Füllgrad im Schnitt zu erwarten sind. Mit delete sieht man, warum Löschen knifflig ist: Der Rest des Clusters wird Schlüssel für Schlüssel neu eingefügt. Der Modus „Java, wie Listing 3.20“ rechnet mit Javas echtem hashCode() und verdoppelt ab halb voll – die neue Tabelle füllt sich dann sichtbar unter der alten.

Erklärung

Hashing mit linearer Sondierung (Algorithmus 3.6), Klasse LinearProbingHashST. Gleiche Hashfunktion wie in [26], aber keine Listen. Alles liegt direkt in zwei parallelen Arrays keys[] und vals[], wie bei der binären Suche [23].

Kollisionsauflösung durch Sondieren: Ist der berechnete Platz belegt, nimm einfach den nächsten freien rechts daneben. Am Arrayende geht es bei 0 weiter ((i + 1) % M). Leere Plätze sind null.

Der Code (Listing 3.20):

  • Startgröße M = 16.
  • put: Ist die Tabelle halb voll (N >= M/2), erst verdoppeln. Dann ab hash(key) nach rechts laufen, bis entweder der Schlüssel gefunden ist (Wert überschreiben) oder ein null kommt (dort einfügen, N++).
  • get: Ab hash(key) nach rechts laufen. Schlüssel gefunden → Wert. null erreicht → erfolglos, denn wäre der Schlüssel da, hätte er spätestens hier stehen müssen.
  • resize (Listing 3.22) kann nicht einfach umkopieren wie in [14]: Die Positionen hängen von M ab, also muss jeder Schlüssel in die neue Tabelle neu eingefügt werden.

Abbildung 3.59 (M = 16, SEARCHEXAMPLE):

  • Die meisten landen direkt: S→6, E→10, A→4, R→14, C→5, X→15, M→1.
  • H hat Hash 4: Platz 4 (A), 5 (C), 6 (S) belegt → H landet auf 7. Schwarz = beim Sondieren angeschaut.
  • P hat Hash 14: 14 (R), 15 (X) belegt, Umbruch → P landet auf 0.
  • L hat Hash 6: 6 (S), 7 (H) belegt → L landet auf 8.
  • Endstand: 0 P, 1 M, 4 A, 5 C, 6 S, 7 H, 8 L, 10 E, 14 R, 15 X.

Cluster: Zusammenhängende Blöcke belegter Plätze, hier etwa 4 bis 8. Sie sind das Kernproblem: Jeder Schlüssel, der irgendwo in den Cluster hasht, verlängert ihn. Große Cluster wachsen dadurch schneller als kleine.

Löschen ist knifflig: Einfach null setzen geht nicht. Löscht man C auf Platz 5, bricht eine Suche nach H (Hash 4) bei 5 ab und findet H auf 7 nie. Lösung: Nach dem Löschen alle Schlüssel rechts davon bis zum Clusterende neu einfügen (Listing 3.21).

Kosten mit Füllgrad α = N/M:

  • Erfolgreiche Suche: etwa ½ (1 + 1/(1 − α)) Sondierungen.
  • Erfolglose Suche/Einfügen: etwa ½ (1 + 1/(1 − α)²).
  • Bei α = ½: rund 1,5 bzw. 2,5 Sondierungen. Geht α gegen 1, explodiert es.
  • Deshalb Resizing: verdoppeln ab halb voll, halbieren unter ein Achtel. So bleibt α zwischen ⅛ und ½, und alles ist im Schnitt konstant.

Sondierung vs. Verkettung [26]:

  • Sondierung: kein Listen-Overhead, alles in einem Array, cachefreundlich, oft schneller. Aber Tabelle muss deutlich größer als N bleiben, Löschen ist umständlich.
  • Verkettung: Füllgrad darf auch über 1 liegen, Löschen einfach. Dafür Extra-Objekte pro Eintrag.

Kapitel 3 komplett:

SucheEinfügengeordnete Ops
3.1 SequenziellNNnein
3.2 Binäre Suchelg NNja
3.3 BSTlg N Schnitt, N worstlg N Schnitt, N worstja
3.4 Rot-Schwarz≤ 2 lg N garantiert≤ 2 lg N garantiertja
3.5 Hashing Verkettungpraktisch konstantpraktisch konstantnein
3.6 Hashing Sondierungpraktisch konstantpraktisch konstantnein

Javas HashMap nutzt übrigens Verkettung, Pythons dict Sondierung (allerdings nicht linear).