1/m

Werbung
Gliederung
5. Compiler
1.
2.
3.
4.
Struktur eines Compilers
Syntaxanalyse durch rekursiven Abstieg
Ausnahmebehandlung
Arrays und Strings
6. Sortieren und Suchen
1. Grundlegende Datenstrukturen
2. Bäume
3. Hashing (Streuspeicherung)
7. Graphen
1. Darstellung und Topologisches Sortieren
2. Kürzeste Wege
3. Fluß- und Zuordnungsprobleme
Hashing (Streuspeicherung)
• Verfahren, die allgemein auf Bäume basieren garantieren
einen logarithmischen Aufwand beim Datenzugriff (Suche,
einfügen, löschen)
• Motivation für Hashverfahren
– Voraussetzung für Binäre Suchbäume: (totale) Ordnung auf der
Nutzinformation
– Ordnung nicht immer verfügbar: z.B. Einträge in einer Datenbank
(Kundendaten etc.)
• Grundidee der Hashverfahren
– Datensätze werden in einem normalen Feld mit direktem Zugriff
gespeichert
– Die Hash-Funktion ermöglicht für jeden gespeicherten Wert den
direkten Zugriff auf den Datensatz
2
Hashing (1)
• Hashing (engl.: to hash=zerhacken) beschreibt eine spezielle Art der
Speicherung der Elemente einer Menge durch Zerlegung des SchlüsselUniversums
• Die Position des Daten-Elements im Speicher ergibt sich (zunächst) durch
Berechnung direkt aus dem Schlüssel
• Die Menge aller möglichen Schlüssel (der Wertebereich) sei D (Domain)
• Der Speicher wird ebenfalls zerlegt in m gleich große Behälter (Buckets)
• Es ist |D| sehr viel größer als m
• Eine Hash-Funktion h kann nun für jeden Schlüssel s die Nummer des
Buckets h(s) ∈ {0,1,...,m − 1} berechnen
• Ideal wäre eine eindeutige Speicher-Zuordnung eines Datums mit
Schlüssel s zum Bucket mit Nummer h(s): Einfügen und Suchen könnten
in konstanter Zeit (O(1)) erfolgen.
• Tatsächlich treten natürlich Kollisionen auf: Mehrere Elemente können
auf die gleiche Hash-Adresse abgebildet werden. Kollisionen müssen
(auf eine von verschiedenen Arten) behandelt werden.
3
Hashing: Prinzip
• Schlüssel werden in einen zweiten, endlichen Bereich, mit dem eine
Reihung angesteuert wird, abgebildet
• Suche wird mit speziellen Verfahren durchgeführt (z.B.
Reihungsindizierung, Kollisionsauflösung)
Schlüssel
Hashfunktion H
(BereichsTransformation)
Hashwert
Suche
Reihungsindizierung
oder Kollisionsauflösung
Objekt
Reihungsindex
oder Kollisionswert
4
Hashing: Beispiel
• Daten werden in einem Feld von 0 bis 9 gespeichert
• Elemente 42 und 119 werden gespeichert
• Hashfunktion: h= i mod 10
Index
Eintrag
0
1
2
42
3
4
Kollision für die
Werte 69 und 119:
69 mod 10 -> 9
119 mod 10 -> 9
5
6
7
8
9
119
5
Hash-Funktionen
Def.: Es sei D ein Wertebereich von Schlüsseln und m die
Anzahl der Behälter Bo , ... , Bm-1 zum Speichern einer
gegebenen Menge {e1, .. en} von Elementen (Datensätzen)
Eine Hash-Funktion h ist eine (totale) Abbildung
h: D → {0, .. m-1},
die jedem Schlüsselwert w ∈ D eine Nummer h(w) und damit
einen Behälter Bh(w) zuordnet.
Die Nummern der Behälter werden auch als Hash-Adressen
und die Menge der Behälter als Hash-Tabelle bezeichnet.
Wertebereich
D
h(w)
0
Bo
1
B1
...
...
...
...
m-1
Bm-1
Hash-Adressen
Hash-Tabelle
6
Probleme beim Hashing
• Menge der möglichen Schlüssel (hier: alle Wörter) sehr viel
größer als die endliche Menge der Tabellenindizes
– Kollisionen, d.h. zwei oder mehrere Schlüssel werden auf dieselbe
Adresse abgebildet; müssen behandelt werden
– Jede Strategie der Kollisionsbehandlung hat Vor- und Nachteile
• Hash-Funktion h sollte die Schlüssel möglichst gleichmäßig
auf den Bereich der Tabellenindizes verteilen (gute
Streuung)
• h soll effizient berechenbar sein (O(log M) oder O(1)).
7
Anforderungen an Hash-Funktionen
• Eine Kollision tritt dann auf, wenn bei Einfügen eines
Elementes mit Schlüssel s der Bucket Bh(s) schon belegt ist.
Nach einer Kollision werden Operationen zur
Kollisionsbehandlung durchgeführt
• Eine Hash-Funktion h heißt perfekt für eine Menge von
Schlüsseln S, falls keine Kollisionen für S auftreten.
• Ist h perfekt und |S| = n, dann gilt: n ≤ m.
• Das Verhältnis
BF = # Gespeicherte Schlüssel/Größe der Hash-Tabelle = n/m
bezeichnet man als Belegungsfaktor der Hash-Tabelle
• Eine Hash-Funktion ist gut gewählt, wenn
– der Belegungsfaktor möglichst hoch ist,
– für viele Schlüssel-Mengen die Anzahl der Kollisionen möglichst klein
ist,
– sie leicht und effizient zu berechnen ist.
8
Hashing (2)
Beispiel: Hash-Funktion für Strings
public static int h (String s){
int k = 0, m = 13;
for (int i=0; i < s.length(); i++)
k += (int)s.charAt (i);
return ( k%m );
}
Folgende Hash-Adressen werden generiert für m = 13.
Schlüssel h(s)
Test
0
Hallo
2
SE
9
Algo
10
h wird perfekter, je größer m gewählt wird.
9
Grundoperationen auf Hash-Tabellen
• Einfügen eines Datenelements
– Wende Hash-Funktion h auf den Schlüssel des Datenelements an:
Einfügeposition ∈ {1, ,,, m-1} in der Hash-Tabelle
– Ist der Platz in der Hash-Tabelle noch frei, Datensatz dort speichern,
ansonsten Kollisionsbehandlung
• Löschen eines Datenelements
– Wende h auf den Schlüssel des Datenelements an:
Position ∈ {1, ,,, m-1} in der Hash-Tabelle
– Falls Datensatz an dieser Position gefunden, Datensatz löschen, sonst
je nach Kollisionsbehandlung weiter
• Suchen eines Datenelements
– Wende h auf den Schlüssel des Datenelements an:
Position ∈ {1, ,,, m-1} in der Hash-Tabelle
– Steht der Datensatz nicht auf dieser Tabellenposition, erfolgen je
nach Kollisionsbehandlung Zusatzoperationen (z.B. weitere Suche)
10
Wahl der Hash-Funktion
• Die Anforderungen hoher Belegungsfaktor und Kollisionsfreiheit stehen in
Konflikt zueinander. Es ist ein geeigneter Kompromiss zu finden.
• Für die Schlüssel-Menge S mit |S| = n und Behälter B0, . . . , Bm-1 gilt:
– für n > m sind Konflikte unausweichlich
– für n < m gibt es eine (Rest-) Wahrscheinlichkeit PK(n,m) für das Auftreten
mindestens einer Kollision.
• Abschätzung für die Wahrscheinlichkeit einer Kollision PK(n,m)
– Für beliebigen Schlüssel s ist die Wahrscheinlichkeit dafür, dass h(s) = j mit
j ∈ {0, . . . ,m - 1}: P[ h(s) = j ] = 1/m, falls Gleichverteilung gilt
– Es gilt PK(n,m) = 1 - P¬K(n,m), wobei P¬K(n,m) die Wahrscheinlichkeit
dafür ist, dass es beim Speichern von n Elementen in m Behälter zu keinen
Kollisionen kommt.
11
Wahrscheinlichkeit von Kollisionen
• Werden n Schlüssel nacheinander auf die Behälter
B0, . . . , Bm-1 verteilt (bei Gleichverteilung), gilt jedes mal
P [ h(s) = j ] = 1/m
• Es sei P(i) die Wahrscheinlichkeit, dass beim Einfügen des i-ten
Datensatzes in m Sektoren keine Kollision auftritt
• Dann gilt:
...
P(1) = 1
P(2) = (m-1) / m
P(i) = (m-i+1) / m
• Damit ist
m(m − 1)...(
m − n + 1)
PK(n,m) = 1 − P(1)* P(2)*...* P(n) = 1 −
mn
Für m = 365 etwa ist P(23) > 50% und P (50) ≈ 97%
(Geburtstagsparadoxon)
12
Hash-Funktionen in der Praxis
In der Praxis verwendete Hash-Funktionen (Siehe D.E. Knuth:
The Art of Computer Programming)
• Sei W = integer, d.h. die Schlüsselwerte sind ganze Zahlen. Dann liefert
die sog. Divisions-Rest-Methode gute Hash-Funktionen:
– h(w) = (a * w) mod m (a ≠ 0, a ≠ m und m Primzahl)
• Sei M eine Menge von Zeichenreihen der Form
s = [s1, ... , sq] . Dann ist (neben der oben bereits genannten einfachen
Funktion)
  k −1 i 
