Algorithmen und Datenstrukturen Prof. Martin Lercher Institut für Informatik Heinrich-Heine-Universität Düsseldorf Teil 5 Hash-Verfahren Version vom: 18. November 2016 1 / 28 Vorlesung 9 18. November 2016 2 / 28 Hash-Verfahren Beim Hashing werden die Schlüssel der Objekte für die Berechnung einer Position in einem Feld verwendet. Sei K eine Schlüsselmenge und I eine endliche Indexmenge (in der Regel I = {0, 1, 2 . . . , m − 1}). Die Hash-Funktion h:K →I wird zum Suchen und zum Platzieren der Elemente eingesetzt. Sie soll die Schlüssel gleichmäßig auf die Indizes verteilen und den Indexbereich ausschöpfen. Sie soll schnell zu berechnen sein. 3 / 28 Hash-Verfahren Wenn mehrere Schlüssel auf denselben Index abgebildet werden, spricht man von einer Kollision. Bei einer Kollision muss ein Ersatzplatz gefunden werden. Welche Funktionen sind als Hash-Funktion geeignet? In der Praxis hat sich Ausblenden und Restbildung bewährt. Ausblenden: Benutze nur einen Teil des Schlüssels (nicht alle Stellen). Restbildung: Der verbleibende Schlüssel k wird ganzzahlig durch die Länge der Hashtabelle dividiert. Der Rest wird als Index verwendet. h(k) = k mod |I | Die besten Ergebnisse erhält man, wenn |I | eine Primzahl ist. Schlüssel, die nicht als Zahlen interpretiert werden können, müssen vorher geeignet umgerechnet werden. 4 / 28 Hash-Verfahren Beispiel Schlüssel sind Zeichenketten der Länge 2. Zeichen werden als Zahlen interpretiert (ASCII = American Standard Code for Information Interchange). Die entstandenen Zahlen werden zum Schlüssel addiert. Zeichenkette GL LX SL GT RX GX k1 71 76 83 71 82 71 k2 76 88 76 84 88 88 k = k1 + k2 147 164 159 155 170 159 i = k mod 7 0 3 5 1 2 5 5 / 28 Hash-Verfahren - Universelle Hash-Funktionen Universelle Hash-Funktionen Gesucht sind Hash-Funktionen, die im Mittel akzeptabel sind. Sei m = |I | und H eine endliche Menge von ≥ m Hash-Funktionen. H heißt universell, wenn für jedes Paar von verschiedenen Schlüsseln x, y ∈ K gilt: |H| |{h ∈ H | h(x) = h(y )}| ≤ m Für jedes Schlüsselpaar x, y führt höchstens der m-te Teil der Hash-Funktionen die beiden Schlüssel x und y zu einer Kollision. 6 / 28 Hash-Verfahren - Universelle Hash-Funktionen Die Wahrscheinlichkeit, dass zwei Schlüssel mit einer Hash-Funktion aus H auf den gleichen Index abgebildet werden ist ≤ m1 . Sei p ≥ m eine Primzahl und ha,b : {0, . . . , p − 1} → {0, . . . , m − 1} mit ha,b (x) = ((ax + b) mod p) mod m. Dann ist {ha,b | 1 ≤ a < p, 0 ≤ b < p} eine universelle Klasse von Hash-Funktionen (lineare Algebra). 7 / 28 Hash-Verfahren - Kollisionsauflösung Kollisionsauflösung: Probleme treten auf: • bei der Platzierung, wenn der Eintrag auf der berechneten Hash-Adresse nicht leer ist, • bei der Suche, wenn der berechnete Platz ein anderes Element enthält. Zur Kollisionsauflösung muss ein Ersatzplatz gefunden werden. 8 / 28 Hash-Verfahren - Kollisionsauflösung 1. Verkettung der Überläufer: Die Überläufer können zum Beispiel in einer linearen Liste verkettet werden, welche an den Hashtabelleneintrag angehängt wird, der sich aus der Hash-Funktion angewendet auf den Schlüssel ergibt. Bei einer erfolglosen Suche nach Schlüssel k betrachten wir alle Elemente in der Liste an h(k). Die durchschnittliche Anzahl der Einträge in h(k) ist n/m, wenn n Einträge auf m Listen verteilt sind. 9 / 28 Hash-Verfahren - Kollisionsauflösung Belegungsfaktor: α= n m Im Mittel ist die Anzahl der bei der erfolglosen Suche nach einem Schlüssel betrachteten Einträge also Cn0 = n =α m Beim Einfügen des j-ten Schlüssels ist die durchschnittliche Listenlänge gerade (j − 1)/m. Also betrachten wir bei einer späteren Suche nach dem j-ten Schlüssel gerade 1 + (j − 1)/m Einträge im Durchschnitt, wenn stets am Listenende eingefügt und kein Datensatz entfernt wurde. 10 / 28 Hash-Verfahren Im Mittel ist die Anzahl der bei der erfolgreichen Suche nach einem Schlüssel betrachteten Einträge also Cn = n n−1 α 1X (1 + (j − 1)/m) = 1 + ≈1+ n 2m 2 j=1 Bemerkung: n X j=1 j= n(n + 1) 2 ⇒ n n−1 X X (n − 1)n (j − 1) = j= 2 j=1 j=0 11 / 28 Hash-Verfahren - Offene Hash-Verfahren 2. Offene Hash-Verfahren: Speichere die Überläufer in der Hashtabelle und nicht in zusätzlichen Listen. Nur sinnvoll für α < 1. Ist die Hash-Adresse h(k) belegt, so wird eine Ausweichposition gesucht. Die Folge der zu betrachtenden Speicherplätze für einen Schlüssel nennt man Sondierungsfolge. Sei h : K → {0, . . . , m − 1} eine Hash-Funktion und s : {0, . . . , m − 1} × K → N0 eine Funktion, so dass für jedes k ∈ K , die Folge (h(k) − s(j, k)) mod m für j = 0, 1, . . . , m − 1 eine Permutation aller Hash-Adressen 0, . . . , m − 1 ist, dann ist s eine Sondierungsfunktion für h. 12 / 28 Hash-Verfahren - Lineares Sondieren Lineares Sondieren: Beim linearen Sondieren ist für Schlüssel k die Sondierungsfolge h(k), h(k) − 1, h(k) − 2, . . . , 0, m − 1, . . . , h(k) + 1 Die Sondierungsfunktion ist somit s(j, k) = j 13 / 28 Hash-Verfahren - Lineares Sondieren Beispiel h(k) = k mod 7, s(j, k) = j (lineares Sondieren) Einfügen der Schlüssel 12, 53, 5, 15, 2, 19. 0 1 2 3 4 5 6 12 53 12 5 53 12 15 5 53 12 15 2 5 53 12 19 15 2 5 53 12 14 / 28 Hash-Verfahren - Lineares Sondieren Nachteil: Das Verfahren neigt zur primären Häufung, indem gewisse Bereiche keine Lücke mehr aufweisen. 15 / 28 Hash-Verfahren - Quadratisches Sondieren Quadratisches Sondieren: Es wird um h(k) herum mit quadratisch wachsendem Abstand nach einem freien Platz gesucht (in beide Richtungen). Sondierungsfolge: h(k), (h(k) + 1) mod m, (h(k) − 1) mod m, (h(k) + 4) mod m, (h(k) − 4) mod m, . . . Sondierungsfunktion: s(j, k) = (dj/2e)2 (−1)j Wenn m ein Primzahl der Form 4i + 3, i ∈ N, ist, dann ist garantiert, dass die Sondierungsfolge Modulo m eine Permutation der Hash-Adressen 0 bis m − 1 ist (ohne Beweis). 16 / 28 Hash-Verfahren - Quadratisches Sondieren Beispiel Für m = 13 ist die Sondierungsfolge keine Permutation der Hash-Adressen 0 bis m − 1. k k k k k k k k k k k k k mod 13 − (d0/2e)2 (−1)0 mod 13 mod 13 − (d1/2e)2 (−1)1 mod 13 mod 13 − (d2/2e)2 (−1)2 mod 13 mod 13 − (d3/2e)2 (−1)3 mod 13 mod 13 − (d4/2e)2 (−1)4 mod 13 mod 13 − (d5/2e)2 (−1)5 mod 13 mod 13 − (d6/2e)2 (−1)6 mod 13 mod 13 − (d7/2e)2 (−1)7 mod 13 mod 13 − (d8/2e)2 (−1)8 mod 13 mod 13 − (d9/2e)2 (−1)9 mod 13 mod 13 − (d10/2e)2 (−1)10 mod 13 mod 13 − (d11/2e)2 (−1)11 mod 13 mod 13 − (d12/2e)2 (−1)12 mod 13 = = = = = = = = = = = = = k k k k k k k k k k k k k mod 13 + 1 mod 13 − 1 mod 13 + 4 mod 13 − 4 mod 13 + 9 mod 13 − 9 mod 13 + 16 mod 13 − 16 mod 13 + 25 mod 13 − 25 mod 13 + 36 mod 13 − 36 mod 13 = = = = = = = = = = = = = k k k k k k k k k k k k k + 0 mod 13 + 1 mod 13 + 12 mod 13 + 4 mod 13 + 9 mod 13 + 9 mod 13 + 4 mod 13 + 3 mod 13 + 10 mod 13 + 12 mod 13 + 1 mod 13 + 10 mod 13 + 3 mod 13 Es fehlen die Indices ((k + j) mod 13) mit j ∈ {2, 5, 6, 7, 8, 11}. 17 / 28 Hash-Verfahren - Quadratisches Sondieren Beispiel h(k) = k mod 7, s(j, k) = (dj/2e)2 (−1)j (quadratisches Sondieren) Einfügen der Schlüssel 12, 53, 5, 15, 2, 19. 0 1 2 3 4 5 6 12 53 12 53 12 5 15 53 12 5 15 2 53 12 5 19 15 2 53 12 5 (Fehler: 19 → 3) 18 / 28 Hash-Verfahren - Quadratisches Sondieren Nachteil: Zwei Schlüssel k und k 0 mit h(k) = h(k 0 ) durchlaufen stets dieselbe Sondierungsfolge. Sie behindern sich also auf Ausweichplätzen (sekundäre Häufung). 19 / 28 Hash-Verfahren - Quadratisches Sondieren Eine Analyse der Effizienz des linearen und quadratischen Sondierens zeigt, dass für die durchschnittliche Anzahl der bei erfolgloser bzw. erfolgreicher Suche betrachteten Einträge Cn0 bzw. Cn gilt: 1 2 Lineares Sondieren: 1 1 1+ Cn0 ≈ 2 (1 − α)2 Quadratisches Sondieren: 1 α 0 Cn ≈ 1 + ln − 1−α 2 Cn ≈ 1 2 1+ 1 (1 − α) 1 Cn ≈ − α + ln 1−α 1 1−α Bemerkung: Offene Hash-Verfahren sind immer ineffizienter als eine Verkettung der Überläufer. 20 / 28 Hash-Verfahren Vergleich der Effizienz zwischen linearem und quadratischem Sondieren: α 0.5 0.9 0.95 0.99 linear erfolgreich erfolglos 1.5 2.5 5.5 50.5 10.5 200.5 50.5 5000.5 quadratisch erfolgreich erfolglos 1.44 2.19 2.85 11.40 3.52 22.05 5.11 103.60 21 / 28 Hash-Verfahren - Double Hashing Verbesserte Kollisionsauflösung: Schlüsselabhängige Sondierung (double Hashing): Verwende für die Sondierungsfolge eine zweite Hash-Funktion h0 . Sondierungsfolge: h(k), (h(k) − h0 (k)) mod m, (h(k) − 2 · h0 (k)) mod m, . . . . . . , (h(k) − (m − 1) · h0 (k)) mod m 22 / 28 Hash-Verfahren - Double Hashing Sondierungsfunktion: s(j, k) = j · h0 (k) Die Funktion h0 muss so gewählt werden, dass die Sondierungsfolge eine Permutation der Hash-Adressen ist. Das bedeutet, dass h0 (k) 6= 0 sein muss und m nicht teilen darf. Ist m eine Primzahl und h(k) = k mod m, so erfüllt h0 (k) = 1 + (k mod (m − 2)) die erwarteten Anforderungen. Anmerkung: h0 (k) = 1 + (k mod (m − 1)) würde die Anforderungen auch erfüllen, wird aber ungern verwendet, da m − 1 eine grade Zahl ist. Unterschiedliche Schlüssel, die auf denselben Index abgebildet werden, erhalten unterschiedliche Inkremente. 23 / 28 Hash-Verfahren - Double Hashing Beispiel m = 7, h(k) = k mod 7, h0 (k) = 1 + (k mod 5) Einfügen der Schlüssel 12, 53, 5, 15, 2, 19. 0 1 2 3 4 5 6 53 12 5 53 12 15 5 53 12 15 2 5 53 12 19 15 2 5 53 12 24 / 28 Hash-Verfahren Verbesserung der erfolgreichen Suche: Die durchschnittliche Suchzeit bei der erfolgreichen Suche variiert bei Hash-Verfahren ohne Häufung mit unterschiedlicher Reihenfolge des Einfügens der Schlüssel. In Fällen, in denen wesentlich häufiger gesucht wird als eingefügt, kann es lohnend sein, die Schlüssel beim Einfügen eines neuen Schlüssels so zu reorganisieren, dass die Suchzeit verkürzt wird. 25 / 28 Hash-Verfahren - Brents Algorithmus Beim selben Double Hashing wie zuvor: 0 1 2 3 4 5 6 53 12 5 53 12 12 53 5 Wird Schlüssel 5 nach Inspektion der Plätze 5, 4, 3 bei Hash-Adresse 3 eingetragen, so ist die durchschnittliche Suchzeit der drei eingetragenen Elemente (1 + 1 + 3)/3 = 5/3 = 1.66 (mittlere Zeile). Man hätte aber auch Schlüssel 5 auf Platz 5 setzen können und Schlüssel 12 weiter sondieren lassen können. Dann wäre die durchschnittliche Suchzeit (1 + 1 + 2)/3 = 4/3 = 1.33 (untere Zeile). 26 / 28 Hash-Verfahren - Brents Algorithmus Brents Algorithmus: Methode: Einfügen eines Schlüssels k: Beginne mit Hash-Adresse i = h(k). Falls Position i belegt ist, betrachte die beiden Hash-Adressen b = (i − h0 (k)) mod m und b 0 = (i − h0 (k 0 )) mod m, wobei k 0 der Schlüssel auf Hash-Adresse i ist. Ist b eine freie Hash-Adresse, dann positioniere Schlüssel k auf Position b, ansonsten, ist b 0 eine freie Hash-Adresse, dann positioniere Schlüssel k auf Position i und k 0 auf Position b 0 , ansonsten fahre rekursiv fort mit dem Versuch Schlüssel k auf Position i = b zu positionieren. 27 / 28 Hash-Verfahren - Brents Algorithmus Beispiel m = 7, h(k) = k mod 7, h0 (k) = 1 + (k mod 5) Einfügen der Schlüssel 12, 53, 5, 15, 2, 19. 0 1 2 3 4 5 6 53 12 12 53 5 15 12 53 5 15 12 53 5 2 19 15 12 53 5 2 28 / 28