Kapitel 8 Hashing

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