w 

h(s) =   ∑ B si  mod 2  mod m

  i =0

eine gute Hash-Funktion
Für die Wahl von B wird der Wert 131 empfohlen – w ist die Wortbreite
des Computers (also w = 32, w = 64)
13
Behandlung von Kollisionen
• Die Behandlung von Kollisionen erfolgt bei verschiedenen
Verfahren unterschiedlich.
• Ein Datensatz mit Schlüssel s ist ein Überläufer, wenn der
Behälter h(s) schon durch einen anderen Satz belegt ist.
• Strategien zur Kollisionsbehandlung
– Verkettung der Überläufer (Offenes Hashing): Es wird eine
Liste mit den Elementen aufgebaut, die dieselbe Position belegen
– Sondieren (Geschlossenes Hashing): Es wird eine alternative
Position im Fall einer Kollision gesucht
14
Verkettung der Überläufer
• Überläufer außerhalb der Hash-Tabelle ablegen
– Z.B. als verkettete lineare Liste
– Liste wird an den Hash-Tabelleneintrag angehängt, der sich durch
Anwendung der Hash-Funktion auf die Schlüssel ergibt
• Separate Verkettung der Überläufer
– die Hash-Tabelle enthält je einen Datensatz und einen Listen-Kopf pro
Behälter.
– Weitere Datensätze (= Überläufer) werden in je einer Liste an den
betreffenden Behälter angehängt
• Direkte Verkettung der Überläufer
– Die Hash-Tabelle enthält nichts als die Listen-Köpfe, alle Datensätze
befinden sich in den Listen
– Alle Datensätze werden in den Überlaufketten gespeichert
15
Direkte Verkettung: Beispiel
• Verwendete Hash-Funktion :
h(s) = (ord(s[1]) + ord(s[2]) + ord(s[3])) mod 4
• Zeichenkette: ‘‘Almodovar“
h ('Almodovar') = (65+108+109) mod 4
= (1+0+1) mod 4 = 2
0
1
Allen
Jarmusch
2
Wenders
Kurosawa
3
Herzog
Fellini
Almodovar
16
Separate Verkettung
• Im Gegensatz zum Speichern durch direkte Verkettung wird
pro Behälter ein Datensatz direkt in der Hash-Tabelle
gespeichert.
• Fallen in einem Behälter Überläufer an, so wird dafür eine
Überlauf-Liste angelegt.
0
1
Allen
Jarmusch
2
Wenders
Kurosawa
3
Herzog
Fellini
Almodovar
• Vergleich mit dem Speichern durch direkte Verkettung:
– Überlauf-Listen werden nur bei Auftreten von Kollisionen benötigt.
– enthält ein Behälter keinen Datensatz, so wird trotzdem Speicher
dafür vorgehalten.
17
Separate Verkettung der Überläufer: Implementierung
• Die Hash-Tabelle ist ein Array (der Länge m) von Listen. D.h.
jeder Behälter der Hash-Tabelle wird durch eine Liste
implementiert
class hashTable {
Liste [] ht;
hashTable (int m){
ht = new Liste[m];
for (int i = 0; i < m; i++)
ht[i] = new Liste();
}
...
Konstruktor konstruiert
Array mit m listen
}
18
Verkettung mit selbst-organisierender Liste
• Selbstorganisierend bedeutet, dass das zuletzt eingesetzte
oder aufgesuchte Element an den Anfang der Liste gebracht
wird.
• Vorteile: Hash-Tabelle funktioniert auch noch (obwohl
langsam), wenn die Größe der Hash-Tabelle zu klein gewählt
war.
• Nachteile: Zusätzlicher Speicherplatz für die Zeiger wird
benötigt. Lange Listen können entstehen.
19
Aufwand des offenen Hashing
• worst case
h(s) liefert immer den gleichen Wert, alle Datensätze sind in
einer Liste: Verhalten wie bei Linearer Liste.
• average case
– bei erfolgreicher Suche (und damit auch für den Fall des Entfernens eines
Datensatzes) gilt: Aufwand ≈ 1 + 0.5 * BF
– bei erfolgloser Suche (und damit auch für den Fall des Einfügens eines
Datensatzes) gilt: Aufwand ≈ BF
• best case
Die Suche führt unmittelbar zum Erfolg: Aufwand ist
O(1)
20
Geschlossene Hash-Verfahren
• Geschlossenes Hashing bedeutet, dass Überläufer nicht in
separaten Listen, sondern in noch freien Bereichen der HashTabelle selbst gespeichert werden.
• Der Prozess der Suche nach einer geeigneten Position
bezeichnet man als Sondieren
• Vorgehensweise zum Speichern eines Datensatzes mit
Schlüssel s:
1. Speichere an Position h(s), wenn dort frei
2. Sonst speichere an der nächsten freien Position, die durch
Sondieren ermittelt wird
3. Ist kein Platz mehr frei, kann nicht gespeichert werden.
21
Sondierverfahren (1)
• Beim linearen Sondieren (linear probing) wird linear und zyklisch nach
dem nächsten freien Platz in der Hash-Tabelle gesucht.
• Sekundärkollision: Schlüssel mit unterschiedlicher Streuadresse
kollidieren
• Quadratisches Sondieren
– Mit quadratisch wachsendem Abstand wird nach einem freien Platz
gesucht.
22
Sondierverfahren
• Lineares Sondieren: Falls die Position h(e) in der Hash-Tabelle besetzt
ist, prüft das Verfahren der Reihe nach die Positionen:
(h(w)+1) mod m, (h(w)+2) mod m, ...
• Quadratisches Sondieren: Lineares Sondieren neigt zur Clusterbildung:
„Klumpen“ in denen alle Positionen bereits besetzt sind und sich dadurch
lange Sondierfolgen bilden. Um dies zu vermeiden, wird die Folge der
Quadratzahlen für die Sondierabstände verwendet
(h(w)+1) mod m, (h(w)+4) mod m, ...,(h(w)+i 2) mod m
• Double Hashing (Doppelte Streuadressierung): Die doppelte
Streuadressierung soll Sekundärkollisionen verhindern
h(w) - h2(w) mod m, h(w) - 2*h2(w) mod m, ..., h(w) - (m-1)*h2(w) mod m
(wobei h2(w)≠ 0 und h2(w) teilerfremd zu m)
Optimierungsmöglichkeit: Sondierung in beide Richtungen
•
•
Lineares Sondieren: (h(w)+1) mod m, (h(w)-1) mod m,(h(w)+21) mod m, ..
Quadratisches Sondieren: (h(w)+1) mod m, (h(w)-1) mod m, (h(w)+4) mod, (h(w)-4) mod
m
23
Löschen bei geschlossenen Hash-Verfahren
• Geschlossenes Hashing erfordert eine besondere Behandlung
der Lösch-Operationen: Soll ein Datensatz gelöscht werden,
so kann ein anderer Datensatz unerreichbar werden
• Zu löschende Datensätze dürfen also nicht physisch gelöscht,
sondern nur als gelöscht markiert werden. Jeder Behälter
muss als "frei", "belegt" oder "gelöscht" markiert sein.
• Beim Sondieren werden Behälter mit gelöschten Datensätzen
wie belegte Sektoren zum "Weiterhangeln" benutzt.
24
Löschen bei geschlossenen Hash-Verfahren: Beispiel
D = Integer, h(w) = w mod 7, (absteigend) lineares Sondieren
1. Einfügen von: 78, 57, 80, 16, 21
Beachte:
(78 % 7 == 1), (57 % 7 == 1), (80 % 7 == 3),
(16 % 7 == 2), (21 % 7 == 0), (29 % 7 == 1)
57 78 16 80
21
2. Einfügen von: 29
57 78 16 80
frei
belegt
29 21
gelöscht
3. Löschen von: 57
57 78 16 80
29 21
Das Löschen von "57" würde "21" und "29" unerreichbar machen, falls nicht markiert!
25
Aufwand bei geschlossenen Hash-Verfahren
• Abschätzung nur über Wahrscheinlichkeiten
• Wahrscheinlichkeit einer Kollision hängt vom Füllungsgrad ab
– Sei α der Anteil der belegten Buckets
– Aufwand einer erfolglosen Suche: 1+ α + α2+.......=1/1-α
• Beispiele
– Bei einer halbgefüllten Hash-Tabelle werden im Mittel 1/1-0,5=2
Zugriffe benötigt
– Bei einem Belegungsfaktor von 90% sind bereits 10 Zugriffe
notwendig
• Faustregel: Beim geschlossenen Hashing sollte der
Belegungsfaktor nicht größer als 80% werden
26
Hybride Hash-Verfahren (1)
Eine Analyse der bisher betrachteten Verfahren zeigt:
• Die offenen Verfahren sind wesentlich schneller als die
geschlossenen, Sondieren ist zeitaufwendig (vor allem bei
hohem Belegungsfaktor)
• Bei den offenen Verfahren wird Speicherplatz für die nicht
belegten Sektoren verschwendet (umso mehr, je niedriger
der Belegungsfaktor ist)
• Das hybride hashing (engl. auch: "Coalesced hashing")
versucht Vorteile beider Verfahren miteinander zu verbinden
– Überläufer werden wie beim geschlossenen Verfahren innerhalb der
Hash-Tabelle gespeichert, aber durch Referenzen miteinander
verkettet.
– Jeder Sektor enthält (potentiell) einen Datensatz und eine HashAdresse, die auf einen anderen Sektor verweist, in dem nach einem
"Überläufer" zu suchen ist.
27
Hybride Hash-Verfahren (2)
Die Hash-Tabelle ist jetzt ein doppeltes Array (der Länge m)
(a) für die Datensätze,
(b) für die Position eines möglichen "Überläufers".
class hashTable {
Datensätze wie bei separater Verkettung.
Elem [] ht;
int [] next;
Zusatz-Array für Sondierziel bei Überlauf.
...
}
Beispiel: W = Integer, h(w) = w mod 7, Sondierziel: höchster freier Behälter:
Einfügen von: 78, 57,
80, 16, 21
21 78 16 80
Einfügen von: 69 (hashAdresse 6 ist bereits belegt!)
21 78 16 80
57
next
6
6
ht
69 57
5
ht
next
28
Dynamische hashverfahren (1)
• Alle bisher betrachteten Verfahren hängen wesentlich vom
Belegungsfaktor BF und damit von der (bisher als fest
angenommenen) Länge der Hash-Tabelle ab.
– Ist BF klein (etwa BF < 0.5), so wird Speicherplatz verschwendet.
– Ist BF groß (etwa BF < 0.8), so werden das Einfügen und Suchen
(zeit-) ineffizient.
• Zeigt die Hash-Tabelle insgesamt ein stark anwachsendes
Verhalten, so verschlechtert sich ihre Leistung mit
wachsendem Belegungsfaktor
• Dieses grundsätzliche Problem kann auf zwei Arten gelöst
werden:
– Globale Reorganisation der Hash-Tabelle: Wahl einer neuen HashFunktion und Umspeichern der Datensätze. (sehr aufwendig,
Sperrung während der Reorganisation notwendig!)
– Dynamisches Hashing: Die Hash-Tabelle wird durch kleine lokale
Reorganisationen ständig an die Anzahl der Datensätze angepaßt.
29
Dynamische hashverfahren (2)
• Grundidee: Beim Überschreiten eines vorgegebenen
Schwellenwerts für den Belegungsfaktor BF wird ein neuer
Behälter in der Hash-Tabelle (mit Hash-Adresse m) angelegt
und die Datensätze des am meisten belegten Behälters werden
möglichst gleichmäßig auf den bisherigen und auf den neuen
Behälter aufgeteilt
• Die einzelnen Verfahren unterscheiden sich im Wesentlichen darin, wie
die Hash-Funktion h zu adaptieren ist
– Beim sog. linearen hashing wird die Hash-Tabelle durch die Aufspaltung eines
Sektors schrittweise expandiert, eine neue Expansionsstufe ist erreicht, wenn
alle Sektoren aufgespalten sind und sich damit die Größe der Tabelle
verdoppelt hat. Die Menge der hash-Adressen läßt sich z.B. erweitern, wenn
man eine hash-Funktion h = w mod m zu h = w mod 2*m modifiziert.
– Beim sog. virtuellen hashing wird der Übergang auf eine neue
Expansionsstufe in einem Schritt vollzogen.
30
Hashing: Zusammenfassung
• Hash-Tabellen sind in der Praxis die schnellsten Verfahren für
Suchen/Einsetzen (vorausgesetzt sind natürlich gute Streufunktion und
eine Tabelle, die nicht mehr als ca. 80%-90% voll ist).
• Hash-Tabellen liefern keine sortierte Reihenfolge oder das Minimum oder
Maximum. Es ist auch nicht möglich, den größten Schlüssel <= k oder
einen Schlüssel zwischen k1 und k2 zu finden
• Eine Hash-Tabelle hat i.d.R. eine feste Größe, die anders als bei den
Suchbäumen nicht mit den Datenmengen wächst. Eine dynamische
Mengenanpassung wird dadurch erreicht, dass ein Schwellenwert
festgelegt wird, bei dem neue Behälter in die Hash-Tabelle angelegt
werden (z.B. bei einer 90%-iger Füllung)
• obere Schranke für Aufwand ist O(m) (im Durchschnitt nahezu konst.)
31
Herunterladen