Kapitel 8 Hashing Dieses Kapitel beschäftigt sich mit einem wichtigen Speicherungs- und Suchverfahren, bei dem die Adressen von Daten aus zugehörigen Schlüsseln errechnet werden, dem Hashing. Dabei stehen die Algorithmen und verschiedene Heuristiken der Kollisionsbehandlung im Vordergrund. Es wird aber auch auf die in Java vordefinierte Klasse Hashtable eingegangen. 115 8.1 Einführendes Beispiel Ein Pizza-Lieferservice in Bielefeld speichert die Daten seiner Kunden: Name, Vorname, Adresse und Telefonnummer. Wenn ein Kunde seine Bestellung telefonisch aufgibt, um dann mit der Pizza beliefert zu werden, dann muss er seine Telefonnummer angeben, da er über diese Nummer eindeutig identifiziert werden kann. Natürlich existiert in Bielefeld jede Telefonnummer nur einmal, während es mehrere Menschen mit gleichem Vornamen, Nachnamen oder Adresse gibt. Das bedeutet: Wenn der Telefonist in der Pizzeria die Telefonnummer des Kunden erfragt (oder von dem Display seines Telefons abliest) und diese in seinen Computer eingibt, dann bekommt er genau einen Kunden mit Name, Vorname und Adresse angezeigt, vorausgesetzt, der Kunde wurde schon einmal in die Datenbank eingetragen. Die Telefonnummer ist also eine Art Schlüssel für die Suche nach einem Datensatz, in diesem Fall die Kundendaten. Abstrakter bedeutet dies, dass wir über einen Schlüssel (key) Zugriff auf einen Wert (value) erhalten. Stellen wir uns die Repräsentation der Daten in dem Programm, das die Pizzeria benutzt, so vor: Telefonnummer Name Vorname PLZ Straße 00000000 Müller Heinz 33615 Unistraße 15 00000001 Schmidt Werner 33615 Grünweg 1 00000002 Schultz Hans 33602 Arndtstraße 12 ... ... ... ... ... 99999997 Meier Franz 33609 Kirchweg 4 99999998 Neumann Herbert 33612 Jägerallee 15 99999999 Schröder Georg 33647 Mühlweg 2 Die Daten werden also in einer Tabelle gespeichert. Dabei entsprechen die Zeilen den Telefonnummern, die es in Bielefeld möglicherweise geben könnte, nämlich den achtstelligen Zaheln von 00000000 bis 99999999 – wobei natürlich einige Zahlen, wie etwa die 00000000 keine gültigen Telefonnummern sind und wir vereinfachend annehmen wollen, dass Telefonnummern, die weniger als acht Stellen haben, mit führenden Nullen aufgefüllt werden können. Bis hierher würde die Datentabelle also so aussehen wie in der obigen Abbildung mit 108 – also 100 Millionen – verschiedene Nummern, die jeweils einer Zeile in der Tabelle entsprechen. Hierbei fällt schon auf, dass diese Zahl natürlich viel zu groß ist, denn es gibt sicherlich nicht annähernd so viele Telefonanschlüsse in Bielefeld. Die Menge der möglichen Schlüssel, also die Zahlen 00000000 bis 99999999 ist gegenüber der Zahl der tatsächlich informativen Einträge viel zu groß, so dass eine große Menge Speicher für Telefonnummern verbraucht wird, die nie zugeordnet werden. Weiterhin ist zu bedenken, dass nicht jeder Einwohner Bielefelds eine 116 Telefonnummer hat und dass nicht alle, die eine Nummer haben, beim PizzaService bestellen, und wenn sie doch eine Pizza bestellen, nicht unbedingt bei dieser Pizzeria. Gehen wir also davon aus, dass von der Menge aller Schlüssel nur ein kleiner Teil wirklichen Datenbankeinträgen entspricht. Bielefeld hat ca. 300.000 Einwohner, dann gibt es vielleicht 200.000 Telefonnummern. Davon bestellt jeder fünfte eine Pizza – bleiben 40.000 potentielle Einträge, verteilt auf mehrere PizzaLieferservices. Optimistisch geschätzt wird unsere Pizzeria also ca. 10.000 Kunden haben. Damit bleiben von den 100 Millionen Schlüsseln nur 10.000 tatsächlich benutzte übrig, das sind 0,01 Prozent. Dies bedeutet für die Tabelle der Kundendaten, dass sie erheblich verkleinert werden kann – nämlich auf 10.000 Zeilen, denn mit mehr Kunden braucht die Pizzeria nicht zu rechnen. Da stellt sich folgende Frage: Wir wissen doch gar nicht, welche Telefonnummern bestellen werden – wie sollen denn dann die Zeilen benannt werden? Unsere Aufgabe ist es, alle 100 Millionen Telefonnummern (denn jede einzelne könnte ja theoretisch bestellen) so abzubilden, dass sie in eine 10.000 Zeilen große Tabelle passen. Hierzu machen wir uns jetzt eine mathematische Operation zunutze, die Modulo-Operation: x mod y liefert als Ergebnis den Rest der ganzzahligen Division x/y. Beispielsweise ergibt 117 mod 20 = 17, da 117 = 5 · 20 + 17. Wenn wir jetzt jede angegebene Telefonnummer modulo 10.000 nehmen, bekommen wir ein Ergebnis zwischen 0 und 9999. Somit können wir alle theoretischen Telefonnummern und damit Pizza-Besteller in einer Tabelle abbilden, die gerade mal 10.000 Zeilen hat. Die Funktion, die wir dazu benutzen, lautet: h(Telefonnummer) = Telefonnummer mod Tabellenlänge, oder allgemein: h(k) = k mod m mit h für Hashfunktion, k für key und m für Tabellenlänge. Wir benutzen also diese Hashfunktion, um jedem Schlüssel einen Index (hier eine Zahl zwischen 0 und 9999) in einer verkleinerten Tabelle (der sogenannten Hashtabelle) zuzuordnen und damit eine Menge Platz zu sparen. Leider kann es allerdings passieren, dass in ungünstigen Fällen zwei oder mehr Schlüssel (Telefonnummern) auf denselben Index in der Hashtabelle abgebildet werden, z.B. ist 01063852 mod 10000 = 08153852 mod 10000 = 3852. Wie solche Kollisionen behandelt werden können, wird im übernächsten Abschnitt diskutiert. 117 8.2 Allgemeine Definitionen Formal gesehen ist Hashing ein abstrakter Datentyp, der die Operationen insert, delete und search auf (dynamischen) Mengen effizient unterstützt. " " " " " " " " " " " " " " " " " " U " " " " (Universum der Schlüssel) " # # # " " " !!!!!! !! ! ! ! ! !! ! ! ! ! " ! !!!! ! " 7 # ! ! ! !"! ! ! ! # ! ! ! 4 ! ! " ! ! ! ! ! !!! ! " !!!! !!! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !! !!! !!! ! ! ! ! ! ! ! ! # ! ! ! ! ! ! ! " ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! " ! ! ! ! !!! ! ! ! ! ! ! ! ! !!!!!!!!!!!! !!! !!!!! !! ! ! ! !! ! ! ! ! ! ! ! !! ! ! ! ! ! ! ! " !!!!! !!! " ! ! ! ! ! ! ! !! ! ! !!!! ! ! ! ! ! ! ! ! ! ! 1 ! ! ! ! ! ! ! ! ! ! !!! !!! !!!!!! " " ! !!! !! !!!! K ! !!! ! !!!! !! !!!!!!!!!! ! ! ! " ! ! ! ! ! ! !!! !! ! (Aktuelle Schlüssel) ! " ! ! ! ! ! ! ! ! ! ! ! ! " !!!!!!!!!! !! !!!! ! !! " ! !!!! !! !!!!!! !! ! ! ! ! ! ! ! ! ! ! ! !! ! ! ! ! !! ! ! ! ! ! ! ! ! ! ! ! ! " ! ! ! ! # !!! " !! ! !!!!!!!!!!!! !!!!!!!!!!!! ! !!!! ! ! ! !!! # ! " 2 !!! 3!! !!!!!!!!!!!! !!!!!!!!!!!! !!!!!!!!!! !!! " ! " ! ! ! ! ! ! ! ! ! ! ! ! ! ! " !! !!! !!! !!!!!! !!!!!!!!!! " !! ! !! ! ! " !!!!!!!!!! !!!!!! !! # ! ! ! ! !!! 5 ! " ! ! ! " ! ! ! !! ! !! !!!!! " " ! ! ! ! !! ! ! !!!!! # ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! " ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! " !! ! ! ! ! !!!!!!!!!!!!!!!!!!!!!!!!!!!"!!!!!!!!!! !!! ! ! 8 ! !!! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !! ! " !!! ! ! ! ! !! !! !! ! ! " ! !! ! ! " !! ! ! ! " ! !! ! ! ! ! " ! !! ! ! !!! ! ! ! ! ! " " ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !! ! " " " " " " " " " " " " " " " " " " " 9 0 6 .... ............. ............. ............. ................................ ............. ......................... . . . . . . . . . . . . ........... ....... 0 .... ............. ............. ............. .............................. ............ ......................... . . . . . . . . . . . . ............ ......... 1 " 2 3 .... ............. ............. ............. .................................. .... ................ ............. ............. ..... ............. 4 5 .... ............. ............. ............. .................................... .... ................ ............. ............. ..... ............. 6 .... ............. ............. ............. .................................... .... ................ ............. ............. ..... ............. 7 8 .... ............. ............. ............. .................................... ... ............... ............. ............. . . . . . . . . . . . . ...... .. 9 T Abbildung 8.1: Direkte Adressierung Hashing ist im Durchschnitt sehr effizient – unter vernünftigen Bedingungen werden obige Operationen in O(1) Zeit ausgeführt (im worst-case kann search O(n) Zeit benötigen). In unserer Darstellung folgen wir Cormen et al. [2]. Zur Vereinfachung nehmen wir an, dass die Daten keine weiteren Komponenten enthalten (d.h. mit den Schlüsseln übereinstimmen). Wenn die Menge U aller Schlüssel relativ klein ist, können wir sie injektiv auf ein Feld abbilden; dies nennt man direkte Adressierung (Abbildung ??). Ist die Menge U aller Schlüssel aber sehr groß (wie im obigen Beispiel des PizzaServices), so können wir nicht mehr direkt adressieren. Unter der Voraussetzung, dass die Menge K aller Schlüssel, die tatsächlich gespeichert werden, relativ klein ist gegenüber U, kann man die Schüssel effizient in einer Hashtabelle abspeichern. Dazu verwendet man allgemein eine Hashfunktion h : U → {0, 1, . . . , m − 1}, 118 die Schlüssel abbildet auf Werte zwischen 0 und m − 1 (dabei ist m die Größe der Hashtabelle). Dies illustriert Abbildung ??. " " " " " " " " " " " " " " " " .... ............. ............. ............. .................................... ..... ................. ............. ............. ............. .... " ! ! ! ! !! !!!! !! !!!! ! ! ! ! ! ! ! ! " !!!! " " ! !!!!!! !!!!!! !! ! ! ! " ! ! !! !!!!!! " " " !!!!!!!!!!! ! ! ! !! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !!!!! !!!! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! " ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! " ! ! ! ! ! ! ! ! ! ! ! !! !! ! ! ! ! ! ! ! ! !! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! " !! ! ! ! !! ! ! " !!!!! ! ! #!!!!!!!! !! ! ! ! ! ! ! ! ! ! ! ! ! ! k1 " " K ! !!! !!! !!!!! !! !! ! ! !!!!!! !!!! !!!! ! " ! ! ! " ! ! ! !!! !!! ! ! !!!! (Aktuelle Schlüssel) " !!!!!! !!! !! ! " !!!!!!! !!!!!!!!!!! ! ! ! ! ! ! ! !!!!! ! ! ! ! !!!!!!!!!!!! !!!!!!!!!!!! !!"!!! !!!!!!!! !!!!!!!!!! ! !!!!!!! ! ! " ! ! ! # ! !! ! ! !!! k4 !!!!!"!!!!! !!!!!!! !!!!!!!!! !!!! " !!!!!!!!!! !!!!! !!! !!!!!!!!!!!!!!! !!!! " ! ! ! ! ! ! ! !!! ! ! ! ! ! ! ! !!!!!! " !!! !!! !!! !!!!!!! !!!!!!!!! !!!!!! " !! ! !! ! ! !!!!!!!!! !!! ! ! !!!!!!!!!!!! !!! ! ! ! ! ! ! ! " ! # ! ! ! ! ! ! !!! ! k " !!!!!! ! ! ! 2 ! ! ! ! ! ! ! ! ! ! # ! ! " ! !! ! ! " !!!!!!!!!!! !!!!!!!!!!! !!!!! ! !! ! !! k5 " !!!!! ! ! !!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!"!!!!!!!!!!! ! ! ! ! ! !!! ! " ! ! !!! !!!!!!!!!!!!! !!!!!!!!!!!! " ! !!! !!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! ! ! " ! ! # ! ! " ! ! ! ! ! ! ! !! " k 3 !! ! ! ! " !! ! ! ! ! " ! ! ! " ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !! " " " " " " " " " " " " " " " " " " " " " U (Universum der Schlüssel) 0 .... ............. ............. ............. .................................... .... ................ ............. ............. ..... ............. " h(k1 ) h(k4 ) .... ............. ............. ............. .................................... ............. ......................... . . . . . . . . . . . . ......... ..... h(k2 ) = h(k5 ) ............. .... ............. ............. .............................. ............. ......................... . . . . . . . . . . . . ............ ........ h(k3 ) .... ............. ............. ............. .................................... ..... ................. ............. ............. ............. .... ............. ..... ............. ............. ................................. ..... ................. ............. ............. ............. .... T m−1 Abbildung 8.2: Eine Hashfunktion h weist Schlüsseln ihren Platz in der Hashtabelle T zu. Beispiel 8.2.1 Eine typische Hashfunktion h für U = N ist h(k) = k mod m. Dabei sollte m eine Primzahl sein, die nicht (zu) nahe an Potenzen von 2 liegt. Der Grund dafür wird in Cormen et al. [2], S. 228, erläutert. Da Hashfunktionen nicht injektiv sind, tritt das Problem der Kollision auf: zwei Schlüsseln wird der gleiche Platz in der Hashtabelle zugewiesen. Kollisionen sind natürlich unvermeidbar, jedoch wird eine “gute” Hashfunktion h die Anzahl der Kollisionen gering halten. D.h. h muss die Schlüssel gleichmäßig auf die Hashtabelle verteilen. Außerdem sollte h einfach zu berechnen sein. In Cormen et al. [2] findet man weitere Erläuterungen zum Thema Hashfunktionen. 119 8.3 Strategien zur Behandlung von Kollisionen 8.3.1 Direkte Verkettung Man kann Kollisionen auflösen, indem jeder Tabellenplatz einen Zeiger auf eine verkettete Liste enthält. Schlüssel mit demselben Hashwert werden in dieselbe Liste eingetragen (Abbildung ??). """"""""""""""""""""" """"""""""""""" """""""""""""""""""""""""""""""" """"""""""""""""""""""" """"" " " " !!!!!!!!!! " " " " "" "!"!"!!!!!!!!!!!!!!!!! !!!!!!!! !! ! ! ! ! ! ! ! ! "" """ ! ! """ !!!!!!!!!!!! """ !!!!!!!!!!!!!!!! !! """ !!!!!! !!!!!!!!!!!!! """ ! ! ! ! ! ! ! # ! k4 " !# !!!!!!!!!!!!!!!!!!!!!!!!!!!!"""""!!!!! """ !!!!!!!!!!!!!!!!!!!!!!!!! k2 " !!!! !!!!!!! !!!!!!!!!!!!!!!!!"""! !!!!!!!!!!!!! !!! # !!!!!!!!!!!!!!!!!!!!!!!! !!!!!!! "" " k5 "" " " " " """""""""" """"""" """" """"""" " """" " " " " """""" """ """" """"""" """""""""""""""""""""""" $ ....... .......... $ ....... .......... k4 .... ............. ............. ............. .................................. ............. .......................... . . . . . . . . . . . . ......... ...... k2 ....... .......... k5 .... ............. ............. ............. ............................ ..................................... . . . . . . . . . . . . ............. ......... Abbildung 8.3: Direkte Verkettung Damit ergeben sich folgende worst-case Laufzeiten: insert: O(1) (neues Element vor die Liste hängen) search: O(n) (n = Länge der Liste) delete: O(1) (vorausgesetzt, man verwendet doppelt verkettete Listen und hat das Element schon gefunden) 8.3.2 Open Hashing Beim Open Hashing werden alle Einträge in der Hashtabelle gehalten. Ist eine Komponente der Tabelle schon belegt, so wird ein freier Platz für einen weiteren Eintrag gesucht. Es gibt u.a. folgende Strategien zur Suche eines freien Platzes: 1. Lineare Verschiebung Falls h(k) bereits durch einen anderen Schlüssel besetzt ist, wird versucht, k in den Adressen h(k) + 1, h(k) + 2, . . . unterzubringen (Abbildung ??). Präziser gesagt, wird folgende Hashfunktion verwendet: ! " h! (k, i) = h(k) + i mod m 120 mit i = 0, 1, 2, . . . , m − 1. """"""""""""""""""""" """""""""""""" """""""""""""""""""""""""""""""" """"""""""""""""""""""" " """"" " " " !!!!!!!!!! " " " " "" "!"!"!!!!!!!!!!!!!!!!! !!!!!!!! !! ! ! ! ! ! ! ! ! "" """ ! ! """ !!!!!!!!!!!! """ !!!!!!!!!!!!!!!! !! """ !!!!!!!!!!!!!!!!!!! """ ! ! ! ! ! ! ! ! # k4 " !# !!!!!!!!!!!!!!!!!!!!!!!!!!!!"""""!!!!! """ !!!!!!!!!!!!!!!!!!!!!!!!! k2 " !!!!!!! ! ! ! """ ! ! !!!!!!!!!!!!!! ! # ! ! ! ! ! ! ! """ !!!! !! " !! k5 "" !!!!!! !! ! ! !! ! !! !!!!!!!!!! " " " " """""""""" """"""" """" """"""" " """" " " " " """""" """ """" """"""" """"""""""""""""""""""" .... ............. ............. ............. ................................... ............. ......................... . . . . . . . . . . . . ......... ..... Abbildung 8.4: Open Hashing mit linearer Verschiebung Unter der Annahme, dass jeder Tabellenplatz entweder einen Schlüssel oder NIL enthält, wenn der Tabellenplatz leer ist1 , können die Operationen insert und search wie folgt implementiert werden: Hash-Insert(T, k) 1 i←0 2 repeat 3 j ← h! (k, i) 4 if T [j] = NIL then 5 T [j] ← k 6 return j 7 i←i+1 8 until i = m 9 error “hash table overflow” Hash-Search(T, k) 1 i←0 2 repeat 3 j ← h! (k, i) 4 if T [j] = k then 5 return j 6 i←i+1 7 until T [j] = NIL or i = m 8 error NIL 2. Quadratische Verschiebung Wenn die Schlüssel natürliche Zahlen sind, dann können wir z.B. den Wert −1 zur Markierung eines leeren Tabellenplatzes benutzen. 1 121 Es wird die Hashfunktion h! (k, i) = (h(k) + c1 i + c2 i2 ) mod m mit i = 0, 1, 2, . . . , m − 1 verwendet. Dabei sind c1 , c2 ∈ N und c2 %= 0 geeignete Konstanten (s. Cormen et al. [2]). 3. Double Hashing Die Hashfunktion h! wird definiert durch h! (k, i) = (h1 (k) + i · h2 (k)) mod m mit i = 0, 1, 2, . . . , m − 1, wobei h1 und h2 Hashfunktionen sind. Die Verschiebung wird dabei durch eine zweite Hashfunktion realisiert. D.h. es wird zweimal, also doppelt, gehasht. Beispiel 8.3.1 Wir illustrieren Open Hashing mit linearer Verschiebung an einem Beispiel. Unsere Hashtabelle hat nur fünf Plätze und die Hashfunktion sei h(k) = k mod 5. Die Werte 0 und 5 seien bereits eingetragen. 0 1 2 0 1 0 insert 11 ! 1 2 0 1 11 0 insert 10 ! 1 2 3 3 3 4 4 4 0 1 11 10 0 delete 1 ! 1 2 3 0 d 11 10 search 10 ! 4 Man erkennt: Gelöschte Felder müssen markiert werden, so dass ein Suchalgorithmus nicht abbricht, obwohl das Element doch in der Liste gewesen wäre. Natürlich kann in die gelöschten Felder wieder etwas eingefügt werden. Dieses Problem muss in der obigen Implementierung zusätzlich berücksichtigt werden. n Der Ladefaktor α für eine Hashtabelle T ist definiert als m , wobei n die Anzahl der gespeicherten Schlüssel und m die Kapazität der Tabelle sind. Theoretische Untersuchungen und praktische Messungen haben ergeben, dass der Ladefaktor einer Hashtabelle den Wert 0.8 nicht überschreiten sollte (d.h. die Hashtabelle darf höchstens zu 80% gefüllt werden). Ist der Ladefaktor ≤ 0.8, so treten beim Suchen im Durchschnitt ≤ 3 Kollisionen auf. Bei einem höheren Ladefaktor steigt die Zahl der Kollisionen rasch an. 8.4 Die Klasse Hashtable in Java Die Klasse java.util.Hashtable implementiert alle Methoden der abstrakten Klasse java.util.Dictionary (vgl. Aufgabe ??). Außerdem enthält Hashtable 122 noch folgende Methoden2 : public synchronized boolean containsKey(Object key) Es wird true zurückgegeben gdw. die Hashtabelle ein Element unter key verzeichnet hat. public synchronized boolean contains(Object element) Gdw. das angegebene element ein Element der Hashtabelle ist, wird true zurückgegeben. Diese Operation ist teurer als die containsKey-Methode, da Hashtabellen nur beim Suchen nach Schlüsseln effizient sind. public synchronized void clear() Alle Schlüssel in der Hashtabelle werden gelöscht. Wenn es keine Referenzen mehr auf die Elemente gibt, werden sie vom Garbage-Collector aus dem Speicher entfernt. public synchronized Object clone() Es wird ein Klon der Hashtabelle erzeugt. Die Elemente und Schlüssel selbst werden aber nicht geklont. Ein Hashtabellenobjekt wächst automatisch, wenn es zu stark belegt wird. Es ist zu stark belegt, wenn der Ladefaktor der Tabelle überschritten wird. Wenn eine Hashtabelle wächst, wählt sie eine neue Kapazität, die ungefähr das doppelte der aktuellen beträgt. Das Wählen einer Primzahl als Kapazität ist wesentlich für die Leistung, daher wird das Hashtabellenobjekt eine Kapazitätsangabe auf eine nahegelegene Primzahl anpassen. Die anfängliche Kapazität und der Ladefaktor kann von den Konstruktoren der Klasse Hashtable gesetzt werden: public Hashtable() Es wird eine neue, leere Hashtabelle mit einer voreingestellten Anfangskapazität von 11 und einem Ladefaktor von 0.75 erzeugt. public Hashtable(int initialCapacity) Eine neue, leere Hashtabelle mit der Anfangskapazität initialCapacity und dem Ladefaktor 0.75 wird generiert. public Hashtable(int initialCapacity, float loadFactor) Es wird eine neue, leere Hastabelle erzeugt, die eine Anfangskapazität der Größe initialCapacity und einen Ladefaktor von loadFactor besitzt. Der Ladefaktor ist eine Zahl zwischen 0.0 und 1.0 und definiert den Beginn eines rehashings der Tabelle in eine größere. Das Voranstellen des Schlüsselworts synchronized bewirkt, dass eine Methode nicht in mehreren nebenläufigen Prozessen (threads) gleichzeitig mehrfach ausgeführt wird. Ansonsten könnte es zu Inkonsistenzen in der Hashtabelle kommen. 2 123 Beispiel 8.4.1 Um die Benutzung der Klasse Hashtable als Wörterbuch (Dictionary) zu demonstrieren, entwerfen wir zunächst eine Klasse Pair zur Speicherung von Name-Wert Paaren. Namen sind vom Typ String. Werte können beliebigen Typ haben, deshalb wird der Wert in einer Variable vom Typ Object abgelegt. class Pair { private String name; private Object value; public Pair(String name, Object value) { this.name = name; this.value = value; } public String name() { return name; } public Object value() { return value; } public Object value(Object newValue) { Object oldValue = value; value = newValue; return oldValue; } } Der Name ist nur lesbar, denn er soll als Schlüssel in einer Hashtabelle benutzt werden. Könnte das Datenfeld für den Namen von außerhalb der Klasse modifiziert werden, so könnte der zugehörige Wert verlorengehen: Er würde immer noch unter dem alten Namen eingeordnet sein und nicht unter dem modifizierten Namen. Der Wert hingegen kann jederzeit verändert werden. Folgende Schnittstelle deklariert drei Methoden: eine, um einem Dic-Objekt ein neues Paar hinzuzufügen; eine, um herauszufinden, ob in einem Dic-Objekt bereits ein Paar mit gegebenem Namen enthalten ist; und eine, um ein Paar aus einem Dic-Objekt zu löschen. interface Dic { void add(Pair newPair); Pair find(String pairName); 124 Pair delete(String pairName); } Hier nun das Beispiel einer einfachen Implementierung von Dic, die die Hilfsklasse java.util.Hashtable verwendet: import java.util.Hashtable; class DicImpl implements Dic { protected Hashtable pairTable = new Hashtable(); public void add(Pair newPair) { pairTable.put(newPair.name(), newPair); } public Pair find(String name) { return (Pair) pairTable.get(name); } public Pair delete(String name) { return (Pair) pairTable.remove(name); } } Der Initialisierer für pairTable erzeugt ein Hashtable-Objekt, um Paare zu speichern. Die Klasse Hashtable erledigt die meiste anfallende Arbeit. Sie verwendet die Methode hashCode des Objekts, um jedes ihr als Schlüssel übergebene Objekt einzuordnen. Wir brauchen keine explizite Hashfunktion bereitzustellen, denn String enthält schon eine geeignete hashCode-Implementierung. Kommt ein neues Paar hinzu, wird das Pair-Objekt in einer Hashtabelle unter seinem Namen gespeichert. Wir können dann einfach die Hashtabelle benutzen, um Paare über deren Namen zu finden und zu entfernen. 8.5 Aufgaben Aufgabe 8.5.1 Implementieren Sie in Java das Verfahren zum Open-Hashing. Beschränken Sie sich dabei auf eine Methode hashinsert zum Einfügen in die Hashtabelle. Verwenden Sie die primäre Hashfunktion h(k) = k mod m und die folgenden drei Verfahren zur Kollisionsbehandlung: (1) lineare Verschiebung; 125 (2) quadratische Verschiebung mit c1 = 1 und c2 = 3; (3) double hashing mit h1 (k) = k mod m und h2 (k) = 1 + (k mod (m − 1)). Verwenden Sie nun Ihre Implementierung, um die Schlüssel 10, 22, 31, 4, 15, 28, 17, 88, 59 in eine Hashtabelle der Größe m = 11 einzutragen. Geben Sie die Hashtabelle nach dem Einfügen jedes Schlüssels an. Aufgabe 8.5.2 Sei m die Größe einer Hashtabelle. Wir definieren die Hashfunktion hstring, die für einen String s einen ganzzahligen Wert hstring(m, s) liefert, wie folgt: hstring(m, s) = hstring! (0, s) mod m. Dabei ist hstring! in Haskell-ähnlicher Notation wie folgt definiert: und hstring(i, []) = i hstring! (i, a : w) = hstring! (i · 31 + ord(a), w) wobei ord eine Abbildung ist, die für ein Zeichen aus dem ASCII-Alphabet eine eindeutige ganze Zahl zwischen 0 und 255 liefert, nämlich ihren ASCII-Wert. 1. Implementieren Sie die Hashfunktion hstring in Java. hstring! sollte dabei iterativ implementiert werden. Die Summen zur Berechnung des Hashwertes wachsen sehr schnell. Verwenden Sie daher long-Werte zur Summation, um einen Überlauf zu vermeiden. 2. Erstellen Sie sich eine Datei strings mit 1000 ASCII-Zufallssequenzen der Länge 8. Wenden Sie hstring auf jede dieser Sequenzen an. Wählen Sie dabei nacheinander in verschiedenen Programmläufen m ∈ {67, 101, 113, 197}. 3. Eine gute Hashfunktion wird die 1000 Strings gleichmäßig auf die m Einträge in der Hashtabelle abbilden, d.h. im Idealfall werden etwa 1000/m Strings auf jeden Eintrag abgebildet. Bestimmen Sie die Güte der Hashfunktion hstring, indem Sie g(m, hstring) = m−1 1 # |1000/m − occ(i)| m i=1 berechnen. Dabei ist occ(i) die Anzahl der Strings w mit hstring(m, w) = i. Welcher Wert ergibt sich für die Güte g(m, hstring) für m ∈ {67, 101, 113, 197}? 126