Algorithmen und Datenstrukturen
Werner Struckmann
Wintersemester 2005/06
7. Mengen, Verzeichnisse und Hash-Verfahren
7.1 Mengen
7.2 Verzeichnisse 7.3 Hashverfahren
Mengen
Unter einer Menge verstehen wir jede Zusammenfassung M von bestimmten, wohlunterschiedenen Objekten m unserer
Anschauung oder unseres Denkens (welche die Elemente von M genannt werden) zu einem Ganzen.
Es kommt es nicht darauf an, wie oft und in welcher Reihenfolge die Elemente einer Menge aufgeführt werden. Es gilt
{
2, 4,6, 8} = {
6, 8, 2,4, 4,6,8} = {
8, 6,2,4}
.Für Mengen sind unter anderem die folgenden Operationen definiert:
◮ Elementbeziehung m
∈
MADT Menge
type
: Set(
T) import
: Booloperators
:∅
: →
Setempty
? :
Set→
Bool is_in:
Set×
T→
Bool insert:
Set×
T→
Set delete:
Set×
T→
Set union:
Set×
Set→
Setintersection
:
Set×
Set→
Set difference:
Set×
Set→
Set· · ·
axioms ∀
s∈
Set,∀
x,y∈
T· · ·
7.1 Mengen 7-2
Geordnete Mengen
◮ Die Elemente einer Menge können durch eine Ordnungsrelation angeordnet sein.
◮ Eine Ordnungsrelation ist eine reflexive, transitive und antisymmetrische Relation.
◮ Wenn je zwei Elemente vergleichbar sind, heißt die Ordnungsrelation linear oder total.
ADT Geordnete Menge
type
: OrderedSet(
T) import
: Booloperators
:∅
: →
OrderedSetempty
? :
OrderedSet→
Bool is_in:
OrderedSet×
T→
Boolinsert
:
OrderedSet×
T→
OrderedSet delete:
OrderedSet×
T→
OrderedSetunion
:
OrderedSet×
OrderedSet→
OrderedSetintersection
:
OrderedSet×
OrderedSet→
OrderedSet difference:
OrderedSet×
OrderedSet→
OrderedSet min:
OrderedSet→
Tmax
:
OrderedSet→
T· · ·
axioms ∀
s∈
OrderedSet,∀
x,y∈
T· · ·
7.1 Mengen 7-4
Mengenimplementierungen
Mengen können auf verschiedene Weisen realisiert werden. Einige davon besprechen wir in diesem Kapitel. Wir befassen uns nur mit endlichen Mengen.
Ungeordnete Mengen:
1. Bitfelder 2. Listen 3. Felder
Geordnete Mengen:
Kopieren von Objekten
◮ Häufig besteht der Bedarf, ein Objekt zu kopieren, d. h. ein zweites Objekt zur Verfügung zu haben, das dem ersten vollständig gleicht.
◮ In objektorientierten Programmiersprachen ist eine Anweisung der Form a
←
b hierfür oft nicht geeignet.◮ Es wird der Variablen a lediglich eine Referenz auf das
ursprüngliche Mengenobjekt zugewiesen. Manipulationen des durch a referenzierten Objekts verändern so zugleich das
durch b bestimmte Objekt.
◮ Man unterscheidet zwischen so genannten flachen und tiefen Kopien.
◮ Programmtechnische Details sind nicht Gegenstand dieser Vorlesung.
7.1 Mengen 7-6
Implementierung durch Bitfelder
Dieses Verfahren ist geeignet, Mengen A zu realisieren, die
Teilmenge einer kleinen endlichen Grundmenge G
= {
g1, . . . , gn}
sind.◮ Es wird ein Bitfeld A mit
|
G| =
n Bits vereinbart.◮ Es gilt gi
∈
A⇔
A[
i] =
1. Wir vereinbaren: 0 , false, 1 , true.g1 1 g1 ∈ A g2 1 g2 ∈ A g3 0 g3 6∈ A
Implementierung durch Bitfelder
proc empty() begin var A: BitSet;
var i: int;
for i ← 1 to n do A[i] ← false;
od;
return A;
end
proc isIn(A: BitSet, i: int): bool begin return A[i];
end
7.1 Mengen 7-8
Implementierung durch Bitfelder
func intersection(B, C: BitSet): Bitset begin var A: BitSet;
var i: int;
for i ← 1 to n do
A[i] ← B[i] && C[i];
od;
return A;
end
func union(B, C: BitSet): Bitset begin var A: BitSet;
var i: int;
for i ← 1 to n do
A[i] ← B[i] || C[i];
Implementierung durch Bitfelder
func difference(B, C: BitSet): BitSet begin var A: BitSet;
var i: int;
for i ← 1 to n do
A[i] ← B[i] && ! C[i];
od;
return A;
end
7.1 Mengen 7-10
Implementierung durch Bitfelder
Die Laufzeit von
isIn
,insert
unddelete
liegt in O(
1)
. Die Laufzeiten der Operationenunion
,intersection
unddifference
sind abhängig von der Kardinalität der Grundmenge und liegen in O(
n)
.Vorteile:
◮ Für kleine Grundmengen ist die Implementierung sehr effizient.
◮ Die Operationen besitzen teilweise eine konstante Laufzeit.
Nachteile:
Implementierung durch Listen
◮ Die Elemente der Menge werden in einer verketteten Liste gespeichert.
◮ Die Reihenfolge der Elemente spielt keine Rolle.
◮ Elemente dürfen nicht mehrfach vorkommen.
◮ Die Anzahl der Elemente der Menge braucht nicht beschränkt zu sein.
◮ Der Platzbedarf richtet sich nach der Größe der Menge, nicht nach der Größe der Grundmenge.
◮ Die Sortierung innerhalb der Liste kann zusätzliche Informationen enthalten (Beispiel: Reihenfolge des Einfügens).
7.1 Mengen 7-12
Implementierung durch Listen
◮ Varianten: einfach und mehrfach verkettete Listen (siehe Abschnitt über Listen).
◮ Pro Element wird mehr Speicherplatz verbraucht als bei der Bitfeldimplementierung.
◮ Operationen sind aufwändiger als bei der Bitfeldimplementierung.
◮ Im Folgenden: ausgewählte Algorithmen (diesmal aus objektorientierter Sicht).
Implementierung durch Listen
func copy(): ListSet begin var result: ListSet;
var tmp: T;
foreach tmp ← L.elements() do result.insert(tmp);
od;
return result;
end
7.1 Mengen 7-14
Implementierung durch Listen
func equals(other: Set): bool begin var tmp: T;
if size
,other.size then return false;
foreach tmp ← L.elements() do if! other.isIn(tmp) then
return false;
od;
return true;
end
Implementierung durch Listen
func union(other: Set): ListSet begin var result: ListSet;
var tmp: T;
result ← other.copy();
foreach tmp ← L.elements() do result.insert(tmp);
od;
return result;
end
7.1 Mengen 7-16
Implementierung durch Listen
func intersection(other: Set): ListSet begin var result: ListSet;
var tmp: T;
foreach tmp ← L.elements() do if other.isIn(tmp) then
result.insert(tmp);
od;
return result;
end
Implementierung durch Listen
func difference(other: Set): ListSet begin var result: ListSet;
var tmp: T;
foreach tmp ← L.elements() do if! other.isIn(tmp) then
result.insert(tmp);
od;
return result;
end
7.1 Mengen 7-18
Vergleich
Operation Liste Bitfeld isIn O
( |
s| )
O(
1)
insert O( |
s| )
O(
1)
delete O( |
s| )
O(
1)
copy O( |
s| )
O( |
G| )
union O( |
t| + |
t| · |
s| )
O( |
G| )
difference O( |
s| · |
t| )
O( |
G| )
intersection O( |
s| · |
t| )
O( |
G| )
equal O( |
s| · |
t| )
O( |
G| )
Implementierung durch Felder
◮ Die Elemente der Menge s werden in einem Feld A fester Länge gespeichert. Dabei belegen die Mengenelemente die Felder 1 bis
|
s|
.◮ Der Index des höchsten belegten Feldes wird in einer Variablen size
= |
s|
gespeichert.◮ Die Größe der Menge ist (in vielen Sprachen) durch die Feldgröße beschränkt.
◮ Es ist keine dynamische Speicherverwaltung notwendig.
◮ Die Sortierung innerhalb des Felds kann zusätzliche
Informationen enthalten (zum Beispiel die Reihenfolge des Einfügens).
7.1 Mengen 7-20
Implementierung durch Felder
func isIn(t: T): bool begin var i: int;
for i ← 1 to size do if A[i] = t then
return true;
od;
return false;
end
Implementierung durch Felder
proc insert(t: T) begin if! isIn(t) then
if size = A.size then
error(“maximale Größe erreicht”);
A[size] ← t;
size ← size + 1;
fi;
end
func copy(): ArraySet begin var result: ArraySet;
var i: int;
for i ← 1 to size do result.A[i] ← A[i];
od;
result.size ← size;
return result;
end
7.1 Mengen 7-22
Implementierung durch Felder
◮ Die Laufzeit fast aller Operationen ist denen der
Listenimplementierung ähnlich. Bei der Operation delete müssen evtl. Bereiche des Felds verschoben werden.
◮ Pro Element ist der Speicherverbrauch geringer als bei Implementation durch verkettete Listen.
◮ Allerdings verbrauchen auch nicht belegte Felder Speicherplatz.
◮ Der Speicherplatzbedarf ist unabhängig von der tatsächlichen Größe der Menge.
◮ Der Speicherplatzbedarf ist bei geringem Füllstand unnötig groß.
Implementierung geordneter Mengen durch Listen
◮ Die Grundidee folgt der Implementierung von (ungeordneten) Mengen durch Listen.
◮ Allerdings sind die Elemente in der Liste jetzt entsprechend der Ordnungsrelation angeordnet.
◮ Einige Operationen lassen sich dadurch beschleunigen.
7.1 Mengen 7-24
Implementierung geordneter Mengen durch Listen
Als Beispiel betrachten wir die Durchschnittsbildung. Jede Liste wird mit einem Zeiger durchlaufen.
1. Setze zv auf den Anfang der ersten und zw auf den Anfang der zweiten Liste.
2. Schreite mit zw solange fort, bis sein Wert w größer oder gleich dem Wert v von zv ist.
3. Fallunterscheidung:
3.1 w = v: Nimm v in die Zielmenge auf und setze zv weiter.
3.2 w > v: Setze zv weiter.
4. Solange keine Zeiger am Ende angekommen ist, fahre mit 2 fort.
Implementierung geordneter Mengen durch Listen
func intersection(other: SortListSet): SortListSet begin
var result: SortListSet;
var i, j: <Referenz auf ListElement>;
i ← L.head;
j ← other.head;
while i <gültige Referenz> && j <gült. Ref.> do if i.wert = j.wert then
result.insert(j.wert);
if i.wert <= j.wert then i ← i.next;
else
j ← j.next;
fi;
od;
return result;
end
7.1 Mengen 7-26
Implementierung geordneter Mengen durch Listen
◮ Die Operation insert muss die Ordnung der Elemente beachten.
◮ Die Operationen isIn und delete müssen im Misserfolgsfall nicht bis zum Ende der Liste suchen. Dies führt zwar nicht zu einer Verbesserung der Komplexitätsklasse, dennoch
bedeutet es eine Effizienzsteigerung.
◮ Bei einigen Operationen (zum Beispiel union, intersection, difference) kann durch die Ordnung der Liste eine
Laufzeitverbesserung erreicht werden, die zu einer anderen Laufzeitklasse führt.
Implementierung geordneter Mengen durch Bäume
◮ Anstatt die Elemente in einer Liste abzulegen, kann auch ein binärer Suchbaum verwendet werden.
◮ Für die Operationen intersection, union, difference kann analog zur Verwendung von Listen bei geordneten Mengen effizienzsteigernd das gleichzeitige Durchwandern beider Bäume durchgeführt werden.
◮ Insbesondere verbessern sich die Laufzeiten von isIn, insert und delete. Dies hängt vom verwendeten Baumtyp ab.
7.1 Mengen 7-28
Verzeichnisse
Es seien zwei (endliche) Mengen A und B gegeben.
◮ Ein Verzeichnis (Dictionary) ist eine Relation D
⊆
A×
B mit∀
a∈
A,b1, b2∈
B: (
a,b1) ∈
D∧ (
a, b2) ∈
D⇒
b1=
b2.◮ Mathematisch gesehen ist ein Verzeichnis eine partielle Abbildung von A nach B.
◮ Bezeichnungen:
A Schlüsselmenge, Definitionsmenge a
∈
A SchlüsselVerzeichnisse
◮ Verzeichnisse werden verwendet, um Schlüssel auf Datenobjekte abzubilden.
◮ Beispiele:
◮ Ergebnisliste einer Klausur: (Matrikelnummer, Note)
◮ Dateisystem: (Dateiname, Datenblockmenge)
◮ Da ein Verzeichnis als Menge von Paaren definiert ist, können die besprochenen Mengenimplementierungen verwendet
werden.
7.2 Verzeichnisse 7-30
Einige Operationen
keys
: P (
A×
B) → P (
A)
keys
(
D) = {
a∈
A| ∃
b∈
B.(
a, b) ∈
D}
delete:
A× P (
A×
B) → P (
A×
B)
delete
(
a, D) =
D\{ (
a,b) | ∃
b∈
B.(
a, b) ∈
D}
associate: (
A×
B) × P (
A×
B) → P (
A×
B)
associate((
a,b)
, D) =
delete(
a, D) ∪ { (
a, b) }
Einige Operationen
value
:
A× P (
A×
B) →
Bvalue
(
a,D) =
b falls∃
b∈
B.(
a,b) ∈
D, sonst undefiniert values: P (
A×
B) → P (
B)
values
(
D) = {
b∈
B| ∃
a∈
A.(
a,b) ∈
D}
size: P (
A×
B) →
N0size
(
D) = |
D|
7.2 Verzeichnisse 7-32
Verzeichnisse
Verzeichnis von Namen und Telefonnummern A Zeichenketten, B Zeichenketten, D = ∅
associate("Linus", "05313250", D) associate("Lena", "05315148", D)
associate("Johannes", "01727512", D)
Führt zu D = {("Linus", "05313250"), ("Lena", "05315148"), ("Johannes", "01727512")}.
keys(D) = {"Linus", "Lena", "Johannes"}
values(D) = {"05313250", "05315148", "01727512"}
value(Linus, D) = "05313250"
value(Peter, D) =
⊥
Hashverfahren - Einführung
◮ Bei der Listenimplementierung einer Menge erfolgt der Zugriff auf ein Element in der Zeit O
(
n)
.◮ Bitfeldimplementierungen benötigen nur die Zeit O
(
1)
, können jedoch sinnvoll nur kleine Mengen behandeln.◮ Idee: Die beiden Möglichkeiten werden kombiniert.
◮ Hashverfahren basieren auf Verzeichnissen D
⊆
A×
B◮ für deren Schlüsselmenge A = {0, . . . ,n − 1} ⊆ N0 gilt
◮ und deren Schlüssel berechnet werden mithilfe einer so genannten Hashfunktion h : B → A .
◮ Das Verzeichnis heißt Hashtabelle.
◮ Für die zu speichernde Menge M ist M
⊆
B.◮ Die Berechnung der Werte der Hashfunktion erfolgt in O
(
1)
Schritten.7.3 Hashverfahren 7-34
Einführung
Beispiel:
A
= {
0, . . . , 9}
, B=
N0,h
:
B→
A, h(
j) =
j mod 10 Die zu speichernde Menge sei M= {
35,67} ⊆
N0.Das Einfügen der Zahlen 35 und 67 führt wegen h
(
35) =
5 und h(
67) =
7 zu folgender Hashtabelle:Schl¨ussel Werte 0
1 2 3 4
5 35
6
7 67
8 9
Einführung
◮ Gegeben sei eine Nummerierung der Buchstaben (A=1, B=2, usw.)
◮ Hashfunktion für Vor- und Nachnamen:
h1: Addiere Nummer des ersten Buchstabens des Vornamens zur Nummer des ersten Buchstabens des Nachnamens.
h2: Addiere Nummer des zweiten Buchstabens des
Vornamens zur Nummer des zweiten Buchstabens des Nachnamens.
◮ Der Hashwert ergibt sich aus diesem Wert durch Restbildung modulo Feldgröße, zum Beispiel mod 10.
◮ Beispiel:
h1
(
„Andreas Rot“) = (
1+
18)
mod 10=
9 h2(
„Andreas Rot“) = (
14+
15)
mod 10=
97.3 Hashverfahren 7-36
Einführung
Ausgangsstruktur ist ein Feld, die Hashtabelle,
◮ dessen Indexmenge der Schlüsselmenge A entspricht und
◮ deren Felder, genannt Buckets, die Elemente der zu speichernden Menge aufnehmen.
Ein Element der Wertemenge wird mithilfe der Hashfunktion auf einen Index des Felds abgebildet und in diesem Feld gespeichert.
Ein Bucket kann im Allgemeinen mehrere Elemente aufnehmen.
Da die Wertemenge üblicherweise sehr groß ist, wird die Hashfunktion nicht injektiv sein.
Einführung
Zusammenfassung des Prinzips:
◮ Es ist eine Menge M
⊆
B zu speichern. B ist die Wertemenge.◮ Die Schlüsselmenge sei A
= {
0, . . . ,n−
1} ⊆
N0.◮ h
:
B→
A ist eine Hashfunktion.◮ Das Feld zur Speicherung der Elemente von Menge M ist die Hashtabelle R. A ist der Indexbereich von R.
∀
i∈
A.R[
i] = {
b∈
M|
h(
b) =
i} ⊆
B∀
i∈
A.Bi= {
b∈
B|
h(
b) =
i} =
h−1(
i) ⊆
B B=
n
[
i=0
Bi disjunkte Zerlegung von B
M
=
[ni=0
b
∈
B|
b∈
M,h(
b) =
i7.3 Hashverfahren 7-38
Einführung
Menge M
Wertemenge B
Bi Bj Bk
Bucket 0: M ∩B0 Bucket 1: M ∩B1 Bucket 2: M ∩B2
· · ·
Bucket n: M ∩Bn h
h
h
isIn
(
M, b)
für b∈
B:Kollisionen
Je mehr Elemente die Menge enthält, desto wahrscheinlicher ist, dass es zu einer Kollision kommt:
∃
b,b′∈
M.b , b′∧
h(
b) =
h(
b′)
. Das heißt, h ist in diesem Fall nicht injektiv.◮ Open Hashing: Größe der Buckets erhöhen (d. h. mehrere Elemente aufnehmen).
|
M|
ist dann unbegrenzt.◮ Closed Hashing: Auf einen anderen Bucket ausweichen (d. h.
maximal ein Element pro Bucket). Dann ist
|
M| ≤ |
R|
.7.3 Hashverfahren 7-40
Hashfunktionen
Hashfunktionen sollten surjektiv sein. Für B
⊆
N0 sind zwei Beispiele für Hashfunktionen wie folgt definiert.◮ Divisionsmethode: h
:
B→
A mith
(
j) =
j mod|
A|
◮ h(j) ist schnell zu berechnen.
◮ Vermieden werden sollte |A| = 2k, k ∈ N.
◮ Besser: |A| ist Primzahl nicht zu nahe an 2k, k ∈ N.
◮ Multiplikationsmethode: h
:
B→
A mith
(
j) = ⌊|
A| · (
j·
c mod 1) ⌋
, 0 < c < 1Open Hashing (with Chaining)
Feld R von Listen mit Indexmenge A
= {
0, 1, . . . ,n−
1}
. BeiKollisionen wird das neue Element b der Liste R
[
h(
b)]
am Kopf hinzugefügt.Beispiel:
M
= {
0,1, 10,11,30, 41, 49,51, 59}
,n=
10, h(
b) =
b mod n9
· · · 1 0
59 49
51 41 11 1
30 10 0
h−(9) ∩M = {59,49}
7.3 Hashverfahren 7-42
Open Hashing (with Chaining)
◮ Die Liste kann einfach oder doppelt verkettet sein.
◮ Einfügen, Löschen und Suchen eines Elements b erfordern
◮ das Berechnen des Hash-Wertes h(b) sowie
◮ das Ausführen der entsprechenden Listenfunktion (isIn(b), insert(b), delete(b)) für die Liste R[h(b)].
Da die Hashfunktion in O
(
1)
ausgeführt wird, dominiert somit die jeweilige Listenfunktion die Laufzeit der Operation.◮ Die Operationen union, intersection und difference können sukzessive für jede Liste durchgeführt werden.
◮ Ist auf B eine Ordnung definiert, bieten sich neben
Open Hashing (with Chaining)
Die Laufzeit der Operation isIn(b)
◮ beträgt im ungünstigsten Fall O
( |
M| )
. Dieser Fall tritt ein, wenn alle Elemente von M in dem Bucket R[
h(
b)]
liegen.◮ beträgt im günstigsten Fall O
(
1)
. Dieser Fall tritt für|
R[
h(
b)] | =
0 ein.Aussagekräftig ist daher nur der mittlere Fall. Es gilt:
Satz: Die Menge M sei durch „zufälliges Ziehen“ von Elementen
b1, . . . , bm aus B entstanden. Für m
= |
M|
ist die mittlere Laufzeitder Operation isIn()
T
(
m) =
O 1+
m|
A|
! .
7.3 Hashverfahren 7-44
Hashfunktionen
Bespiel 1: h(Name) = Anfangsbuchstabe des Namens
◮ Bei deutschen Nachnamen sind die Buckets ’S’ und ’E’ sehr voll, die Buckets ’C’, ’Y’, ’X’, ’Q’ dagegen sehr leer.
◮ Besser ist es, bei Namen bzw. Zeichenketten y
=
y1y2y3 . . .yz die folgende Hashfunktion zu verwenden.h
(
y) =
z
X
j=1
OrdnungCodeX
(
yi)
mod
( |
A| )
Hashfunktionen
Beispiel 2: Ein Compiler trägt die Bezeichner eines Programms in eine Hashtabelle ein.
M sei die Menge der Bezeichner,
|
M| =
m.Die Laufzeit für den Eintrag eines Bezeichers x ergibt sich durch:
Auswerten von h
(
x)
: 1Überprüfen von R
[
h(
x)]
:|
R[
h(
x)] | +
1evtl. Anfügen von x: 1
|
R[
h(
x)] | +
37.3 Hashverfahren 7-46
Hashfunktionen
Unter idealen Verhältnissen, also wenn nach dem Einfügen der Elemente b1,b2, . . . , bi−1
|
R[
h(
bi)] | =
i−
1|
A|
ist, dann gilt für die Laufzeit zum Einfügen von m Elementen:
T
(
m) =
Xmi=1
( |
R[
h(
bi)] | +
3) =
Xmi=1
i
−
1|
A| +
3!
=
m m+
12
|
A| −
1|
A| +
3!
Hashfunktionen
Wenn der Programmierer nur Bezeichner der Art „A00“ bis „A99“
verwendet werden, also M
= {
A00, A01, . . . ,A99}
ist, und außerdemh
(
y) =
z
X
j=1
OrdnungASCII
(
yi)
mod
( |
A| )
und A
= {
0, . . . ,99}
gelten, dann ist zwar|
A| = |
M|
, aber eswerden nur 19 der 100 Buckets benutzt.
7.3 Hashverfahren 7-48
Hashfunktionen
j0 j0
+
1 j0+
2· · ·
j0+
9 j0+
10· · ·
j0+
18 A00 A01 A02· · ·
A09A10 A11
· · ·
A18 A19A20
· · ·
A27 A28· · ·
· · · · · · · · · · · ·
A90 A91
· · ·
A99 Es gibt also einen Bucket mit 10 Elementen und je zwei mit9, 8, 7, . . . ,1 Elementen. Die Laufzeit für das Auffüllen eines
Buckets mit m Elementen beträgt
l
(
m) =
3+
4+
5+ · · · + (
m+
3)
, die Gesamtlaufzeit alsol
(
10) +
2 X9l
(
i) =
742 349 5=
T(
100)
Open Hashing (with Chaining)
Zusammenfassung:
◮ Das Problem der Kollisionen wird durch Bucketlisten gelöst.
◮ Die Menge M bzw. die Buckets können (im Prinzip) beliebig groß werden.
◮ Falls die Listen lang werden, steigt die Zugriffszeit.
7.3 Hashverfahren 7-50
Closed Hashing
Jedes Element von R fasst maximal ein Element aus B. Es wird bei einer Kollision eine Folge von Hashfunktionen verwendet:
h0
:
B→
A, h1:
B→
A, . . . , hn:
B→
A.Einfügen von b
∈
B: 1. i←
02. Fallunterscheidung bezüglich des Felds R
[
hi(
b)]
:◮ Feld ist frei: Füge b ein und beende den Algorithmus.
◮ Feld enthält bereits b: Beende den Algorithmus.
◮ Feld ist bereits durch ein Element b′ , b belegt (Kollision):
i ← i + 1.
Closed Hashing
Einfügen von 1,6, 5,3, die Indexmenge sei A
= {
0,1, 2,3}
und h0:
B→
A mit h0(
j) =
j mod|
A| =
j mod 4hi
:
B→
A mit hi(
j) =
h0(
j) +
i mod|
A| ;
i > 03 2 1 0
+1 h0(1) = 1
1 3 2 1 0
+6
h0(6) = 2 6 1 3 2 1 0
+5 h0(5) = 1
5 6 1 3 2 1 0
+3 h0(3) = 3
5 6 1 3
3 2 1 0
h1(5) = 2 h2(5) = 3
h1(3) = 0
Das Feld R ist nun voll, 5 und 3 konnten nicht am „eigentlich“
vorgesehenen Platz eingefügt werden.
7.3 Hashverfahren 7-52
Closed Hashing
Suchen von b
∈
B 1. i←
02. Fallunterscheidung bezüglich des Felds R
[
hi(
b)]
:◮ Feld ist frei: b ist nicht enthalten, beende den Algorithmus.
◮ Feld enthält b: b ist gefunden, beende den Algorithmus.
◮ Feld ist durch ein Element b′ , b belegt: i ← i + 1
3. Falls i < n wiederhole Schritt 2, andernfalls beende den Algorithmus (Das Element ist im vollständig gefüllten Feld nicht enthalten).
3 0
Closed Hashing
Löschen eines Elements:
Problem: Das gelöschte Feld muss von einem leeren Feld
unterscheidbar sein, da es auf dem Suchpfad eines Elements liegen kann.
Eine Lösung: Das Element bleibt gespeichert, das Feld wird dafür mit einem booleschen Flag versehen, dass angibt, ob es sich um ein aktives oder ein gelöschtes Element handelt. Notwendige
Änderungen:
◮ Der Algorithmus zum Suchen muss über als gelöscht markierte Felder hinweggehen.
◮ Der Algorithmus zum Einfügen kann Elemente auch auf als gelöscht markierten Feldern einfügen.
Folgerung: Die Laufzeit der Suche/des Löschens hängt nicht länger vom tatsächlichen Füllgrads des Felds ab.
7.3 Hashverfahren 7-54
Lineare Kollisionsbehandlung
Bei der linearen Kollisionsbehandlung werden Hashfunktionen der folgenden Form verwendet:
◮ h0
:
B→
A Hashfunktion,◮ hi
:
B→
A mit hi(
j) = (
h0(
j) +
i)
mod|
A|
für i > 0.Nach Bestimmung des ersten Hashwertes mit h0 berechnet h1 den nächsten Index durch Addition von 1. Bei Erreichen des letzten
Indexes wird 0 als nächster Index bestimmt.
Lineare Kollisionsbehandlung
Die lineare Kollisionsbehandlung kann zu einer Clusterbildung (primary clustering) führen. Je größer ein Cluster, desto höher die
Wahrscheinlichkeit, dass der Hashwert h0
(
z)
eines einzufügenden Elements z in dem Cluster liegt, wodurch dieser verlängert wird. Dies hat negative Auswirkungen auf die Laufzeit der Suche nach freien Feldern.h0(j) h1(j) h2(j) h3(j)
Cluster belegter Felder
7.3 Hashverfahren 7-56
Quadratische Kollisionsbehandlung
Bei der quadratischen Kollisionsbehandlung werden Hashfunktionen der folgenden Form verwendet:
◮ h0
:
B→
A Hashfunktion,◮ hi
:
B→
A mit hi(
j) = (
h0(
j) +
c1·
i+
c2·
i2)
mod|
A|
für i > 0 und Konstanten c1,c2 , 0.◮ Bezüglich des Clusterbildung ist die quadratische Kollisionsbehandlung günstiger als die lineare.
◮ Um die Hashtabelle vollständig auszunutzen, sind c1 und c2 geeignet zu wählen.
◮ Da sich auch hier die Hashwerte von hi zweier Elemente b,b′
∈
B mit h0(
b) =
h0(
b′)
im weiteren Verlauf gleichen,Doppeltes Hashing
Beim doppelten Hashing werden Hashfunktionen der folgenden Form verwendet:
◮ ha
:
B→
A,hb:
B→
A◮ hi
:
B→
A mit hi(
j) = (
ha(
j) +
i·
hb(
j))
für i≥
0◮ Eine der besten Methoden für Closed Hashing, da die Folgen von Hashwerten nahezu zufällig erscheinen.
◮ Der erste Hashwert eines Elements hängt von nur einer Hashfunktion ha ab.
◮ Der zweite dann auch von der zweiten Funktion hb.
◮ Dass beide Hashfunktionen für zwei unterschiedliche Elemente aus B die gleichen Resultate erzeugen, ist
(abhängig von der Wahl von ha und hb) unwahrscheinlich.
7.3 Hashverfahren 7-58
Doppeltes Hashing
hb
(
j)
und|
A|
sollten teilerfremd sein, damit die gesamte Indexmenge durchsucht wird.Möglichkeiten:
◮
|
A| =
2k und hb(
j)
produziert nur ungerade Zahlen.◮
|
A|
ist eine Primzahl und∀
j∈
B:
0 < hb(
j)
<|
A|
Beispiel (Cormen): ha
:
B→
A mit ha(
j) =
j mod|
A|
undhb
:
B→
A mit hb(
j) =
1+ (
j mod k)
mit k etwas kleiner als|
A|
. j=
123456,|
A| =
701, k=
700 führt zu ha(
j) =
80,hb(
j) =
257 Der erste untersuchte Feld hat den Index 80, danach wird jedesClosed Hashing
◮ Ist die Hashfunktion bzgl. B identisch gleichverteilt, so sind bei einem Füllfaktor α
=
|An| < 1 höchstens 11−α Versuche notwendig, um zu entscheiden, dass ein Element nicht in M enthalten ist.
◮ Ist die Hashfunktion bzgl. B identisch gleichverteilt, so sind bei einem Füllfaktor α
=
|An| < 1 höchstens 11−α Versuche notwendig, um ein Element einzufügen.
◮ Ist die Hashfunktion bzgl. B identisch gleichverteilt, so sind bei einem Füllfaktor α
=
n|A| < 1 höchstens α1 ln 11
−α Versuche notwendig, um ein Element zu finden. (Falls die
Wahrscheinlichkeit, mit der nach einem Element gesucht wird, für alle Elemente gleich ist.)
Die Beweise dieser Aussagen findet man beispielsweise bei Cormen et al., S. 242 ff.
7.3 Hashverfahren 7-60
Zusammenfassung
Operation isIn
(
x)
, n= |
M|
Anzahl der Elemente der Menge Implementierung Worst Case Average CaseBitfeld O
(
1)
O(
1)
Liste unsortiert O