Algorithmen und Datenstrukturen - Universität Düsseldorf: Informatik

Werbung
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
Herunterladen