4. Hashverfahren geg.: Wertebereich D, Schlüsselmenge S = {s 1,..., s n } D Menge A von Speicheradressen; oft: A = {0,..., m 1} jedes Speicherverfahren realisiert h : D A mögliche Implementierungen von h durch Liste, Baum,... alternativ: Hashing berechne h(s) direkt (meist in O(1)) aus s Problem: D meist sehr groß daher: h i.allg. nicht injektiv; oft: h(s) = s mod m Kollision bei Einfügen von s, wenn s S, s s h(s) = h(s ) 110
4.1 Kollisionsbehandlung 1.) offene Hashverfahren: verwende anderen, freien Platz h (s) A (innerhalb der Hashtabelle) 2.) Hashverfahren mit Verkettung: h(s) führt zu Liste mit Elementen s, für die h(s ) = h(s) 111
4.2 Wahl der Hashfunktion Anforderung: wenige Kollisionen 4.2.1 Divisionsrest-Methode in Praxis meist am besten h(s) = s mod m ungünstiges m: 2er-Potenz, 10er-Potenz,... Beispiel: Preise und m = 100: h({49, 69, 99, 149, 199, 299, 399}) = {49, 69, 99} gut: m Primzahl 112
4.2.2 Multiplikative Methode multipliziere mit irrationaler Zahl θ und ignoriere ganzzahligen Anteil h(s) = m (s θ s θ ) effizient implementierbar bei m = 2 k durch int, 2 Shifts gut: θ = 5 1 2 0.6180339887 (goldener Schnitt - 1) 113
4.3 Hashverfahren mit Verkettung Synonyme werden in dyn. Datenstruktur (z.b. Liste) außerhalb der Hashtabelle gespeichert 4.3.1 Separate Verkettung jeder Hashtabelleneintrag ist Anfang einer Liste von Synonymen Beispiel: (mit h(s) = s mod 7, S = {1, 10, 3, 6, 20, 13}) 0 1 2 3 4 5 6 1 10 6 3 20 13 114
Operationen in Hashtabelle Suchen: beginne mit h(s) und folge Verweisen bis gefunden oder Listenende Einfügen: füge s am Ende der Liste h(s) ein, wenn s nicht gefunden (!) Löschen: suche s und entferne s aus Liste 115
4.3.2 Direkte Verkettung jeder Hashtabelleneintrag ist Zeiger auf Liste von Synonymen Vorteil: weniger Ausnahmebehandlungen/ Abfragen als bei separater Verkettung Beispiel: 0 1 2 3 4 5 6 1 10 6 3 20 13 Suche, Einfügen und Löschen analog zu separater Verkettung 116
Direkte Verkettung in Java public class HashTable<D> implements MyCollection<Integer,D>{ } protected int size; protected List<Integer,D>[] tab; public HashTable(int n) {size = n; for (int i=0; i<tab.length; i++) private int h(int key) {return key % size;} public D find(integer key) throws Exception { List<Integer,D> list = return list.find(key);} tab[h(key)]; public void insert(integer key, D content) { List<Integer,D> list = list.insert(key,content);} tab[h(key)]; public void delete(integer key) { List<Integer,D> list = tab[h(key)]; list.delete(key);} tab = new List[size]; tab[i] = new List<Integer,D>();} // Mehrfacheintraege erlaubt public HashIterator iterator() {return new HashIterator();} 117
Analyse von direkter Verkettung im schlechtesten Fall: alle Schlüssel in gleicher Liste tw suche (n) O(n) im Mittel: Annahme: alle Hashadressen gleich wahrscheinlich def.: Belegungsfaktor α := n m durchschnittliche Länge l einer Liste: l = α bei erfolgloser Suche: bei erfolgreicher Suche: ˆt A suche (n) = 1 n ta suche (n) = α O(1) für n m n (1 + j 1 m ) = 1 + n 1 2m 1 + α 2 j=1 Aufwand des Löschens: wie bei erfolgreicher Suche Einfügen: 1 (bzw. wie bei erfolgloser Suche, wenn keine Duplikate) 118
Vergleich: separate vs. direkte Verkettung α separate Verkettung direkte Verkettung erfolgreich erfolglos erfolgreich erfolglos 0.50 1.250 1.110 1.250 0.50 0.90 1.450 1.307 1.450 0.90 0.95 1.475 1.337 1.475 0.95 1.00 1.500 1.368 1.500 1.00 Anzahl bei der Suche betrachteter Einträge (nach Ottmann, Widmayer) Bemerkung: die Synonymlisten können (aufgeteilt in Seiten) im Sekundärspeicher liegen α > 1 möglich 119
4.4 Offene Hashverfahren Synonyme innerhalb der Hashtabelle gespeichert betrachte Folge von Hashfunktionen h i : D A i = 0, 1, 2,... Einfügen: sind h 0 (s),..., h i 1 (s) belegt und h i (s) frei, so speichere s in h i (s) Suche: suche s in h 0 (s),..., h i (s) bis h i (s) enthält s oder bis h i (s) frei 120
Löschen s zunächst suchen Problem: naives Entfernen zerstört Suchketten für andere Elemente Lösungsmöglichkeit: s nur als gelöscht markieren aber nicht wirklich entfernen markierte Plätze beim Einfügen wiederverwenden Effizienz leidet unter Löschmarkierungen offenes Hashing nur geeignet, wenn Löschen selten 121
4.4.1 Lineares Sondieren Beispiel: offenes Hashverfahren mit füge ein 6, 10, 13, 20, 3, 1 h i+1 (s) = h i (s) + 1 mod m für i IN 0 1 2 3 4 5 6 6 Vorteil: einfach implementierbar 10 6 Nachteil: primäre Häufung verschlechtert 13 10 6 Effizienz 13 20 10 6 13 20 10 3 6 13 20 1 10 3 6 122
Aufwand im schlechtesten Fall: linear (O(n)) im Mittel: ( Knuth) Annahme: alle Hashadressen gleich wahrscheinlich falls Tabelle beim Einfügen bereits k Einträge enthält: p 1 = m k m gefunden Wahrscheinlichkkeit, daß beim 1. Versuch freier Platz p 2 = k m k m m 1... beim 2. Versuch... p 3 = k k 1 m m 1 m 2... beim 3. Versuch... allgemein: p i = ( i 2 j=0 Erwartungswert beim (k + 1)-ten Einfügen: k+1 E k+1 = i=1 k j m j ) m k m i+1... beim i. Versuch... i p i = }{{} (Induktion, Mehlhorn) m + 1 m k + 1 123
E = 1 k k = m+1 k E j j=1 k j=1 Mittlerer Einfügeaufwand bei k Schlüsseln 1 m j+2 = m+1 k (H m+1 H m k+1 ) wobei: H i = i m+1 k (ln(m + 1) ln(m k + 1)) = m+1 k ln( m+1 m k+1 ) = 1 k m+1 ln( 1 1 k m+1 = 1 α (ln(1) ln(1 α )) }{{} =0 ) j=1 = 1 α ln(1 α ) wobei α = k m+1 1 k m 1 j ln(i) + 0.57721566 }{{} Eulersche Konstante harmonische Zahlen 124
Bemerkungen bei linearem Sondieren ist Gleichverteilungsannahme unrealistisch daher in Praxis: E = 1 α 2 1 α α 0.5 0.75 0.9 0.95 E 1.39 1.85 2.56 3.15 E 1.50 2.50 5.50 10.50 (nach Wirth) Folgerung: Tabelle in Praxis 10% zu groß wählen 125
4.4.2 Quadratisches Sondieren h i (s) = (h 0 (s) + i 2 ) mod m ggf. Einfügen unmöglich, obwohl noch Plätze frei für i > 0, m prim besser als lineares Sondieren, da keine primäre Häufung 126
4.5 Perfektes Hashing Idee: analog zu optimalem Suchbaum Hashverfahren für feste Schlüsselmenge S = {s 1,..., s n } {1,..., N} optimieren gesucht: perfekte (d.h. injektive) Hashfunktion, die effizient berechenbar (in O(1)) und mit möglichst kleiner Hashtabelle erreichbar: (Details s. Mehlhorn) Auswertung in O(1) Tabellengröße m < 3n also: α > 1/3 Ermittlung einer geeigneten Hashfunktion in O(n N) (bei großem N verbesserbar zu: O(n 3 log n + log(log N))) 127
Ermittlung einer geeigneten Hashfunktion 1) bestimme k (1 k < N) mit n 1 S i 2 < 3n i=0 wobei S i := {x S (kx mod N) mod n = i} für i = 0,..., n 1 Aufwand: O(n N) (geeignetes k existiert! Mehlhorn) 2) c i := S i ( S i 1) + 1 für i = 0,..., n 1 m := n 1 c i i=0 = n + n 1 i=0 < n + 3n n S i 2 n 1 i=0 S i = 3n 128
Ermittlung einer geeigneten Hashfunktion (2) 3) bestimme k i (i = 0,..., n 1, 1 k i < N) mit h i (x) := (k i x mod N) mod c i auf S i injektiv Aufwand für i: O( S i N) 4) h(x) := let i = (k x mod N) mod n, in j = (k i x mod N) mod c i i 1 + j l=0 c l }{{} vorberechnet (geeignetes k i existiert! Mehlhorn) Auswertung: O(1) (2 *, 4 mod, 1 + ) 129
Beispiel: Perfektes Hashing S = {1, 3, 4, 7}, N = 7, n = 4 1) probiere k = 1: S 0 = {4, 7}, S 1 = {1}, S 2 =, S 3 = {3} 3 S i 2 = 4 + 1 + 0 + 1 = 6 < 12 = 3n k = 1 geeignet i=0 2) c 0 := 3, c 1 := 1, c 2 := 1, c 3 := 1, m := 6 3) probiere k 0 = 1: h 0 (x) := (x mod 7) mod 3 h 0 (4) = 1, h 0 (7) = 0 k 0 = 1 geeignet für i = 1,..., 3: k i = 1 trivialerweise geeignet, wegen S i 1 4) Hashtabelle: 0 1 2 3 4 5 7 4 1 3 } {{ } S 0 }{{} S 1 }{{} S 2 }{{} S 3 130
4.6 Universelles Hashing Details s. Mehlhorn statt Hashfunktion h manuell vorgeben: h zufällig aus Topf H ziehen Vorteil: bei mehrfachem Aufbau einer Hashtabelle für (fast) festes S wird die Effizienz über H gemittelt (z.b. für Symboltabelle eines Compilers) ein dauerhaft unglücklich gewähltes h wird vermieden z.b. H := {h a,b h a,b (x) := (ax + b mod p) mod m, a, b {0,..., N 1}} wobei m, N IN, p prim 131
4.7 Dynamisches Hashing erweiterbare bzw. verkleinerbare Hashtabelle insbesondere für Sekundärspeicher Varianten: Lineares Hashing Erweiterbares Hashing Gridfile (Details: Ottmann, Widmayer) 132
Lineares Hashing Folge von Hashfunktionen h i (x) = x mod (m 0 2 i ) i = 0, 1,... stets zwei Funktionen, h i und h i+1, aktiv... h i+1 j: 87,167,247 j: 167... h i m *2i : m0 *2 i : 0 h i+1 h i+1... Erweitern j + m *2i : j + m *2 i : 0 0 frei... 87,247............ hi+1 h i frei 133
Erweitern und Verkleinern beim Linearen Hashing Erweitern falls α > α max : j := j + 1; Schlüssel in Bucket B j 1 werden gemäß h i+1 umgespeichert falls hiernach j = m 0 2 i : Tabellengröße verdoppeln; j := 0; i := i + 1 Verkleinerung umgekehrt, wenn α < α min 134
Zusammenfassung: Hashverfahren im Mittel sehr effizient (bei α < 90%) (meist: O(1)) im Worst Case: schlecht (O(n)) #Schlüssel muss ungefähr bekannt sein, sonst Platzverschwendung bzw. Ineffizienz (Abhilfe: dynamische Hashverfahren) keine sortierte Ausgabe möglich ( Variante: monotone Hashfunktion) Löschen bei offenem Hashing umständlich auch mehrdimensionales Hashing möglich ( partial match queries) 135