Computerorientierte Mathematik II

Transcription

Computerorientierte Mathematik II
Computerorientierte Mathematik II
mit Java
Rolf H. Möhring
Technische Universität Berlin
Institut für Mathematik
Sommersemester 2005
ii
Vorbemerkungen
Diese Vorlesung ist der zweite Teil des Zyklus Computerorientierte Mathematik und schließt sich
direkt an die Computerorientierte Mathematik I an. Dieses Skript basiert auf meiner Vorlesung vom
Sommersemester 2004. Zwei Studenten der Vorlesung, Elisabeth Günther und Olaf Maurer, haben
im Sommer 2004 eine ausgezeichnete Ausarbeitung der Vorlesung angefertigt, die von mir nur noch
leicht überarbeitet und ergänzt wurde. Das Resultat ist dieses Skript, das auch online unter
http://www.math.tu-berlin.de/coga/moehring/Coma/Skript-II-Java/
zur Verfügung steht.
Die Vorlesung umfasst folgende Punkte: Wir behandeln zunächst ein Sortierverfahren namens Bucketsort, das durch besondere Anforderungen an die Schlüsselmenge schon in linearer Zeit sortieren kann.
Dann werden Bäume, insbesondere binäre Bäume besprochen und wie diese zur Datenkompression
mit dem Huffman-Algorithmus genutzt werden können. Bäume finden als Suchbäume und insbesondere als AVL-Bäume weitere Verwendung. Wir kommen dann zu optimalen statischen Suchbäumen
und besprechen eine Alternative zum Suchen in Bäumen, das sogenannte Hashing. Den Abschluss
des Semesters bildet ein Kapitel über Schaltkreistheorie und Programmierbare Logische Arrays.
iii
iv
VORBEMERKUNGEN
Inhaltsverzeichnis
Vorbemerkungen
iii
Inhaltsverzeichnis
v
1
Bucketsort
1
1.1
Einfaches Bucketsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
1.1.1
Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
1.1.2
Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
1.1.3
Aufwandsanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
Sortieren von Strings mit Bucketsort . . . . . . . . . . . . . . . . . . . . . . . . . .
5
1.2.1
Sortieren von Strings der Länge k . . . . . . . . . . . . . . . . . . . . . . .
6
1.2.2
Sortieren von Binärzahlen . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
1.2.3
Sortieren von Strings variabler Länge . . . . . . . . . . . . . . . . . . . . .
9
Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
1.2
1.3
2
Bäume und Priority Queues
15
2.1
Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
2.1.1
Grundbegriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
2.1.2
Implementation von binären Bäumen . . . . . . . . . . . . . . . . . . . . .
17
2.1.3
Traversierung von Bäumen . . . . . . . . . . . . . . . . . . . . . . . . . . .
22
Priority Queues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
2.2.1
Mögliche Implementationen einer Priority Queue . . . . . . . . . . . . . . .
26
Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
2.2
2.3
3
Huffman Codes und Datenkompression
29
v
vi
INHALTSVERZEICHNIS
3.1
Codierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
29
3.1.1
Präfixcode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31
3.2
Der Huffman Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
3.3
Weitere Datenkompressionsverfahren . . . . . . . . . . . . . . . . . . . . . . . . .
41
3.3.1
Der adaptive Huffmancode . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
3.3.2
Der run length code“ . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
”
Der Lempel-Ziv Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
3.4
Abschließende Bemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
3.5
Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
3.3.3
4
Suchbäume
45
4.1
Basisoperationen in Suchbäumen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
4.1.1
Suchen nach Schlüssel k . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
4.1.2
Einfügen eines Knoten . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
4.1.3
Löschen eines Knoten . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
51
4.2
5
AVL-Bäume
53
5.1
Grundsätzliche Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
53
5.2
Rotationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
57
5.3
Die Basisoperationen in AVL-Bäumen . . . . . . . . . . . . . . . . . . . . . . . . .
61
5.3.1
Suchen eines Knotens v . . . . . . . . . . . . . . . . . . . . . . . . . . . .
62
5.3.2
Einfügen eines neuen Knotens v . . . . . . . . . . . . . . . . . . . . . . . .
62
5.3.3
Löschen eines Knotens v . . . . . . . . . . . . . . . . . . . . . . . . . . . .
65
Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
67
5.4
6
7
41
Optimale statische Suchbäume
69
6.1
Statische Suchbäume allgemein . . . . . . . . . . . . . . . . . . . . . . . . . . . .
69
6.2
Optimalität statischer Suchbäume . . . . . . . . . . . . . . . . . . . . . . . . . . .
70
6.3
Konstruktion eines optimalen statischen Suchbaumes . . . . . . . . . . . . . . . . .
73
6.4
Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
81
B-Bäume
83
7.1
83
Definition und Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
INHALTSVERZEICHNIS
7.2
7.3
8
Basisoperationen in B-Bäumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
86
7.2.1
Suchen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
86
7.2.2
Einfügen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
86
7.2.3
Löschen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
88
Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
91
Hashing
93
8.1
Hash-Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
94
8.1.1
Divisionsmethode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
95
8.1.2
Multiplikationsmethode . . . . . . . . . . . . . . . . . . . . . . . . . . . .
95
Kollisionsbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
96
8.2.1
Chaining . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
96
8.2.2
Offene Adressierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
97
8.2
8.3
9
vii
Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Schaltkreistheorie und Rechnerarchitektur
107
9.1
Schaltfunktionen und Schaltnetze . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
9.2
Vereinfachung von Schaltnetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
9.3
9.4
9.5
9.2.1
Das Verfahren von Karnaugh . . . . . . . . . . . . . . . . . . . . . . . . . . 117
9.2.2
Das Verfahren von Quine und McCluskey . . . . . . . . . . . . . . . . . . . 120
9.2.3
Das Überdeckungsproblem . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
Schaltungen mit Delays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
9.3.1
Addierwerke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
9.3.2
Das Fan-In-Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
PLAs und das Prinzip der Mikroprogrammierung . . . . . . . . . . . . . . . . . . . 131
9.4.1
Aufbau eines PLAs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
9.4.2
Zur Programmierung von PLAs . . . . . . . . . . . . . . . . . . . . . . . . 133
Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
Literaturverzeichnis
141
Index
142
viii
INHALTSVERZEICHNIS
Kapitel 1
Bucketsort
Bucketsort ist ein Sortierverfahren, das grundsätzlich anders als alle Sortierverfahren funktioniert, die
wir bisher kennen gelernt haben. Es zeichnet sich dadurch aus, dass es nicht wie die Verfahren aus
Teil I der Vorlesung auf paarweisen Vergleichen von Schlüsseln basiert, sondern voraussetzt, dass
die Schlüsselmenge klein und bekannt ist, und dass es Objekte direkt dem richtigen Bucket“ (Fach)
”
zuordnet.
Anschaulich lässt sich dieses Verfahren mit der Verteilung der Post im Postamt auf die Häuser einer
Straße vergleichen. Der Briefträger hat eine Reihe von Fächern, die den Hausnummern entsprechen.
Er geht die Briefe der Reihe nach durch und legt jeden Brief in O(1) (also konstanter) Zeit in das Fach
mit der entsprechenden Hausnummer. Dabei können in einem Fach natürlich mehrere Briefe sein, die
aber aus Sicht der Ordnungsrelation gleich sind (da sie in das gleiche Bucket sortiert werden, haben
sie ja die gleiche Nummer) und daher nicht mehr innerhalb des Fachs sortiert werden müssen. Der
Briefträger entnimmt die Briefe den Fächern der Reihe nach und hat sie damit nach Hausnummern
sortiert.
Bei m Hausnummern und n Briefen sortiert er also in O(m + n). Da in der Regel m ≤ n gilt, wenn
Bucketsort angewendet wird, befindet sich der Algorithmus dann in der Komplexitätsordnung Θ(2n) =
Θ(n) und man erhält so also einen Sortieralgorithmus, dessen Aufwand linear von der Anzahl der zu
sortierenden Schlüssel abhängt.
Man beachte, dass die in der CoMa I ermittelte untere Komplexitätsschranke von Ω(n log n) nur
Sortieralgorithmen betrifft, die auf paarweisen Vergleichen beruhen. Bucketsort beruht jedoch nicht
auf paarweisen Vergleichen von Schlüsseln und setzt außerdem zusätzliche Informationen über die
Schlüsselmenge voraus. Daher liegt Bucketsort nicht in der Klasse der Sortieralgorithmen, die von
dieser Schranke betroffen sind.
Umgesetzt auf Datenstrukturen bedeutet dies:
Version vom 24. März 2006
1
2
KAPITEL 1. BUCKETSORT
Wirklichkeit
Fächer
Hausnummern
Stapel im Fach
Briefe am Anfang
Briefe am Ende
1.1
1.1.1
Datenstruktur
Array
Array-Indizes
Liste an jedem Array-Index
Liste
Liste
Einfaches Bucketsort
Definition
Wir geben jetzt einen Algorithmus für die oben erklärte Situation an. Gegeben seien also n Objekte a1 , a2 , . . . , an mit Schlüsselwerten s(a1 ), s(a2 ), . . . , s(an ) in einer Liste L. O.B.d.A. seien die
Schlüsselwerte Zahlen zwischen 0 und m − 1, also s(a j ) ∈ {0, 1, . . . , m − 1}, j = 1, . . . , n. Es gibt
dann also genau m paarweise verschiedene Schlüsselwerte.
Algorithmus 1.1 (Einfaches Bucketsort)
1. Initialisiere ein Array mit m leeren Queues Qi (Buckets), je eine für jeden Wert i = 0, 1, . . . , m−1
und je einer Referenz (head bzw. tail) auf den Anfang und das Ende der Queue Qi .
2. Durchlaufe L und füge das Objekt a j entsprechend seines Schlüsselwertes in die Queue Qs(a j )
ein.
3. Konkateniere die Queues Q0 , Q1 , . . . , Qm−1 über die head- und tail-Referenzen zu einer Liste L
und gebe L zurück.
Beispiel 1.1 Sei m = 5 und n = 9. Die Liste L ist gegeben durch Abbildung 1.1.
head
r - a1 - a2 - a3 - a4 - a5 - a6 - a7 - a8 - a9
2
r
1
r
0
r
2
r
2
r
4
r
0
r
4
r
1
Abbildung 1.1: Liste L der zu sortierenden Elemente
Es wird ein Array A mit je 2 Referenzen auf head und tail der Queues Qi eingerichtet. Abbildung 1.2
zeigt die Queues nach Abarbeitung der Liste L, also am Ende von Schritt 2.
Dann werden die einzelnen Listen konkateniert. Das Konkatenieren der Listen ist sehr einfach, da nur
das letzte Element (über tail) auf das erste Element (über head) der nächsten nichtleeren Liste gesetzt
werden muss, siehe Abbildung 1.3.
3
1.1. EINFACHES BUCKETSORT
A
r
A[0]
A[1]
A[2]
A[3]
A[4]
- a3 - a7
0
r
r
r
r
r
r
r
r
2
r
2
r
- a6 - a8
4
r
1
- a1 - a4 - a5
2
r
r
r
- a2 - a9
1
r
r
0
r
4
r
Abbildung 1.2: Queues nach Abarbeitung der Liste
A
r
A[0]
A[1]
A[2]
A[3]
A[4]
- a3 - a7
0
r
r
r
r
r
r
r
r
2
r
2
r
- a6 - a8
4
r
1
- a1 - a4 - a5
2
r
r
r
- a2 - a9
1
r
r
0
r
4
r
Abbildung 1.3: Queues nach Konkatenation der Einzellisten
4
KAPITEL 1. BUCKETSORT
1.1.2
Implementation
Eine Implementation in Java könnte in etwa wie folgt aussehen:
class QueuePointer {
public ListNode head;
public ListNode tail;
}
QueuePointer[] A = new QueuePointer[n];
// array with head- and tail-reference in each field
Das Einfügen des Knoten node mit Schlüsselwert i in die i-te Queue Qi geschieht dann mit einer
Anweisung der Form
A[i].tail.setNext(node);
Die Konkatenation zweier Queues erfolgt über die Anweisung
A[i].tail.setNext(A[j].head);
Dabei ist j die erste nichtleere Liste nach i.
1.1.3
Aufwandsanalyse
Satz 1.1 (Aufwand von einfachem Bucketsort) Algorithmus 1.1 sortiert die Liste korrekt in O(m +
n) Zeit.
Beweis: Am Ende von Schritt 2 enthält jede Queue Qi nur Objekte a j mit Schlüssel s(a j ) = i. Die
Konkatenation der Queues in der Reihenfolge Q0 , Q1 , . . . , Qm−1 liefert also eine korrekt sortierte Liste.
Das Einfügen von a j in die Queue Qi mit i = s(a j ) erfolgt durch Umhängen von Referenzen in O(1)
Zeit. Beim Durchlaufen der Liste L sind alle Vorgänger von a j bereits aus L entfernt und der Listenzeiger current der Liste L zeigt auf a j . Die Sequenz
A[i].tail.setNext(current);
A[i].tail = current;
current = current.getNext();
hängt a j aus L aus und in Qi ein. Das Einfügen aller Objekte a j geschieht also in O(n) Zeit.
Das Konkatenieren der Queues kann in einer Schleife mit O(1) Aufwand pro Queue geschehen. Wir
geben ein Code-Fragment an, das die Konkatenierung durchführt:
1.2. SORTIEREN VON STRINGS MIT BUCKETSORT
5
k = 0;
while ((A[k].head == NULL) && (k < m))
k++;
// k is now the first nonempty list in A, if there is one
i = k + 1;
while (i < m) { // while-loop: O(m)
// at this point k is the last list we have already concatenated
// we will now look for the next nonempty list after k
while ((A[i].head == NULL) && (i < m))
i++;
if (i==m)
break;
// if (i==m), we have iterated through all nonempty lists
// in A and are finished
// we have found a new nonempty list, concatenate it to k
A[k].tail.setNext(A[i].head);
// and prepare k for the next iteration
k = i;
i++;
} // endwhile
Weil das Konkatenieren pro Queue nur einen Aufwand in O(1) benötigt und maximal m Buckets konkateniert werden müssen, ist der Aufwand damit insgesamt O(n) + O(m) = O(m + n).
Falls m ≤ n gilt, ist der Aufwand in der Komplexitätsklasse O(n). Bucketsort schlägt dann also die
untere Komplexitätsschranke für Sortieralgorithmen, die auf paarweisen Vergleichen beruhen. Dafür
benötigt Bucketsort allerdings Informationen über die Werte der auftretenden Schlüssel, weil sonst
m nicht klein gehalten werden kann und der Aufwand von Bucketsort nur dann in O(n) liegt, wenn
m ≤ n gilt.
1.2
Sortieren von Strings mit Bucketsort
Wir wollen jetzt Bucketsort zum Sortieren von Strings gemäß der lexikographischen Ordnung nutzen.
Wir definieren zuerst, was wir unter lexikographisch kleiner“ verstehen wollen:
”
Sei S die Menge der Zeichen und ≤ eine lineare Ordnung auf S. Seien A = a1 a2 . . . a p und B =
b1 b2 . . . bq zwei Strings der Länge p bzw. q über S. Dann heißt A lexikographisch kleiner als B, in
Zeichen
A ≤lex B
6
KAPITEL 1. BUCKETSORT
falls einer der folgenden Fälle zutrifft:
1. p ≤ q und ai = bi für i = 1, . . . , p (d.h. A ist ein Anfangsstück von B).
2. Es gibt j ∈ {1, . . . , p} mit a j ≤ b j und ai = bi für i = 1, . . . , j − 1 (d.h. an der ersten Stelle j, an
der A und B verschieden sind, ist a j kleiner oder gleich b j bezüglich der linearen Ordnung auf
S).
Beispiel 1.2 Hall ≤lex Hallo, Arbeit ≥lex Album
Wir sortieren mit Bucketsort lexikographisch, indem wir für jede Komponente einfaches Bucketsort
verwenden. Zunächst betrachten wir den Spezialfall, dass alle Strings die gleiche Länge k haben. Dies
beinhaltet insbesondere das Sortieren k-stelliger Binärzahlen beziehungsweise von Strings auf der
Zeichenmenge Sb = {0, 1}.
1.2.1
Sortieren von Strings der Länge k
Die Idee besteht darin, die Strings bezüglich der Stellen mit Bucketsort zu sortieren, wobei die Stellen von hinten nach vorn durchlaufen werden. Dies gewährleistet, dass vor dem Bucketsort bezüglich
Stelle i (also nach k − i Iterationen) die Strings bereits nach den letzten Stellen i + 1, . . . , k lexikographisch sortiert sind. Diese Sortierung wird trotz der späteren Durchläufe erhalten, weil durch Bucketsort die Elemente, die an einer Stelle i gleich sind, in der gleichen Reihenfolge eingefügt werden, in der
sie schon waren (durch die vorherige Iteration beziehungsweise von Anfang an). Diese Eigenschaft
eines Sortieralgorithmus bezeichnet man als Stabilität, Bucketsort ist ein stabiler Sortieralgorithmus.
Algorithmus 1.2 (Bucketsort)
Input: Eine Liste L mit Strings A1 , A2 , . . . , An der Länge k mit Ai = ai1 , ai2 , . . . , aik und ai j ∈ S =
{0, 1, . . . , m − 1}
Output: Eine Permutation B1 , . . . , Bn von A1 , . . . , An mit Bi ≤lex B2 ≤lex · · · ≤lex Bn
Methode:
1. Richte eine Queue Q ein und füge A1 , . . . , An in Q ein. 1
2. Richte ein Array Bucket von m Buckets ein (wie beim einfachen Bucketsort)
3. for jede Stelle r := k downto 1 do
3.1 Leere alle Buckets Bucket[i]
3.2 while Q nicht leer ist do
1 Einfügen bedeutet hier immer, dass nur Referenzen auf A eingefügt werden. Das Einfügen geschieht also in O(1) und
i
nicht wie beim zeichenweisen Einfügen des Strings in O(k).
1.2. SORTIEREN VON STRINGS MIT BUCKETSORT
7
• Sei A j das erste Element in Q
• Entferne A j aus Q und füge es in Bucket[i] mit i = a jr ein
endwhile
3.3 Konkateniere die nichtleeren Buckets in die Queue Q
endfor
Beispiel 1.3 Sei S = {0, 1} und 0 < 1. Seien A1 = 010, A2 = 011, A3 = 101, A4 = 100.
Wir sortieren zunächst nach der letzten Komponente: 010 011 101 100
Dadurch erhalten wir Folgendes:
0 : 010 100
⇒ 010 100 011 101
1 : 011 101
Wir sortieren dann nach der zweitletzten Komponente: 010 100 011 101
Wir erhalten:
0 : 100 101
⇒ 100 101 010 011
1 : 010 011
Nun die letzte Iteration, also Sortierung nach der ersten Komponente: 010 100 011 101
Wir erhalten:
0 : 010 011
⇒ 010 011 100 101
1 : 100 101
Nach dem Durchlauf der for-Schleife stehen die Strings in folgender Reihenfolge in Q:
r=3
r=2
r=1
010
100
010
100
101
011
011
010
100
101
011
101
nach letzter Stelle sortiert
nach letzten 2 Stellen sortiert
nach letzten 3 Stellen sortiert
Wir sehen also: Das Sortierverfahren arbeitet (bei diesem Beispiel) korrekt. Das motiviert folgenden
Satz:
Satz 1.2 (Aufwand von Bucketsort) Algorithmus 1.2 sortiert A1 , . . . , An lexikographisch korrekt in
O((m + n) · k) Zeit.
Beweis: Wir beweisen folgende Invariante: Nach dem i-ten Durchlauf sind die Strings bezüglich der
letzten i Zeichen lexikographisch aufsteigend sortiert.
Daraus folgt dann insbesondere, dass beim Sortieren von k-stelligen Strings nach dem k-ten Durchlauf
die Strings bezüglich der letzten k Stellen (also allen Stellen) lexikographisch korrekt sortiert sind
und damit die Behauptung. Der Beweis der Invariante erfolgt durch vollständige Induktion über die
Iterationsschritte, hier bezeichnet mit r.
8
KAPITEL 1. BUCKETSORT
r = 1: (einfaches Bucketsort nach letzter Komponente):
In diesem Fall folgt die Korrektheit aus Satz 1.1, da die Strings nach Satz 1.1 lexikographisch korrekt
nach der letzten Stelle sortiert werden.
r → r + 1:
Die Behauptung sei also für r bewiesen. Betrachte nun die beiden Strings Ai und A j in der (r + 1)-ten
Iteration. Wir unterscheiden zwei Fälle:
Fall I: Ai und A j werden in der (r + 1)-ten Iteration in unterschiedliche Buckets sortiert.
Da Ai und A j in unterschiedliche Buckets sortiert werden, unterscheiden sich Ai und A j also an der
gerade betrachteten Stelle. Die lexikographische Korrektheit der Sortierung folgt dann wieder aus Satz
1.1, da wir ja wieder einfaches Bucketsort an der (r + 1)sten Stelle von hinten betrachten. Daher sind
sie lexikographisch korrekt sortiert.
Fall II: Ai und A j werden in der (r + 1)-ten Iteration in das gleiche Bucket sortiert.
Da die einzelnen Buckets durch Queues realisiert werden, werden die Strings in der Reihenfolge,
in der sie im vorigen Durchgang schon waren, hinten eingefügt und in der nächsten Iteration beziehungsweise in der Konkatenation wieder in dieser Reihenfolge ausgelesen. Da die Strings aber nach
der r-ten Iteration schon lexikographisch korrekt sortiert waren, sie sich aber an der (r + 1)-ten Stelle
von hinten nicht unterscheiden, sind sie dann nach den letzten (r + 1) Stellen lexikographisch korrekt
sortiert.
Da für jedes Zeichen in dem String genau ein Durchlauf erfolgt, erfolgen genau k Durchläufe. Da
jeder dieser Durchläufe (wie in Satz 1.1 bewiesen) in O(n + m) erfolgt, erfolgt die ganze Sortierung
daher in O (k · (n + m)).
1.2.2
Sortieren von Binärzahlen
Als spezielle Anwendung gibt es das Sortieren von n k-stelligen Binärzahlen in O(k · n). In CoMa I
wurde aber bewiesen, dass zum Sortieren von n Zahlen ein Aufwand in der Größenordnung O(n log n)
erforderlich ist. Der scheinbare Widerspruch ist aber keiner:
Mit k Stellen kann man nur 2k paarweise verschiedene Binärzahlen bilden. Für die Darstellung von n
paarweise verschiedenen k-stelligen Binärzahlen muss daher gelten:
2k ≥ n ⇔ k ≥ log n
Also gilt für dieses von n abhängige k:
n log n ≤ k · n
1.2. SORTIEREN VON STRINGS MIT BUCKETSORT
1.2.3
9
Sortieren von Strings variabler Länge
Erste Idee:
Die erste Idee für diesen Sortieralgorithmus ist es, ein Bucket hinzuzufügen, in das die Strings sortiert
werden, die im aktuellen Durchlauf an der betrachteten Stelle kein Zeichen haben. Weil diese Strings
lexikographisch kleiner sind, steht dieses Bucket vor allen anderen Buckets. Die Sortierung erfolgt
dann einfach mit Algorithmus 1.2.
Beispiel 1.4 Wir betrachten die Strings bab, abc und a und wollen sie in ihre lexikographisch korrekte
Reihenfolge sortieren. In der ersten Iteration erhalten wir

kein Zeichen : a 


a:
⇒ a bab abc
b:
bab 


c:
abc
Dann sortieren wir nach dem zweitletzten Zeichen:

kein Zeichen : a 


a:
bab
⇒ a bab abc
b:
abc 


c:
Und schließlich sortieren wir noch nach dem ersten Zeichen:

kein Zeichen :



:
a
a
abc
⇒ a abc bab
b:
bab



c:
Durch diesen Ansatz werden in jeder Iteration alle Strings betrachtet. Bezeichne `max die Länge des
längsten dieser Strings. Dann hat dieser Sortieralgorithmus den gleichen Aufwand wie der Sortieralgorithmus 1.2 zur Sortierung von Strings der festen Länge k = `max . Nach Abschnitt 1.2.1 ist also der
Gesamtaufwand dieses Algorithmus in der Klasse O (`max · (n + m)).
Es geht aber besser: Bezeichne `total die Gesamtanzahl der Zeichen. Wir können den Algorithmus so
modifizieren, dass er in O(`total + m) liegt.
Ideen für einen besseren Algorithmus
Sei wieder `max die Länge des Strings mit der größten Länge.
1. Sortiere die Strings Ai nach absteigender Länge `i .
2. Verwende `max -mal Bucketsort wie vorher, aber betrachte in Phase r nur die Strings Ai , für die
`i ≥ `max − r + 1 gilt (also die Strings, die an der aktuell betrachteten Stelle ein Zeichen haben,
weil sie genügend lang sind).
10
KAPITEL 1. BUCKETSORT
3. Um leere Buckets zu vermeiden, bestimme vorab die nötigen Buckets in jeder Phase und konkateniere am Ende einer Phase nur die nichtleeren Buckets. (Verringert den Aufwand zur Konkatenation auf O(#nichtleer) statt O(m).)
Algorithmus 1.3
Input: Strings (Tupel) A1 , . . . , An
ai j ∈ {0, . . . , m − 1}
Ai = (ai1 , ai2 , . . . , ai`i ),
(oder auch ein beliebiges anderes Alphabet)
`max = max `i
i
Output: Permutation B1 , . . . , Bn von A1 , . . . , An mit B1 ≤lex B2 ≤lex · · · ≤lex Bn
Methode:
1. Generiere ein Array von Listen NONEMPTY[] der Länge `max und für jedes `, 1 ≤ ` ≤ `max eine
Liste in NONEMPTY[`], die angibt, welche Zeichen an einer der `-ten Stellen vorkommen und
welche Buckets daher in der (`total − `)-ten Iteration benötigt werden.
Dazu:
1.1 Erschaffe für jedes ai` , 1 ≤ i ≤ n, 1 ≤ ` ≤ `i ein Paar (`, ai` ) (das bedeutet: das Zeichen ai`
kommt an `-ter Stelle in einem der Strings vor)
1.2 Sortiere die Paare lexikographisch mit Algorithmus 1.2, indem man sie als zweistellige
Strings betrachtet.
1.3 Durchlaufe die sortierte Liste der (`, ai` ) und generiere im Array NONEMPTY[], sortierte
Listen, wobei das Array NONEMPTY[`], 1 ≤ ` ≤ `max eine sortierte Liste aller ai` enthält.
Dabei lassen sich auch gleich auf einfache Weise eventuell auftretende Duplikate entfernen.
2. Bestimme Länge `i jedes Strings und generiere Listen LENGTH[`] aller Strings mit Länge ` (nur
Referenzen auf die Strings in LENGTH[`] verwalten, daher nur O(1) für Referenzen umhängen)
3. Sortiere Strings analog zu Algorithmus 1.2.3, beginnend mit `max . Aber:
• nach der r-ten Phase enthält Q nur die Strings der Länge ≥ `max − r + 1; diese sind lexikographisch korrekt sortiert bezüglich der letzten r Komponenten.
•
NONEMPTY [] wird benutzt, um die Listen in BUCKET[] neu zu generieren und außerdem
zur schnelleren Konkatenation der Einzellisten. Dies ist nötig, weil wir nur die nichtleeren
Buckets verwalten wollen.
• vor dem r + 1-ten Durchlauf wird LENGTH [`max − r] am Anfang2 der Queue Q eingefügt.
Die kurzen Strings stehen dann am Anfang und damit am lexikographisch richtigen Platz,
falls sie mit anderen im selben Bucket landen.
2 Das
ist zwar ein wenig ungewöhnlich, bereitet aber grundsätzlich keine Probleme.
11
1.2. SORTIEREN VON STRINGS MIT BUCKETSORT
Wir erinnern noch einmal: BUCKET[] ist ein Array von Queues, in das sortiert wird, und Q ist eine
Queue, die die Strings enthält, die zur Zeit betrachtet werden, also genügend lang sind.
Wir geben nun Pseudocode für Teil 3 von Algorithmus 1.3 an.
Algorithmus 1.4
1. Leere Q
2. for j:=0 to m − 1 do
2.1 leere BUCKET[j]
3. for `:=`max downto 1 do
3.1 Füge LENGTH[`] am Anfang von Q ein
3.2 while Q nicht leer do
3.2.1 Sei Ai erster String in Q
3.2.2 lösche Ai in Q und füge Ai in BUCKET[ai` ] ein
3.3 for jedes j in NONEMPTY[`] do
3.3.1 füge BUCKET[j] am Ende von Q ein
3.3.2 leere BUCKET[j]
Beispiel 1.5 Sortieren wir nun die gleichen Strings wie vorher, also a, bab und abc. Weil wir mit
Referenzen arbeiten, spielt die Stringlänge für das Einfügen keine Rolle.
Teil 1 des Algorithmus erzeugt durch einfaches Durchlaufen der Strings folgende Paare:
(1, a), (1, b), (2, a), (3, b), (1, a), (2, b), (3, c)
in der Liste. Daraus liefert Algorithmus 1.2 dann die sortierte Liste
(1, a), (1, a), (1, b), (2, a), (2, b), (3, b), (3, c)
Durch einfaches Durchlaufen dieser sortierten Liste von links nach rechts werden daraus die Listen
im Array NONEMPTY[] mit den richtigen Einträgen gefüllt:
= a, b
= a, b
NONEMPTY [3] = b, c
NONEMPTY [1]
NONEMPTY [2]
Dann werden die Längen der einzelnen Strings bestimmt:
`1 = 1, `2 = 3, `3 = 3
Mit Hilfe dieser Information erzeugt der Algorithmus dann das Array
eine Liste aller Strings der Länge ` enthält:
LENGTH [],
wobei
LENGTH [`]
12
KAPITEL 1. BUCKETSORT
= a
= 0/
LENGTH [3] = bab, abc
LENGTH [1]
LENGTH [2]
Nun führen wir den dritten Teil des Algorithmus aus, dessen Pseudocode wir angegeben haben:
Zuerst werden in Q alle Elemente von
letzten Stelle sortiert:
LENGTH [`max ]
=
LENGTH [3]
eingefügt und dann nach der
BUCKET[a]
= 0/
BUCKET[b] = bab
BUCKET[c] = abc
Durch das Array NONEMPTY[] wissen wir, dass wir das erste Bucket gar nicht betrachten müssen. Es
werden daher nur die letzten beiden Listen konkateniert und die Elemente am Ende von Q eingefügt.
Q enthält nun bab,abc. Es wird daraufhin LENGTH[2] = 0/ am Anfang von Q eingefügt.
Daraufhin wird Q nach der zweitletzten Stelle sortiert:
BUCKET[a]
= bab
= abc
BUCKET[c] = 0/
BUCKET[b]
Wieder wissen wir schon, dass das letzte Bucket leer ist und brauchen es daher nicht zu betrachten. Es
werden die ersten beiden Listen konkateniert und am Ende von Q eingefügt. Q enthält nun bab, abc.
Daraufhin wird LENGTH[1] = a am Anfang von Q eingefügt, Q enthält nun also a, bab, abc.
Daraufhin wird Q nach der ersten Stelle sortiert:
Q[a] = a, abc
Q[b] = bab
Q[c] = 0/
Dann werden die Listen konkateniert und wir erhalten als Ergebnis: a, abc, bab.
Zum Aufwand
Um die Paare zu generieren, müssen alle Strings durchlaufen werden und für jedes Zeichen in jedem
der Strings muss genau ein Paar erzeugt werden. Der Aufwand dafür ist
n
O(`1 + `2 + · · · + `n ) = O
∑ `i
!
= O(`total )
i=1
Beim Sortieren der Paare ist `max = 2. Dieser Teil erfordert einen Aufwand von
13
1.3. LITERATURHINWEISE

2. Komponente
1.Komponente
z
}|
{ z
}|
{
`max ≤`total
O 2 ·  `total + |{z}
m + `total + `max  ⊆ O(`total + m)
|{z}
|{z}
|{z}


Elemente
Buckets
Elemente
Buckets
Um NONEMPTY[] einzurichten, müssen wir nur die sortierte Liste der Paare einmal durchlaufen, das
geht also in O(`total ). Das Berechnen der Länge (O(`total )) und Erzeugen des Arrays LENGTH[] (das
lmax Elemente besitzt) geht in O(`total + `max ) ⊆ O(`total ).
Satz 1.3 Algorithmus 1.3 sortiert die Liste korrekt in O(`total + m) Zeit.
Beweis: Die Korrektheit ist klar, folgt wie in Algorithmus 1.2. Zum Aufwand:
O(`total )
+ O(`total + m) +
O(`total )
+
Teile 1,2
Paare generieren
Paare sortieren
NONEMPTY einrichten
O(`total )
LENGTH einrichten
Teil 3:
Sei n` die Anzahl der Strings, deren Länge `i größer gleich ` ist. Sei m` die Anzahl der verschiedenen
Symbole, die an der Stelle ` auftreten. m` ist dann auch gleichzeitig die Länge von NONEMPTY[`].
Ein Durchlauf der WHILE-Schleife 3.2 hat dann als Aufwand O(n` ) (weil sich in jeder Iteration n`
Elemente in Q befinden). Ein Durchlauf der FOR-Schleife 3.3 hat einen Aufwand von O(m` ) (weil
wir genau m` Buckets verbinden müssen).
`max
Der Aufwand des Pseudocodes ist dann insgesamt: O ∑ (m` + n` ) .
`=1
`max
O
!
∑ (m` + n` )
`=1
= O
`max
`max
`=1
`=1
∑ m` + ∑ n`
!
⊆ O(`total ) + O(`total )
⊆ O(`total )
1.3
Literaturhinweise
Die Darstellung von Bucketsort folgt [HU79]. Varianten von Bucketsort (Counting Sort, Radix Sort) und eine
average vase Analyse werden in [CLRS01] behandelt.
14
KAPITEL 1. BUCKETSORT
Kapitel 2
Bäume und Priority Queues
2.1
Bäume
Bisher haben wir als dynamische Datenstrukturen Listen kennengelernt. Da der Zugriff in Listen in
der Regel nur sequentiell erfolgen kann, ergibt sich für das Einfügen bzw. Suchen in einer (sortierten)
Liste bei Länge n ein linearer Aufwand. Das heißt:
O(n) im worst case und
O(n/2) im average case.
Dies ist für viele Anwendungen, in denen ein sich dynamisch ändernder Datenbestand verwaltet werden muss, zu langsam (z.B. Verwaltung von Identifiern in einem Programm durch den Compiler,
Autorenkatalog einer Bibliothek, Konten einer Bank, usw.). Bessere Methoden bietet unter anderem
die Datenstruktur der Bäume, die in diesem Kapitel erläutert wird.
2.1.1
Grundbegriffe
Gerichtete Bäume (kurz Bäume) kann man auf zwei Arten erklären. Eine graphentheoretische Definition 1 wurde bereits in der Coma I im Zusammenhang mit Graphen behandelt. Etwas abstrakter ist
die rekursive Definition, die in der Coma I in Zusammenhang mit der Rekursion erläutert wurde. Sie
wird hier noch einmal erklärt und in Abbildung 2.1 visualisiert:
1 Ein
gerichteter Baum ist ein Digraph T = (V, E) mit folgenden Eigenschaften:
– Es gibt genau einen Knoten r, in dem keine Kante endet (die Wurzel von T ).
– Zu jedem Knoten i 6= r gibt es genau einen Weg von der Wurzel r zu i.
Dies bedeutet, dass keine zwei Wege in den gleichen Knoten einmünden. Der Graph kann sich ausgehend von der Wurzel
also nur verzweigen. Daher kommt auch der Name Baum.
15
16
KAPITEL 2. BÄUME UND PRIORITY QUEUES
Ein Baum T
• ist entweder leer
• oder er entsteht aus endlich vielen, voneinander verschiedenen Bäumen T1 , . . . , Tn mit Wurzeln
w1 , . . . , wn , die in T als Teilbäume unter der Wurzel w von T (einem neuen Knoten) hängen.
w1
wn
A
A
T1 A
Tn A
... A
A
A
A
A
A
w r
@
@
=⇒
w1
...
A
T1 A
A
A
A
@
@ wn
A
Tn A
A
A
A
Abbildung 2.1: Baum, rekursiv aufgebaut
Beispiele für die Verwendung von Bäumen sind:
• Darstellung von Hierarchien
• Auswertung arithmetischer Ausdrücke
z.B.: ((a + b) ∗ (c + d))/e + f /g (siehe Abb. 2.6, Seite 24)
• Rekursionsbaum
Im Zusammenhang mit Bäumen ist die folgenden Terminologie üblich: Blätter, innere Knoten, Wurzel, Kinder / Söhne / Brüder, Vater / Eltern, Nachfolger, Vorgänger und Teilbäume. Ein Knoten v kann
einen Vater und Söhne haben. Die Söhne eines Vaters sind Brüder. Hat ein Knoten keinen Vater, ist
er die Wurzel des Baumes. Hat er keine Söhne, ist er ein Blatt. Wenn ein Knoten verschieden von der
Wurzel ist und mindestens einen Sohn hat, ist er ein innerer Knoten.
Eine besondere Rolle spielen die binären Bäume. Sie sind entweder leer oder bestehen aus der Wurzel
und einem linken und einem rechten binärem Baum (den Teilbäumen). Jeder Knoten hat maximal
zwei Söhne, man spricht vom linken und vom rechten Sohn. In den folgenden Abschnitten werden
wir ausschließlich binäre Bäume behandeln und deshalb das Wort Baum in der Bedeutung binärer
Baum verwenden. Bekannte Beispiele binärer Bäume sind der Stammbaum mit Vater, Mutter und
einer Person als deren Nachfolger (!) oder die Aufzeichnung eines Tennisturniers, in der jedes Spiel
durch einen Knoten mit dem Namen des Gewinners charakterisiert ist und die beiden vorausgehenden
Spiele als dessen Nachfolger aufgeführt sind.
Die rekursive Struktur von Bäumen ist von großer Bedeutung für viele Algorithmen auf Bäümen.
Auch viele charakteristische Größen von Bäumen lassen sich rekursiv beschreiben oder definieren.
17
2.1. BÄUME
Ein Beispiel dafür ist die Höhe von Bäumen. Die Höhe gibt den längsten Weg von der Wurzel bis zum
Blatt gemessen in Anzahl der Kanten an. Sie ergibt sich wie folgt:
h(T ) =
n
−1
falls T leer
max{h(T1 ), h(T2 )} + 1
sonst
(2.1)
Besteht T beispielsweise nur aus einem Knoten, ergibt sich aus Gleichung (2.1) die Höhe von T zu
h(T ) = max{−1, −1} + 1 = 0.
2.1.2
Implementation von binären Bäumen
Im Folgenden wird gezeigt, wie sich binäre Bäume als abstrakte Datenstruktur implementieren lassen.
Ein Baum besteht aus Knoten und Kanten zwischen den Knoten. Die Knoten sind hier Objekte der
inneren Klasse BinTreeNode. Für die Kanten nutzt man die Zeigereigenschaft von Referenzobjekten.
So kennt ein BinTreeNode das Objekt, das im Knoten steht, seinen linken und seinen rechten Sohn und
in manchen Implementationen auch seinen Vater. Das wird in Abbildung 2.2 deutlich. Zusätzlich sind
get und set Methoden sinnvoll sowie Methoden, die testen, ob der linke bzw. rechte Sohn vorhanden
sind.
class BinTreeNode {
Object
BinTreeNode
BinTreeNode
data;
lson;
rson;
// saved object
// left son
// right son
// sometimes also usefull
BinTreeNode
parent; // parent
...
// constructors, get methods,
// set methods ...
}
Objekt
r
Ref. auf
linken Sohn
r
A
AAU
Ref. auf
rechten Sohn
Abbildung 2.2: Struktur eines Knotens
Wie in Abb. 2.3 dargestellt, ist ein Baum eine Verzeigerung“ von Knoten. Jeder BinTreeNode zeigt
”
auf seine Söhne und, wie oben schon erwähnt, in manchen Implementationen auch auf seinen Vater.
18
KAPITEL 2. BÄUME UND PRIORITY QUEUES
Es gibt einen BinTreeNode, hier root“ genannt, dessen rechter (oder linker) Sohn immer auf die
”
eigentliche Wurzel des Baumes zeigt. Zusätzlich gibt es eine Referenz curr“ (lies: karr), die auf
”
einen beliebigen Knoten im Baum zeigt und die auf jeden Knoten umgesetzt werden kann.
root
qH
H
HH
j
Objekt
q
q
Q
Q
q
Q
+
Q
s
Q
Objekt
q
q
Objekt
q
@
@
R
@
Objekt
q
q
@
R
@
q
@
@
R
@
Objekt
q
@
curr
q
@
...
@
R
@
...
Abbildung 2.3: Baum, dargestellt als verkettete Struktur
class BinTree {
BinTreeNode dummy;
BinTreeNode curr;
// dummy node whose left son is the root
// points at the current node
...
}
Das folgende Programm 2.1 stellt ein Beispiel einer abstrakten Klasse dar, von der binäre Bäume
abgeleitet werden können. Einige Methoden werden im Folgenden genauer erklärt.
Programm 2.1 BinTree
/**
* abstract base class for all sorts of binary trees
*
* @author N.N.
*/
abstract class BinTree {
/**
* class for tree nodes
*/
protected class BinTreeNode {
19
2.1. BÄUME
public BinTreeNode() {
}
// default constructor
public BinTreeNode(Object obj) { // init constructor
}
public boolean isLeaf() {
}
// is node a leaf in tree?
public boolean isRoot() {
}
// is node root of tree?
public boolean isLeftChild() {
}
// is node left child
// of parent?
public BinTreeNode getLeftChild() {
}
// get left child
public BinTreeNode getRightChild() { // get right child
}
public BinTreeNode getParent() {
}
public String toString() {
}
}
// get parent
// conversion to string
// class BinTreeNode
/***
data
******************************************************/
/***
constructors
**********************************************/
// default constructor, initializes empty tree
public BinTree() {
}
/***
get methods
***********************************************/
public boolean isEmpty() {
}
// is tree empty?
20
KAPITEL 2. BÄUME UND PRIORITY QUEUES
// root node of tree
// -> what should be returned if tree is empty??
protected BinTreeNode _getRoot() {
}
// current number of tree nodes
public int getSize() {
}
// height of tree
public int getHeight() {
}
/***
set methods
***********************************************/
// switch debugging mode
public static void setCheck(boolean mode) {
}
/***
methods for current node
**********************************/
// reset current node to first node in inorder sequence
public void reset() {
}
// does current node stand at end of inorder sequence?
public boolean isAtEnd() {
}
// reset current node to successor in inorder sequence
public void increment() {
}
// object referenced by current node
public Object currentData() {
}
// ist current node a leaf?
public boolean isLeaf() {
}
21
2.1. BÄUME
/***
conversion methods
****************************************/
// convert tree to string
// use getClass() somewhere so that class name of "this" shows
public String toString() {
}
/***
debugging methods
*****************************************/
// check consistency of links in entire tree
protected boolean _checkLinks() {
}
}
Es gibt viele Methoden, die man an oder mit Bäumen durchführen kann. Dazu gehören beispielsweise Methoden zum Einfügen und Löschen von Knoten, zum Durchlaufen des Baumes (vgl. Abschnitt 2.1.3 usw. Wir wollen uns eine mögliche Methode zum Berechnen der Höhe eines Baumes
genauer anschauen. Diese benutzt die Gleichung 2.1 zur Berechnung der Höhe und nutzt die rekursive Struktur von Bäumen.
Programm 2.2 getHeight()
int getHeight() {
if (isEmpty()){
// empty tree
return -1;
} else {
int lheight = _getRoot().getLeftSon().getHeight();
int rheight = _getRoot().getRightSon().getHeight();
return Math.max(rheight,lheight)+1;
}
}
Implementation im Array
Bäume können auch mit Hilfe von Arrays implementiert werden. Hierbei handelt es sich zwar nicht
um eine dynamische Datenstruktur, diese Umsetzung ist allerdings für manche Programmiersprachen
(z.B. FORTRAN) erforderlich. Die Idee hierbei ist, die Indizes als Zeiger auf die Söhne zu nutzen. Das
lässt sich explizit (durch Abspeicherung) oder implizit (durch Berechnung) lösen. Bei der expliziten
Variante sehen die Knoten so aus:
class ArrayBinTreeNode {
Object data;
int
lson;
22
KAPITEL 2. BÄUME UND PRIORITY QUEUES
int
rson;
}
Der Baum wird dann, wie auch in Abbildung 2.4 veranschaulicht, als Array umgesetzt:
ArrayBinTreeNode[] tree = new ArrayBinTreeNode[n];
0
1
i
s
...
n−2 n−1
j
...
?
Objekt
i
...
j
Abbildung 2.4: Baum als Array
Dazu gehören natürlich noch die oben schon dargestellten Zugriffsfunktionen. Die Höhe wird ebenfalls auf die schon erklärte Weise rekursiv berechnet.
Bei der impliziten Variante werden die beiden Söhne nicht im Knoten gespeichert, sondern in getMethoden berechnet. Die Indizes der Söhne des Knoten i ergeben sich bei binären Bäumen immer zu
2i + 1 für den linken Sohn und 2i + 2 für den rechten Sohn.
Der Nachteil an einer Implementation mit Arrays ist leider, dass man bei nicht vollen Bäumen im
Vergleich zur üblichen Implementation mehr Speicherplatz benötigt.
2.1.3
Traversierung von Bäumen
Mit Traversierung eines Baumes bezeichnet man den Durchlauf von Knoten zu Knoten, um in jedem
Knoten etwas zu tun. In den Knoten sind Daten, ähnlich wie in einer Liste, und um mit diesen arbeiten
zu können, müssen sie nacheinander erreicht werden. Jedoch ist die Reihenfolge des Durchlaufens
eines Baumes nicht mehr eindeutig wie bei einer Liste. Standardmäßig benutzt man die folgenden
drei Traversierungen:
WLR: Der Preorder-Durchlauf. Hier wird zuerst die Wurzel betrachtet, dann der linke Teilbaum mit
derselben Regel und dann der rechte Teilbaum wieder mit der selben Regel.
LWR: Der Inorder-Durchlauf. Hier wird zuerst der linke Teilbaum, dann die Wurzel und dann der
rechte Teilbaum besucht, wobei die Teilbäume wieder mit derselben Regel durchlaufen werden.
LRW: Der Post-Durchlauf. Die Wurzel wird erst erreicht, nachdem zuerst der linke und dann der
rechte Teilbaum jeweils mit derselben Regel durchlaufen wurden.
Die Kürzel WLR, LWR und LRW zeigen vereinfacht jeweils die Reihenfolge des Durchlaufens an.
Die Vorsilben Pre-, In- und Post- beziehen sich jeweils auf die Rolle der Wurzel.
23
2.1. BÄUME
A
B
D
C
E
F
Abbildung 2.5: Beispielbaum für die Traversierung
Beispiel 2.1 Dieses Beispiel zeigt die drei Traversierungsmöglichkeiten für den Baum in Abbildung 2.5.
WLR: A, B, D, E, C, F
LWR: D, B, E, A, C, F
LRW: D, E, B, F, C, A
Ist es einfach nur wichtig, unabhängig von der Reihenfolge alle Knoten zu erreichen, spielt es keine
Rolle, welche Traversierung gewählt wird. Allerdings gibt es verschiedene Anwendungen, die jeweils
unterschiedliche Reihenfolgen benutzen. Beim Aufrufbaum oder beim Rekursionsbaum beispielsweise, die in Coma I behandelt wurden, werden die Methoden in Postorder Reihenfolge abgearbeitet. Im
folgenden Beispiel wird verdeutlicht, welchen Einfluss die verschiedenen Reihenfolgen auf arithmetische Ausdrücke haben.
Beispiel 2.2 Der arithmetische Ausdruck
((a + b) ∗ (c + d))/e + f /g
wird vom Compiler in einen Baum, wie in Abb. 2.6, umgewandelt. In diesem Baum stehen die Identifier in den Blättern. In den inneren Knoten und der Wurzel stehen Operatoren. Diese verknüpfen jeweils ihren linken Teilbaum als arithmetischen Ausdruck mit dem Ausdruck ihres rechten Teilbaums.
Durchläuft man den Baum in Inorder, ergibt sich der arithmetische Ausdruck in Infix-Notation:
((a + b) ∗ (c + d))/e + f /g
Durchläuft man den Baum aber in Postorder, erhält man den Ausdruck in Postfix-Notation beziehungsweise umgekehrter polnischer Notation (UPN):
ab + cd + ∗e/ f g/+
Dieser wird dann vom Computer, wie in Coma I behandelt, mit Hilfe eines Stacks berechnet.
Im Gegensatz zur Infix-Notation ist der Baum aus der Postfix-Notation arithmetischer Ausdrücke ohne Hilfe von Klammern (re)konstruierbar. Indem man den Ausdruck in Postfix-Notation von hinten
durchläuft, kann man den Baum über die Postorder-Reihenfolge von hinten nach vorne (wieder) aufbauen.
24
KAPITEL 2. BÄUME UND PRIORITY QUEUES
+
/
/
e
∗
+
a
f
g
+
b c
d
Abbildung 2.6: Ein arithmetischer Ausdruck als Baum dargestellt
Implementation
Um einen Baum in den verschiedenen Reihenfolgen zu durchlaufen, kann man sich in den JavaMethoden die rekursive Struktur der Bäume nützlich machen. Die Umsetzung zeigt die folgenden
Methoden, die sinnvollerweise zur Klasse BinTree gehören.
Programm 2.3 Traversierung eines Baumes
void preOrderTraversal() {
if (isEmpty()) {
return;
}
// work on root
getLeftSon().preOrderTraversal();
getRightSon().preOrderTraversal();
}
void inOrderTraversal(){
if (isEmpty()) {
return;
}
getLeftSon().inOrderTraversal();
// work on root
getRightSon().inOrderTraversal();
}
void postOrderTraversal() {
2.2. PRIORITY QUEUES
25
if (isEmpty()) {
return;
}
getLeftSon().postOrderTraversal();
getRightSon().postOrderTraversal();
// work on root
}
}
Neben den rekursiven Methoden gibt es auch die Möglichkeit den Baum iterativ zu durchlaufen. Exemplarisch wird hier nur die Inorder Traversierung angesprochen. Die Umsetzung wird in der Übung
behandelt. Zur iterativen Traversierung werden drei Methoden benötigt:
1. public void reset()
2. public void increment()
3. public boolean isAtEnd()
Die Methode reset() sucht sich den am weitesten links stehenden Knoten des Baumes und setzt den
curr-Zeiger auf diesen Knoten. Die Methode increment() setzt den curr-Zeiger auf den Nachfolger,
also auf den nächsten Knoten entsprechend der Inorder-Reihenfolge. Die Methode isAtEnd() prüft,
ob der Inorder-Durchlauf das Ende erreicht hat. Objekte mit solchen Methoden bezeichnet man als
Iterator und die Methoden werden dementsprechend Iteratormethoden genannt.
2.2
Priority Queues
Bei einer Priority Queue handelt es sich um eine Datenstruktur mit folgenden Kennzeichen:
• Sie hat einen homogenen Komponententyp, wobei jede Komponente einen Schlüssel (Wert)
besitzt.
• Die folgenden Operationen sind möglich:
1. Einfügen einer Komponente
2. Zugriff auf die Komponente mit dem kleinsten Wert
3. Entfernen der Komponente mit dem kleinsten Wert
4. Änderung des Wertes einer Komponente
Die Priority Queue wurde schon in Coma I im Zusammenhang mit Heapsort behandelt. Jedoch lag
dort die Aufmerksamkeit auf der Komponente mit dem größten Wert, nicht auf der mit dem kleinsten
Wert.
26
2.2.1
KAPITEL 2. BÄUME UND PRIORITY QUEUES
Mögliche Implementationen einer Priority Queue
a) Als sortiertes Array
Wenn die Anzahl n der zu speichernden Elemente bekannt ist, können die Elemente in einem Array,
Abb. 2.7, gespeichert werden, wobei das kleinste Element in der ersten Komponente des Arrays steht
und die übrigen aufwärts sortiert folgen. Damit ist ein sehr schneller Zugriff auf das kleinste Element
gewährleistet, jedoch dauern die übrigen Operationen lange, wie in der folgenden Auflistung zu sehen
ist.
1. Einfügen:
O(n) (binäre Suche + Verschieben)
2. Zugriff:
O(1)
3. Entfernen:
O(n)
4. Wert ändern: O(n)
0
1
2
3
4
5
6
7
12 18 24 35 44 53 63 72
6
kleinstes
Element
Abbildung 2.7: Priority Queue als sortiertes Array
Eine bessere Variante ist die folgende:
b) Als Heap
Wie bei Heapsort wird das Array als Baum mit Heap-Eigenschaft aufgefasst. Die Heapeigenschaft ist
dann erfüllt, wenn die Wege von der Wurzel zu jedem Blatt jeweils aufsteigend sortiert sind. Zur Herstellung der Heapeigenschaft wird die Methode heapify()“ verwendet. Ihre genauere Funktionsweise
”
wurde bereits in Coma I erläutert.
0
12
1
18
35
4
HH
H
2
53
63
6
@
@
3
7
24
@
@
5
72
44
Abbildung 2.8: Priority Queue als Heap
Für die Operationen im Heap ergibt sich dann dieser Aufwand im worst case:
27
2.3. LITERATURHINWEISE
1. Einfügen:
O(log n)
2. Zugriff:
3. Entfernen:
4. Wert ändern:
O(1)
O(log n)
O(log n)
als Blatt in die letzte Arraykomponente einfügen
und nach oben wandern lassen
letzte Komp. an die 0-te Stelle tauschen und absinken lassen
aufsteigen oder absinken lassen
Also sind neben dem sehr schnellen Zugriff auf das kleinste Element auch die anderen Operationen
schneller als im sortierten Array.
Es gibt aber noch andere Implementationen, die die Operationen noch schneller, allerdings nur amortisiert, schaffen. Dazu gehören zum Beispiel die Fibonacci Heaps.
2.3
Literaturhinweise
Bäume und Priority Queues werden in jedem Buch über Datenstrukturen behandelt, vgl. etwa [CLRS01, Knu98,
OW02, SS02].
28
KAPITEL 2. BÄUME UND PRIORITY QUEUES
Kapitel 3
Huffman Codes und Datenkompression
Das Ziel der Datenkompression ist es, Daten mit weniger Speicherplatz abzuspeichern. Abhängig von
den Daten geschieht das verlustfrei oder nicht verlustfrei. Audio-, Video- und Bilddateien werden
in der Regel komprimiert, indem Informationen weggelassen werden. Das Prinzip bei MP3-Dateien
beispielsweise ist es, all die Informationen wegzulassen, die das menschliche Ohr nicht wahrnehmen kann. Somit stellt der Informationsverlust keinen Qualitätsverlust für die gespeicherte Musik
dar. Textdateien möchte man ohne Verlust von Informationen komprimieren. Um zu verstehen, wie
das funktioniert, muss man verstehen, wie Texte abgespeichert werden. Der folgende Abschnitt soll
Aufschluss darüber geben.
3.1
Codierung
Wir betrachten einen Zeichensatz C (z.B. das Alphabet, ein Java Zeichensatz, alle Wörter im Duden)
und ein Zeichen c ∈ C. Diese Zeichen werden im Computer mit Codewörtern codiert, und zwar über
dem Alphabet {0, 1}. Die Gesamtheit der Codewörter für den Zeichensatz C heißt dann Code für C.
Üblicherweise haben die Codewörter aller Zeichen eines Zeichensatzes C die selbe Länge. Ein solcher
Code heißt Blockcode. So sind auch der ASCII Code, bei dem alle Codewörter die Länge 8 haben,
und der Unicode, bei dem alle Codewörter die Länge 16 haben, Blockcodes.
Beispiel 3.1 (Blockcode der Länge 3)
Wenn C = {a, b, c, d, e, f , g, } ist, ist
a
b
c
d
e
f
g
−→
−→
−→
−→
−→
−→
−→
−→
29
000
001
010
011
100
101
110
111
30
KAPITEL 3. HUFFMAN CODES UND DATENKOMPRESSION
ein zugehöriger Blockcode der Länge 3.
Es gibt auch Codes, bei denen die Länge der Codewörter der einzelnen Zeichen eines Zeichensatzes
C unterschiedlich lang ist, die sogenannten variable length codes.
Beispiel 3.2 ( variable length code“)
”
Wir betrachten wieder C = {a, b, c, d, e, f , g, }.
a
b
c
d
e
f
g
−→
−→
−→
−→
−→
−→
−→
−→
0
1
10
11
100
101
110
111
Eine Textdatei ist nun eine Folge von Zeichen c ∈ C. Um sie abzuspeichern, muss sie verschlüsselt
werden. Die Verschlüsselung oder auch Codierung entspricht dem Ersetzen jedes Zeichens durch
sein Codewort. Damit die Datei später wieder lesbar ist, müssen die Codewörter wieder in die Zeichen des Zeichensatzes umgewandelt werden. Die Entschlüsselung bzw. Decodierung entspricht dem
umgekehrten Prozess. Das Codieren/Decodieren einer Nachricht entspricht also einer bijektiven Abbildung.
Beispiel 3.3 (Codierung/Decodierung mit Blockcode)
Angenommen, die Datei besteht aus diesen Zeichen:
fa gaga gaff fege
Wird die Datei mit der Codetabelle von Beispiel 3.1 codiert, ergibt sich diese Bitfolge:
f
a
g
a
g
a
g
a
f
f
f
e
g
e
z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{
101 000 111 110 000 110 000 111 110 000 101 101 111 101 100 110 100
Die Decodierung ist hier sehr einfach, da wir wissen, dass drei Bits immer einem Zeichen entsprechen.
Es entsteht wieder eindeutig unsere Datei:
000 |{z}
111 |{z}
101 |{z}
111 |{z}
100
101 |{z}
000 |{z}
111 |{z}
110 |{z}
000 |{z}
110 |{z}
110 |{z}
000 |{z}
101 |{z}
101 |{z}
100 |{z}
110 |{z}
|{z}
f
a
g
a
g
a
g
a
f
f
f
e
g
e
31
3.1. CODIERUNG
Beispiel 3.4 (Codierung / Decodierung mit variable length code)
Betrachten wir jetzt diese beiden Dateien:
Datei 1: abba
Datei 2: ag
Mit der Codetabelle von Beispiel 3.2 codiert, sehen sie so aus:
Datei 1:
(abba)
0110
(a g )
Datei 2: 0110
Doch mit der Decodierung wird es schwer, denn die Dateien sind nicht mehr eindeutig entschlüsselbar.
Man sagt, der Code von Beispiel 3.2 ist nicht eindeutig entzifferbar.
Ein Code heißt also eindeutig entzifferbar, wenn verschiedene Dateien, codiert mit dem selben Code, auch zu verschiedenen Codierungen führen. Der Grund dafür, dass der Code von Beispiel 3.2
nicht eindeutig entzifferbar ist, liegt in der Beschaffenheit der Codewörter. Das Problem besteht darin, dass es Codewörter gibt, die auch durch Zusammensetzen anderer Codewörter entstehen können.
So ist zum Beispiel das Codewort von g, die 110, identisch mit den hintereinander geschriebenen Codewörtern von d und a, die auch 110 ergeben. Es gibt keine Möglichkeit zu unterscheiden, ob dort ein
g oder ein d und ein a steht. Eine Lösung für dieses Problem sind präfixfreie Codes.
3.1.1
Präfixcode
Ein Code heißt Präfixcode1 , wenn kein Codewort Anfangsstück eines anderen Codewortes ist. Jeder
Blockcode ist zum Beispiel ein Präfixcode, weil kein Codewort in einem anderen enthalten sein kann.
Aber auch variable length codes können präfixfrei sein.
Lemma 3.1 Jeder Präfixcode ist eindeutig entzifferbar.
Beweis: Beim Lesen der codierten Datei ist eindeutig klar, wann ein Codewort zu Ende ist. Dann lässt
sich eindeutig sagen, welches Zeichen mit diesem Codewort codiert wurde.
Die Umkehrrichtung von Lemma 3.1 gilt aber nicht. Nicht jeder eindeutig entzifferbare Code muss
ein Präfixcode sein. Das zeigt das folgende Beispiel:
1 Der Name ist etwas verwirrend, denn eigentlich müsste der Code präfixfreier Code heißen. Aber Präfixcode hat sich in
der Literatur durchgesetzt.
32
KAPITEL 3. HUFFMAN CODES UND DATENKOMPRESSION
Beispiel 3.5 Wir betrachten diesen Code:
−→
−→
−→
a
b
c
1
100000
00
Auch wenn dieser Code nicht präfixfrei ist, kann man codierte Dateien wieder decodieren, indem man
die Nullen zählt. Steht hinter einer Eins eine gerade Anzahl von Nullen, handelt es sich um ein a mit
entsprechend vielen c’s dahinter. Steht aber hinter einer Eins eine ungerade Anzahl von Nullen, und
zwar mindestens fünf, handelt es sich um ein b mit entsprechend vielen c’s dahinter. Dafür ist es aber
unter Umständen nötig, sich erst die ganze Datei anzusehen, um zu wissen wie viele Nullen nach den
Einsen kommen. Es ist also möglich, den folgenden codierten Text zu entschlüsseln:
1 |{z}
00 |{z}
00 |{z}
1 |{z}
00 100000
|{z}
| {z }
a
c
c
a
c
b
Im weiteren Verlauf werden wir uns nur noch mit Präfixcodes befassen, da vor allem diese in der
Praxis üblich sind.
Lemma 3.2 Präfixcodes lassen sich mit binären Bäumen T identifizieren, bei denen die Zeichen c ∈ C
in den Blättern stehen und die Wege von der Wurzel bis zum Blatt die Codewörter bilden (eine Kanten
nach links entspricht der 0, eine Kante nach rechts entspricht der 1).
Beweis:
=⇒: Sei C ein Zeichensatz mit einem zugehörigen Präfixcode. Konstruiere einen Baum T , in dem
pro Codewort der Weg gegangen wird, der durch die 0 bzw. die 1 vorgegeben ist. Bei der 0 gehe
nach links, bei der 1 nach rechts. Da der Code präfixfrei ist, endet man immer in einem Blatt.
Es ergibt sich also ein Baum mit den geforderten Eigenschaften.
⇐=: Sei C ein Zeichensatz und T ein Baum mit den Zeichen c ∈ C in seinen Blättern. Betrachte
den Weg von der Wurzel zu den Zeichen c als Codewort für jedes einzelne Zeichen, wobei
der Weg nach links der 0 und der Weg nach rechts der 1 entspricht. Da die Zeichen in den
Blättern des Baumes stehen, ist klar, dass kein Weg zu einem Zeichen in einem Weg zu einem
anderen Zeichen enthalten sein kann. Daher kann kein Codewort Anfangsstück eines anderen
Codewortes sein. Also handelt es sich bei dem Code um einen Präfixcode.
Beispiel 3.6 Der Baum T in Abb. 3.1 entspricht diesem Präfixcode:
a
b
c
d
e
−→
−→
−→
−→
−→
00
01
100
11
101
33
3.2. DER HUFFMAN ALGORITHMUS
1
0
0
1
a
0
0
b
1
1
c
d
e
Abbildung 3.1: Präfixcode als Baum
Kommen wir nun zurück zur Frage der Komprimierung. Der benötigte Speicherplatz für eine Textdatei
entspricht immer der Anzahl der Bits bezüglich ihrer Codierung.
Betrachten wir noch einmal Beispiel 3.3
fa gaga gaff fege
und codieren es mit dem Code von Beispiel 3.1,
f
a
g
a
g
a
g
a
f
f
f
e
g
e
z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{
101 000 111 110 000 110 000 111 110 000 101 101 111 101 100 110 100
so benötigen wir 17 · 3 = 51 Bits. Codiert man aber das Beispiel mit dem Code von Beispiel 3.2,
f
a
g
a
g
a
g
a
f
f
f
e
g
e
z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{ z}|{
101 0 111 110 0 110 0 111 110 0 101 101 111 101 100 110 100
benötigt man 43 Bits.
Verschiedene Codierungen nehmen also unterschiedlich viel Speicherplatz in Anspruch. Bei der Komprimierung wird also (dateiabhängig) ein Code gefunden, der den Speicherplatz reduziert. Dieser Code muss natürlich ein Präfixcode sein, damit die Datei wieder einfach decodierbar ist.
3.2
Der Huffman Algorithmus
Textdateien werden standardmäßig im Blockcode codiert und gespeichert. Der Huffman Algorithmus
konstruiert einen Präfixcode variabler Länge, so dass die Anzahl der benötigten Bits kleiner wird.
Der Code wird so konstruiert, dass Zeichen, die sehr häufig auftreten, kurze Codewörter bekommen,
und weniger häufige Zeichen längere Codewörter. So wird, abhängig von der Datei, der benötigte
Speicherplatz verringert. Der Huffman Algorithmus ist sogar so gut, das der entstehende Präfixcode
optimal bezüglich des benötigten Speicherplatzes ist. Es sind Speicherplatzeinsparungen von 20% bis
90% üblich, je nach Beschaffenheit der Datei.
34
KAPITEL 3. HUFFMAN CODES UND DATENKOMPRESSION
Um den optimalen Präfixcode zu konstruieren, muss die Datei erst einmal gelesen werden, wobei die
Häufigkeiten f (c) für alle Zeichen c ∈ C ermittelt werden. Ist die Häufigkeit der Zeichen bekannt,
und wird die Datei mit dem Präfixcode T (als binärer Baum aufgefasst) codiert, so ergibt sich der
benötigte Speicherplatz der Dateiwie folgt:
B(T ) =
∑ f (c) · hT (c)
(3.1)
c∈C
Dabei gibt hT (c) die Höhe des Zeichens c im Baum T an, also die Anzahl der Kanten des Baumes
von der Wurzel bis zu dem Blatt, in der das Zeichen c steht, an. Dies entspricht nach Lemma 3.1 der
Länge des Codewortes für das Zeichen c.
Beispiel 3.7 zeigt, wie der Speicherplatz bei unterschiedlichen Codierungen variieren kann.
Zeichen c
a
b
c
d
e
f
f (c) in 1000
45
13
12
16
9
5
Code 1
000
001
010
011
100
101
Code 2
0
101
100
111
1101
1100
Code 1
0
0
a
0
1
1
0
b c
Code 2
1
0
a
0
1
d
0
e
1
1
f
0
c
0
1
1
0
1
b
d
0
1
f
e
Abbildung 3.2: Die Bäume der beiden Codes
Dann ergibt sich der benötigte Speicherplatz für Code 1 zu:
B(T1 ) = (45 + 13 + 12 + 16 + 9 + 5) · 1000 · 3 = 300000
Der benötigte Speicherplatz für Code 2 beträgt hingegen:
B(T2 ) = (45 · 1 + 13 · 3 + 12 · 3 + 16 · 3 + 9 · 4 + 5 · 4) · 1000 = 224000
35
3.2. DER HUFFMAN ALGORITHMUS
Wir wollen wir uns nun Huffman Algorithmus ansehen, der für jede Textdatei einen optimalen Präfixcode
ermittelt.
Algorithmus 3.1 (Huffman Algorithmus)
1. Fasse jedes Zeichen c ∈ C als einelementigen Baum auf und füge es in eine Priority Queue Q
ein, wobei die Häufigkeit f (c) als Schlüsselwert dient.
2. Solange Q mehr als einen Baum enthält:
• Wähle die beiden Bäume T1 und T2 mit den kleinsten Häufigkeiten (muss nicht eindeutig
sein).
• Entferne sie aus Q.
• Konstruiere einen neuen Baum aus den T1 , T2 als Teilbäume unter einer neuen Wurzel und
gebe ihm die Häufigkeit f (T1 ) + f (T2 ).
• Füge diesen Baum in Q ein.
3. Gebe (den einzig übrig gebliebenen Baum) T in Q zurück. Dieser Baum (der so genannte Huffman Baum oder Huffman Code) ist ein optimaler Präfixcode.
Bei der Codierung einer Datei gemäß T muss neben der codierten Datei natürlich der Code z.B. als
Baum mit abgespeichert werden, denn er ist zur Decodierung notwendig. Der Speicherplatz dafür ist
aber bei genügend großen Dateien im Vergleich zum eingesparten Speicherplatz so gering, dass er
vernachlässigt werden kann.
Das folgende Beispiel zeigt, wie der Huffman Algorithmus funktioniert:
Beispiel 3.8 (Konstruktion eines Huffman Baumes)
Zeichen c
f (c)
a
45
b
13
c
12
d
16
e
9
f
5
Die Zeichen werden alle als einknotige Bäume mit f (c) als Schlüssel in die Priority Queue Q eingefügt:
45
Q:
a
13
b
12
c
16
d
9
e
5
f
Die beiden Bäume mit den kleinsten Schlüsseln werden aus Q entfernt, zu einem neuen Baum zusammengefügt und wieder in Q eingefügt:
36
KAPITEL 3. HUFFMAN CODES UND DATENKOMPRESSION
45
Q:
a
13
12
b
16
14
d
c
f
e
Das wird fortgesetzt, bis nur noch ein Baum in Q ist:
45
Q:
25
14
d
b
c
45
Q:
16
a
f
e
25
30
a
b
d
c
f
e
45
Q:
55
a
b
c
d
e
f
37
3.2. DER HUFFMAN ALGORITHMUS
45
Q:
a
b
d
c
f
e
Am Ende wird der fertige Huffman Baum von Q entfernt:
45
a
b
c
d
e
f
Die Laufzeit des Huffman Algorithmus
Ist die Priority Queue als Heap implementiert (siehe Abschnitt 2.2.1 auf Seite 26) und hat man n Zeichen, so ergibt sich folgende Laufzeit für den Huffman Algorithmus:
1.
2.
alle Zeichen einfügen in Q:
n − 1 Phasen:
- die beiden Kleinsten aus Q entfernen:
- neuen Baum bauen:
- wieder einfügen in Q:
3. Baum zurück geben:
insgesamt:
O(3n)
n − 1 mal:
O(2 log n)
O(1)
O(log n)
O(1)
O(n log n)
38
KAPITEL 3. HUFFMAN CODES UND DATENKOMPRESSION
Die Optimalität des Huffman Codes
Ein Präfixcode T heißt optimal für einen Zeichensatz C und Häufigkeiten f (c), c ∈ C, wenn
B(T ) ≤ B(T 0 ),
(3.2)
für jeden anderen Präfixcode T 0 (zu C und denselben Häufigkeiten f (c)).
Lemma 3.3 Sei C eine Menge von Zeichen mit den Häufigkeiten f (c). Seien x, y die Zeichen mit den
niedrigsten Häufigkeiten. Dann gibt es einen optimalen Präfixcode T , in dem x und y die größte Höhe
und gemeinsamen Vater haben. Die beiden Codewörter für x und y haben dann dieselbe Länge und
unterscheiden sich nur im letzten Bit.
Beweis:
Die Idee des Beweises ist es, einen Baum T zu betrachten, der einen optimalen Präfixcode repräsentiert
und ihn gegebenenfalls so zu modifizieren, dass er optimal bleibt und die beiden Zeichen x und y in
den Blättern mit der größten Höhe stehen und denselben Vater haben. Ist das möglich, sind die beiden
gewünschten Eigenschaften im optimalen Präfixcode erfüllt. Abbildung 3.3 skizziert diese Vorgehensweise.
T s
s
y
-
HH
Hs
s
@
x
@s
@
s
@s
a
b
s
y
T 0s
T 00s
-
HH
Hs
s
@
a
@s
@
s
@s
x
b
s
b
HH
Hs
s
@
a
@s
@
s
@s
x
y
Abbildung 3.3: Modifizieren des Baumes T für den Beweis von Lemma 3.3
Zunächst überlegen wir uns, dass es einen optimalen Präfixcode gibt. Dies folgt daraus, dass der Wert
B(T ) immer ganzzahlig und positiv ist. In dieser Menge von Zahlen existiert ein kleinster Wert B(T ),
und der zugehörige Baum T ist ein optimaler Präfixcode.
Sei also nun T ein optimaler Präfixcode. Sei a ein Blatt in T mit größter Höhe hT (a). Dann hat a
aufgrund der Optimalität von T einen Bruder b, denn hätte a keinen Bruder, könnte man a einen
Level höher hängen“ und hätte den Wert B(T ) verbessert, was im Widerspruch zur Optimalität von
”
T stünde. Betrachte nun x und y, die beiden Zeichen mit den geringsten Häufigkeiten. Vertauscht man
a und x im Baum T , so entsteht der Baum T 0 . Für den Speicherplatz der beiden Bäume gilt dann:
B(T ) − B(T 0 ) =
∑ f (c) · hT (c) − ∑ f (c) · hT (c)
0
c∈C
=
c∈C
f (x) · hT (x) + f (a) · hT (a) − f (x) · hT 0 (x) − f (a) · hT 0 (a)
| {z }
| {z }
=hT (a)
= ( f (a) − f (x)) · (hT (a) − hT (x))
|
{z
} |
{z
}
≥0
≥ 0
=⇒
B(T ) ≥ B(T 0 )
≥0
=hT (x)
39
3.2. DER HUFFMAN ALGORITHMUS
Da T aber optimal ist, kann es keinen Präfixcode geben, der weniger Speicherplatz benötigt. Also gilt:
B(T ) = B(T 0 )
Der Präfixcode T 0 ist also auch optimal.
Entsteht T 00 , indem man in T 0 b und y vertauscht, lässt sich auf analoge Weise zeigen, dass auch T 00
optimal ist. T 0 erfüllt dann die Aussagen des Lemmas.
Lemma 3.4 (Prinzip der optimalen Substruktur) Sei C ein Zeichensatz mit den Zeichen c ∈ C und
den Häufigkeiten f (c), und seien x und y die beiden Zeichen mit den geringsten Häufigkeiten. Sei T
ein Präfixcode für C und f (c), in dem x und y einen gemeinsamen Vater z haben. Sei T 0 der Baum,
der aus T entsteht, indem x und y wegfallen, statt dessen aber z als neues Zeichen mit der Häufigkeit
f (z) = f (x) + f (y) hinzukommt. T 0 ist dann ein Präfixcode für C 0 := C \ {x, y} ∪ {z}.
Unter diesen Voraussetzungen gilt:
T ist optimal für C
⇐⇒
T 0 ist optimal für C 0
Beweis: Da wir nur die Rückrichtung benötigen, wollen wir nur zeigen:
T 0 ist optimal für C 0
=⇒
T ist optimal für C
Für T und T 0 gilt:
B(T ) − B(T 0 ) =
f (x)hT (x) + f (y)hT (y) − f (z) hT 0 (z)
| {z }
=:α
(hT (x) = hT (y) = α + 1, da x und y Söhne von z)
=
f (x)(α + 1) + f (y)(α + 1) − ( f (x) + f (y))α
= ( f (x) + f (y))(α + 1) − ( f (x) + f (y))α
= ( f (x) + f (y))(α + 1 − α)
=⇒
0
B(T ) − B(T ) =
=⇒
f (x) + f (y)
B(T ) = B(T 0 ) + f (x) + f (y)
(3.3)
Sei T 0 optimal, T aber nicht. Betrachte den optimalen Baum T ∗ für C, d.h. B(T ∗ ) < B(T ). Nach
Lemma 3.3, darf angenommen werden, dass x und y an der tiefsten Stelle im Baum T ∗ stehen und
einen gemeinsamen Vater w haben. Betrachte nun den Baum T ∗ 0 , der entsteht, wenn x und y im Baum
T ∗ wegfallen und w als neues Zeichen mit der Häufigkeit f (w) = f (x) + f (y) hinzukommt. Dann
ergibt sich nach Gleichung 3.3 für den Speicherbedarf:
B(T ∗ ) = B(T ∗ 0 ) + f (x) + f (y)
40
KAPITEL 3. HUFFMAN CODES UND DATENKOMPRESSION
Da T 0 optimal für C 0 ist und T ∗ 0 ein Präfixcode für C 0 und dieselben Häufigkeiten ist, folgt B(T 0 ) ≤
B(T ∗ 0 ). Damit gilt:
3.3
B(T ∗ ) < B(T ) = B(T 0 ) + f (x) + f (y)
≤ B(T ∗ 0 ) + f (x) + f (y)
= B(T ∗ )
Das ergibt einen Widerspruch. Also muss auch T optimal sein.
Satz 3.1 (Optimalität) Der Huffman Code T ist optimal unter allen Präfixcodes, das heißt:
B(T ) :=
∑ f (c) · hT (c) ≤ B(T 0 )
c∈C
für alle Präfixcodes T 0 .
Beweis: (Induktion nach der Anzahl der Zeichen n = |C|)
Induktionsanfang: Für n = 2 liefert der Algorithmus eine Codierung, bei denen die Codewörter nur
aus einer 0 bzw. 1 bestehen. Das entspricht dem optimalen Speicherplatz.
Induktionsschritt von n − 1 auf n: Sei n = |C|. Der Huffman Algorithmus ermittelt die Zeichen x
und y mit den kleinsten Häufigkeiten und ersetzt sie durch einen neuen Baum, mit dem Zeichen z
in der Wurzel und den beiden Söhnen x und y, wobei f (z) = f (x) + f (y) ist. Danach wird mit dem
Zeichensatz C 0 = C\{x, y} ∪ {z} weitergearbeitet. Nach Induktionsvoraussetzung liefert der Huffman
Algorithmus einen optimalen Baum T 0 für C 0 . Ersetzt man z wieder entsprechend Lemma 3.4, so
erhält man einen optimalen Präfixcode T für C. Da der Huffman Algorithmus gerade auf diese Weise
den Baum T konstruiert, ist der Huffman Code optimal unter allen Präfixcodes.
Bemerkungen zu Huffman Codes
Hier soll nochmal auf besondere Eigenschaften des Huffman Algorithmus und auf Alternativen dazu
hingewiesen werden.
Der Huffman Algorithmus entspricht einer zeichenweisen Codierung. Es wird jedem Zeichen c ∈ C
ein eigenes Codewort zugewiesen. Wie wir später noch sehen werden, gibt es auch Algorithmen, die
Codewörter für Teilstrings konstruieren. Auch beim Huffman Algorithmus wäre das möglich, indem
man Strings der Länge k als Zeichen betrachtet.
Beim Huffman Code handelt es sich um einen statischen Code. Das bedeutet, dass die ganze Nachricht vorab gelesen und analysiert wird, um die Häufigkeiten der einzelnen Zeichen zu ermitteln und
3.3. WEITERE DATENKOMPRESSIONSVERFAHREN
41
dementsprechend feste Codewörter zu konstruieren. Danach muss die Datei noch einmal gelesen werden, um sie mit den entstandenen Codewörtern zu codieren. Das Gegenteil hierzu wären die dynamischen bzw. adaptiven Codes. Bei diesen Codierungen werden die Codewörter während des Lesens des
Textfiles erstellt und im Laufe des Lesens geändert, wenn es mehr Informationen über die Nachricht
gibt. Die Datei wird während des Lesens codiert, und zwar mit sich ändernden Codewörtern. Diese
Vorgehensweise erspart ein zweimaliges Lesen. Das dynamische Codieren wird auch Komprimierung
on the fly“ genannt.
”
Die Datenkompression mittels des Huffman Algorithmus ist im Gegensatz zur Kompression von
Audio-, Video- und Graphikdateien verlustfrei. Die ursprüngliche Nachricht kann also ohne Verlust
von Informationen wieder rekonstruiert werden.
3.3
3.3.1
Weitere Datenkompressionsverfahren
Der adaptive Huffmancode
Im Gegensatz zum statischen Huffmancode, wird bei diesem Verfahren ein dynamischer Code erstellt. Im Laufe des Lesens werden zu jedem Zeitpunkt, abhängig von der schon gelesenen Nachricht, die wahrscheinlichen Häufigkeiten abgeschätzt und auf dieser Grundlage die Codewörter erstellt. Der Präfixcode wird also immer so verändert, dass er für die aktuellen Abschätzungen optimal
ist. Beim Verschlüsseln wird die Beschaffenheit der Quelldatei gelernt“. Beim Entschlüsseln muss
”
der Huffman Baum kontinuierlich aktualisiert werden, damit die Zeichen wieder mit den richtigen
Codewörtern übersetzt werden können. Wie oben schon beschrieben liegt der Vorteil darin, dass die
Quelldatei nur einmal gelesen werden muss. Abhängig von der Datei, kann diese Vorgehensweise
bessere, aber auch schlechtere Leistung bringen. Der Algorithmus bildet die Basis für den Unixbefehl
compact, der in der Regel eine Kompressionsrate von 30-40% erbringt.
3.3.2
Der run length code“
”
Dieses Verfahren wird zum Komprimieren von Bildern verwendet. Der Computer speichert Bilder, indem er sie in viele Bildpunkte (Pixel) aufteilt und sich die genaue Farbe eines Pixels merkt. Die Pixel
werden dann in einer bestimmten Reihenfolge (z.B. zeilenweise) abgespeichert. Um das Bild nun zu
komprimieren, werden nicht alle Pixel gespeichert, sondern es wird ausgenutzt, dass oft Wiederholung eintritt (z.B. schwarze Fläche). Daher werden nur Pixel mit der Vielfachheit ihres wiederholten
Auftretens gespeichert. Damit wird Speicherplatz eingespart.
3.3.3
Der Lempel-Ziv Code
Dieser Algorithmus wurde 1977 entwickelt. Er arbeitet verlustfrei und adaptiv. Es ist leider nicht
möglich für ihn eine Optimalitätsaussage zu treffen, jedoch ist er empirisch gut. Typisch sind Leistungen, die in der Größenordnung von 50 − 60% liegen. Da er in zip, compress und gzip genutzt wird,
soll er hier genauer erklärt werden.
42
KAPITEL 3. HUFFMAN CODES UND DATENKOMPRESSION
Der Lempel-Ziv Algorithmus erstellt sog. Ketten“, die Strings wachsender Länge entsprechen. In
”
jedem Schritt des Algorithmus wird von der Rest-Nachricht, die noch nicht codiert wurde, der längste
Präfixcode ermittelt, der einer bereits definierten Kette α entspricht. Diese wird dann mit dem danach
kommenden Zeichen c als String αc in eine Tabelle, dem sog. Wörterbuch“, eingetragen und sie wird
”
mit ic codiert, wobei i dem Codewort von α entspricht. Die Kette ic erhält dann ein eigenes Codewort.
Die Codewörter haben dieselbe vordefinierte Länge, die dann die Größe der Tabelle und die Länge
der längsten Kette bestimmt.
Um den Lempel-Ziv Algorithmus besser zu verstehen, folgen hier zur Veranschaulichung zwei Beispiele.
Beispiel 3.9 Gegeben sei die Nachricht:
aa bbb cccc ddddd eeeeee fffffffgggggggg
und der dazugehörige Blockcode:
Zeichen
a
b
c
d
e
f
g
Codewort
000
001
010
011
100
101
110
111
Daraus ergibt sich dann mit dem beschriebenen Verfahren der Lempel-Ziv Code:
String
Kette
Codewortnummer
leer
0
0
a
0a
1
a
1
2
b
0b
3
String
Kette
Codewortnummer
eee
13e
14
f
5f
15
f
0f
16
ff
16f
17
bb
3b
4
fff
17f
18
c
0c
6
0
5
g
0g
19
cc
6c
7
gg
19g
20
c
6
8
d
0d
9
dd
9d
10
dd
10
11
e
0e
12
ee
12e
13
ggg
20g
21
Die komprimierte Nachricht sieht dann so aus:
(0a)(1 )(0b)(3b)(0 )(0c)(6c)(6 )(0d)(9d)(10 )(0e)(12e)(13e)(5f)(0f)(16f)(17f)(0g)(19g)(20g)(20)
Um 21 Codewortnummern abzuspeichern, benötigt man dlog 21e = 5 Bit. Für die Zeichen an sich
werden zum Abspeichern die ursprünglichen 3 Bit benötigt. Dann folgt für den Speicherplatz:
21 Zeichen
+ 1 Zeichen
a
a
5 Bit
5 Bit
+ 3 Bit
=⇒
=⇒
168 Bit
5 Bit
173 Bit
43
3.4. ABSCHLIESSENDE BEMERKUNGEN
Beispiel 3.10 Das nun folgende Beispiel benutzt den uns bereits bekannten ASCII Code, der eine
Länge von 8 Bit hat:
fischers fritze fischt frische fische frische fische fischt fischers fritze
Der Lempel-Ziv Code sieht dann wie folgt aus
String
Kette
Codewortnummer
leer
0
0
String
Kette
Codewortnummer
sc
3c
14
String
Kette
Codewortnummer
isch
23h
27
f
0f
1
ht
5t
15
t
0t
28
i
0i
2
s
0s
3
c
0c
4
0
16
fri
9i
17
sch
14h
18
fi
26i
29
h
0h
5
e f
12f
19
scher
25r
30
s f
8f
31
e
0e
6
is
2s
20
ri
7i
32
r
0r
7
s
3
8
ch
4h
21
fr
1r
9
it
2t
10
z
0z
11
e fr
19r
22
isc
20c
23
e fi
19i
24
e
6
12
fi
1i
13
sche
18e
25
f
16f
26
tz
28z
33
und codiert die Nachricht folgendermaßen:
(0f)(0i)(0s)(0c)(0h)(0e)(0r)(3 )(1r)(2t)(0z)(6 )(1i)(3c)(5t)(0 )(9i)(14h)(12f)
(2s)(4h)(19r)(20c)(19i)(18e)(16f)(23h)(0t)(26i)(25r)(8f)(7i)(28z)(6)
Zum Abspeichern von 33 Codewörtern werden dlog 33e = 6 Bit und zum Abspeichern eines Zeichens
mit ASCII Code werden 8 Bit benötigt. Das heißt für den Speicherplatzbedarf:
33 · (6Bit + 8Bit) + 6Bit = 468Bit
3.4
Abschließende Bemerkungen
Abschließend soll darauf hingewiesen werden, dass es eine von den Häufigkeiten abhängige untere
Schranke für Datenkompression, die Entropie, gibt. Um diese zu erklären benötigen wir die folgenden
beiden Begriffe. Zum Einen gibt es den Begriff der normierten Häufigikeit P(c), die die Häufigkeit eines Zeichens c in Abhängigkeit von der Summe der Häufigkeiten aller Zeichen der Quelldatei angibt:
P(c) =
f (c)
∑ f (u)
(3.4)
u∈C
Wird der Speicherplatz B(T ), der die Gesamtheit der Wortlängen angibt, normiert, ergibt sich die
mittlere Wortlänge:
A(T ) = ∑ P(c) · h(c)
(3.5)
c∈C
Nun können wir die Entropie H(C) einer Quelldatei C definieren:
H(C) =
∑ [P(c) · (− log P(c))]
c∈C
(3.6)
44
KAPITEL 3. HUFFMAN CODES UND DATENKOMPRESSION
Der Satz von Shannon sagt nun, dass die mittlere Wortlänge jeder verlustfreien Komprimierung über
der Entropie liegt:
A(T ) ≥ H(C)
für jede Codierung T von C
(3.7)
Um nun quantitativ sagen zu können wie gut ein Kompressionsverfahren ist, gibt es den Begriff der
Redundanz . Die Redundanz kann als Differenz zwischen der mittleren Wortlänge der codierten Datei
und der Entropie der Quelldatei definiert werden:
R(T ) = A(T ) − H(C)
für eine Codierung T von C
(3.8)
Beispiel 3.11 In der gegebenen Quelldatei treten die folgenden Zeichen mit den angegebenen normierten Häufigkeiten auf:
c∈C
P(c)
a
0, 45
b
0, 13
c
0, 12
d
0, 16
e
0, 09
f
0, 05
Dann folgt mit Gleichung 3.6, dass die Entropie H(c) = 2, 2199 beträgt. Komprimiert man die Datei
mit dem Huffman Code, ergibt sich mit Gleichung 3.5 eine mittlere Wortlänge von A(T ) = 2, 24. Die
Redundanz beträgt dann nach Gleichung 3.8 R(T ) = 0.0201. Die Redundanz ist also beim Huffman
Code sehr gering.
Der Huffman Code ist asymptotisch optimal unter allen Codierungsverfahren. Betrachtet man nämlich
den verallgemeinerten Huffman Block Code, der Teilstrings fester Länge k als einzelne Zeichen behandelt, gilt folgender Satz:
Satz 3.2 Zu jedem ε > 0 gibt es ein k, so dass für den Huffman Block Code Tk mit den Teilstrings der
Länge k gilt:
A(Tk ) ≤ H(C) + ε
3.5
Literaturhinweise
Der Huffman Algorithmus wird ausführlich [CLRS01] behandelt. Eine ausgezeichnete Übersicht und ach tiefergehende Informationen über Kompressionsverfahren findet man unter http://www.data-compression.com/
Kapitel 4
Suchbäume
Die Hintergrundanwendung für Suchbäume ist das Verwalten von dynamischen Daten. Wir möchten
die Operationen
• Einfügen
• Löschen
• Suchen
schnell ausführen. Suchbäume haben viele Anwendungen, zum Beispiel in Datenbanken oder Dateisystemen auf Festplatten. Wir kennen dafür bisher nur Listen; der Hauptnachteil an Listen ist der rein
sequentielle Zugriff, der zu einem Worst Case Aufwand von O(n) für das Suchen führt. Wir beschleunigen die Suche durch die Verwendung von Suchbäumen und erhalten dabei O(log n) statt O(n) als
Aufwand.
Was ist also ein Suchbaum? Ein Suchbaum hat in jedem Knoten einen Datensatz und die Suchschlüssel sind aufsteigend sortiert bezüglich der Inorder-Traversierung.
7
8
5
2
6
10
9
45
46
KAPITEL 4. SUCHBÄUME
Äquivalent dazu ist folgende Definition: Für jeden Knoten v gilt:
1. Alle Knoten im linken Teilbaum zu v haben einen kleineren Schlüssel als v
2. Alle Knoten im rechten Teilbaum zu v haben einen größeren Schlüssel als v
4.1
Basisoperationen in Suchbäumen
4.1.1
Suchen nach Schlüssel k
1. Beginne in der Wurzel w: falls der Schlüssel in w den Wert k hat, gebe w zurück
2. sonst falls der Schlüssel in w > k: suche im linken Teilbaum weiter
3. sonst suche im rechten Teilbaum weiter
Der Aufwand, gemessen in der Anzahl der Vergleiche, entspricht der Höhe des zu suchenden Knoten
plus eins. Der Worst-Case-Aufwand ist daher h(T ) + 1 = O(h(T )).
4.1.2
Einfügen eines Knoten
Das natürlichen“ Einfügen funktioniert folgendermaßen:
”
1. Nach dem neuen Schlüssel suchen, bis man in einem Blatt angelangt ist
2. Dann gemäß Schlüsselwert des Blattes den neuen Schlüssel links oder rechts an das Blatt
anhängen
Als Beispiel fügen wir die Sequenz 5,7,8,3,2,12,0,6,9 in einen Suchbaum ein und stellen einige Teilschritte in Abbildung 4.1 dar.
Der Aufwand zum Einfügen in den Suchbaum T entspricht der Höhe des Blattes, an das angehängt
wird, plus eins. Der Worst Case hat also den Aufwand h(T ) + 1 = O(h(T )).
Ein Problem beim natürlichen Einfügen besteht darin, dass der Baum zu einer Liste entarten kann und
Suchen und Einfügen dann einen Worst-Case von O(n) haben, siehe Abblidung 4.2.
4.1.3
Löschen eines Knoten
1. Suche den zu löschenden Knoten v.
2. Falls v Blatt ist, so lösche v.
3. Falls v nur einen Sohn w hat, so hänge w an den Vater von v und lösche v.
47
4.1. BASISOPERATIONEN IN SUCHBÄUMEN
5
5
5
7
7
3
6
2
0
8
12
9
Abbildung 4.1: Natürliches Einfügen der Sequenz 5,7,8,3,2,12,0,6,9 in einen Suchbaum.
1
2
3
4
Abbildung 4.2: Durch natürliches Einfügen zu einer Liste entarteter Suchbaum.
4. Falls v zwei Söhne hat:
4.1 Suche den Knoten w im linken Teilbaum von v, der am weitesten rechts steht (dieser ist
Vorgänger von v bzgl. Inorder).
4.2 Tausche die Daten von v und w.
4.3 Lösche w (w ist Blatt oder hat nur einen Sohn und kann daher mit Schritt 1 oder 2 gelöscht
werden).
Statt den am weitesten rechts stehenden Knoten im linken Teilbaum zu suchen, kann man natürlich
auch den am weitesten links stehenden Knoten im rechten Teilbaum suchen und analog verfahren.
Beispiel 4.1 Es soll die 7 aus dem Suchbaum links in Abbildung 4.3 gelöscht werden. Zuerst wird
der am weitesten rechts stehende Knoten im linken Teilbaum der 7 gesucht und liefert die 6 (Mitte).
Daraufhin werden die 6 und die 7 vertauscht und die 7 gelöscht (rechts).
Wie beim Suchen und Einfügen beträgt der Aufwand im Worst Case h(T ) + 1 = O(h(T )).
Satz 4.1 Der Aufwand für das Suchen, das natürliche Löschen und das natürliche Einfügen erfordert
im Worst Case O(h(T )) Vergleiche.
48
KAPITEL 4. SUCHBÄUME
2
1
12
8
6
10
5
10
5
4
6
7
7
4
2
9
6
12
8
9
4
8
7
2
1 3
3
10
5
12
9
1 3
Abbildung 4.3: Löschen des Knoten 7.
Im Extremfall kann h(T ) gleich n − 1 sein, der Aufwand dann also O(n). Deswegen werden wir die
Operationen so verbessern, dass h(T ) klein“ bleibt. Die dafür entscheidenden Operationen sind die
”
Rotationen, mit denen die Gestalt eines Baumes verändert werden kann um die Höhe klein zu halten.
Wir unterscheiden zwischen Links- und Rechtsrotation, die Linksrotation RotLinks(v, w) um die Knoten v und w, und die dazu symmetrische Rechtsrotation RotRechts(v, w). Beide sind in ist in Abbildung 4.4 dargestellt. Man beachten, dass das Resultat in beiden Fällen wieder ein Suchbaum ist, die
Teilbäume T1 , T2 , T3 also korrekt umgehängt“ werden.
”
v
w
RotLinks(v,w)
w
v
T1
T3
T2
T3
T1
T2
v
w
RotRechts(v,w)
w
T3
T1
T2
v
T1
T2
Abbildung 4.4: Linksrotation RotLinks(v,w)
T3
4.1. BASISOPERATIONEN IN SUCHBÄUMEN
49
Linksrotation und Rechtsrotation sind in O(1) durchführbar, da nur konstant viele Referenzen geändert
werden müssen. Eine Java-Methode für die Linksrotation könnte in etwa wie folgt aussehen:
TreeNode rotateLeft (TreeNode v) {
TreeNode aux = v.rson;
v.rson = aux.lson;
aux.lson = v;
v = aux;
return v;
}
Diese Anweisungen werden genauer in Abbildung 4.5 erläutert. Die erste Zeile
TreeNode aux = v.rson;
setzt die Hilfsreferenz aux auf den rechten Sohn des zu rotierenden Knotens v. Die Zeile
v.rson = aux.lson;
setzt den rechten Sohn für den zu rotierenden Knoten v neu. Danach setzt die Zeile
aux.lson = v;
den linken Sohn des von aux referenzierten Knotens auf den zu rotierenden Knoten v. Dann wird
durch
v = aux;
v neu gesetzt und schließlich durch
return v;
v zurückgegeben.
Der folgende Satz sagt, dass Rotationen ausreichen, um Suchbäume beliebig zu ändern und daher
insbesondere ausreichen, um h(T ) klein zu halten.
Satz 4.2 Seien T1 , T2 Suchbäume zur selben Schlüsselmenge. Dann gibt es eine endliche Folge von
Rechts- und Linksrotationen, die T1 in T2 transformiert.
Beweis: Wir beweisen den Satz durch vollständige Induktion nach der Anzahl der Schlüssel. Abbildung 4.6 illustriert das Prinzip. Für n = 1 ist nichts zu zeigen, denn zwei Bäume mit nur einem
Schlüssel und der gleichen Schlüsselmenge sind gleich.
50
KAPITEL 4. SUCHBÄUME
v
aux
v
aux
w
w
T1
T1
T3
T2
T2
T3
aux
aux
w
v
w
v
T3
T1
T2
T3
T1
T2
Abbildung 4.5: Umhängen der Referenzen bei der Linksrotation RotLinks(v, w).
Für n = 2 gibt es nur zwei verschiedene Suchbäume zur gleichen Schlüsselmenge, die durch genau
eine Links- beziehungsweise Rechtsrotation ineinander überführt werden können.
n → n + 1 : Sei T1 der Ausgangsbaum, w(T1 ) seine Wurzel und T1` der linke und T1r der rechte Teilbaum von T1 . Sei T2 der Baum, in den T1 durch Rotationen umgewandelt werden soll.
Das Wurzel w(T2 ) von T2 ist, da die beiden Bäume die gleiche Schlüsselmenge haben, auch in T1 enthalten, etwa o.B.d.A. im rechten Teilbaum T1r von T1 . Auf die Teilbäume von T1 trifft die Induktionsvoraussetzung zu, und wir können daher T1` mit Rotationen so transformieren, dass der transformierte
Baum T̃1` eine linke“ Liste wird (siehe Baum links unten in Abbildung 4.6). Entsprechend können
”
wir T1r mit Rotationen so transformieren, dass der transformierte Baum T̃1r eine rechte“ Liste wird.
”
O.b.d.A. war w(T2 ) im rechten Teilbaum von T1 enthalten. Wegen der speziellen Listengestalt der
Bäume T̃1` und T̃2r können wir durch eine Folge von Linksrotationen um die Wurzel des ganzen Baumes dafür sorgen, dass w(T2 ) die Wurzel wird (siehe Baum rechts unten in Abbildung 4.6).
Wegen der Suchbaumeigenschaft haben die Teilbäume T 0` und T 0r des so entstehenden BaumesT 0 mit
Wurzel w(T2 ) dieselben Schlüssel wie T2` und T2r . Nach Induktionsvoraussetzung können wir daher
mit Rotationen T 0` in T2` und T 0r in T2r überführen.
Die Zusammensetzung dieser Folgen von Rotationen (zunächst Teilbäume von T1 zu Listen, dann
w(T2 ) an die Wurzel, dann Teilbäume von T 0 zu T2` und T2r ) liefert dann die gewünschte Folge von
Rotationen, die T1 in T2 überführt.
51
4.2. LITERATURHINWEISE
5
6
3
1
7
4
4
1
6
7
5
3
5
4
3
6
6
5
7
4
7
1
3
1
Abbildung 4.6: Illustration des Beweises von Satz 4.2.
Rotationen reichen also aus, um Suchbäume zu balancieren. Wir definieren daher:
Eine Klasse T von Suchbäumen heißt balanciert zur Höhe f (n), wenn gilt:
1. Jede Schlüsselmenge mit n Knoten (n = 1, 2, . . . ,) kann durch einen Suchbaum T ∈ T dargestellt
werden und es gilt h(T ) ≤ f (n).
2. Basisoperationen können für T ∈ T in O(h(T )) durchgeführt werden.
3. Die Klasse T ist abgeschlossen gegenüber Basisoperationen, d.h. Einfügen und Löschen in
T ∈ T führen wieder zu einem Baum in T.
Gesucht ist eine Klasse von Suchbäumen mit f (n) ∈ O(log n). Es gibt verschiedene solche Klassen,
die historische älteste ist die der sogenannten AVL-Bäume. Für sie ist f (n) ≤ 1.4405 log(n + 2) −
0.3277. Wir werden jedoch nur zeigen, dass f (n) ≤ 2 · log n gilt.
4.2
Literaturhinweise
Suchbäume werden in jedem Buch über Datenstrukturen behandelt, vgl. etwa [CLRS01, Knu98, OW02, SS02].
52
KAPITEL 4. SUCHBÄUME
Kapitel 5
AVL-Bäume
Die historisch älteste Klasse balancierter Suchbäume zur Höhe f (n) ∈ O(log n), die Klasse der AVLBäume, wurde 1962 von Adelson-Velski und Landis aus der UdSSR definiert. Die grundsätzliche Idee
hierbei ist, das Entarten der Suchbäume zu einer Liste durch eine Forderung an die Höhendifferenz
der beiden Teilbäume eines jeden Knotens zu verhindern. Mit dieser Forderung wird erreicht, dass die
Höhe von f (n) = 1, 44 . . . log n nicht überschritten wird.
5.1
Grundsätzliche Eigenschaften
Um AVL-Bäume definieren zu können, benötigen wir zunächst die Definition der Balance:
Sei T ein binärer Suchbaum und sei v ∈ V ein Knoten. Seien T` (v) und Tr (v) die (evtl. leeren Teilbäume)
von v. Dann heißt
β (v) := h(Tr (v)) − h(T` (v))
mit h(0)
/ = −1
(5.1)
die Balance des Knoten v.
v
T` (v)
h(T` (v))
h(Tr (v))
Tr (v)
β (v)
Abbildung 5.1: Balance als Höhendifferenz
Wie in Abb. 5.1 zu sehen, beschreibt sie die Höhendifferenz zwischen dem rechten und linken Teilbaum des Knoten v. Damit können wir AVL-Bäume definieren:
53
54
KAPITEL 5. AVL-BÄUME
Ein binärer Suchbaum T heißt AVL-Baum genau dann, wenn für alle v ∈ V gilt:
|β (v)| ≤ 1.
(5.2)
Beispiel 5.1 Abbildung 5.2 zeigt die beiden Bäume T und T 0 . Die Zahlen an den Knoten geben ihre
Balance an. Bei T handelt es sich um einen AVL-Baum. T 0 erfüllt die AVL-Eigenschaft nicht, da die
Balance der Wurzel −2 beträgt.
T
1
T0
−1
1
1
0
0
−2
−1
0
0
−1
0
0
Abbildung 5.2: Der AVL-Baum T und der Suchbaum T 0
Aus der Definition von AVL-Bäumen folgt, dass auch jeder Teilbaum eines AVL-Baums wieder ein
AVL-Baum ist. Wir zeigen nun, dass die Balance-Bedingung dafür sorgt, dass AVL-Bäume nicht allzu
hoch werden.
Satz 5.1 Sei T ein AVL-Baum mit n Knoten (n ≥ 2). Dann gilt für seine Höhe h(T ):
h(T ) ≤ 2 log n
(5.3)
Beweis: Um diese Abschätzung zu beweisen, betrachten wir extremale AVL-Bäume, die zu vorgegebener Höhe h eine minimale Knotenzahl aufweisen. Sei deshalb
n(h) := min{n | T ist AVL-Baum mit n Knoten und h(T ) = h}
(5.4)
Abbildung 5.3 zeigt, wie solche extremalen AVL-Bäume aufgebaut sind. Bezüglich der Struktur eines
extremalen AVL-Baumes T , lassen sich daher folgende Eigenschaften vermuten:
1. Die Höhendifferenz der Teilbäume eines extremalen AVL-Baumns ist 1 oder −1 (außer der
Baum hat nur einen Knoten).
2. Die Teilbäume T` und Tr eines extremalen AVL-Baums der Höhe h ≥ 1 sind selber wieder
extremale AVL-Bäume zu den Höhen h − 1 und h − 2 bzw. h − 2 und h − 1.
Wir wollen nun diese Vermutungen beweisen:
55
5.1. GRUNDSÄTZLICHE EIGENSCHAFTEN
h
0
1
2
3
...
...
...
...
Abbildung 5.3: Extremale AVL-Bäume.
zu 1.: Angenommen, T sei ein extremaler AVL-Baum mit einer Höhendifferenz 0. Nimmt man nun,
wie in Abb. 5.4, im linken Teilbaum alle Blätter weg, ergibt sich wieder ein AVL-Baum, da
das Löschen der Blätter die Balancebedingung |β (u)| ≤ 1 für alle u ∈ V nicht verletzt. Dieser
AVL-Baum hat dieselbe Höhe, aber weniger Knoten als T . Damit ist T nicht mehr extremal.
Das ergibt einen Widerspruch.
zu 2.: Da nach Definition von AVL-Bäumen klar ist, dass alle Teilbäume von AVL-Bäumen wieder
AVL-Bäume sind, bleibt zu zeigen, dass seine Teilbäume extremal zu den Höhen h − 1 und
h − 2 sind. Angenommen, T sei extremal, aber ein Teilbaum, o.B.d.A. T` , nicht. Dann gibt es
einen AVL-Baum T` 0 mit weniger Knoten als T` , aber derselben Höhe. Ersetze nun T` durch T` 0
in Baum T . Damit ergibt sich ein neuer Baum T 0 mit weniger Knoten, aber derselben Höhe.
Da T` 0 und Tr beides AVL-Bäume sind und die Höhe von T` 0 gleich der Höhe von T` ist, sich
also die Balance in der Wurzel nicht ändert, ist auch T 0 ein AVL-Baum. Da dieser aber weniger
Knoten hat als T , kann T nicht extremal sein. Das ergibt einen Widerspruch. Also sind die
Teilbäume von T extremal. Dass sie die Höhen h − 1 und h − 2 haben, ergibt sich aus der ersten
Vermutung.
T
−1
0
−1
1
Abbildung 5.4: Löschen der Blätter im linken Teilbaum
Die bewiesene Vermutung führt zu der zentralen Rekursionsformel für die Knotenzahl extremaler
AVL-Bäume in Abhängigkeit von der Höhe:
n(0) = 1, n(1) = 2
n(h) = n(h − 2) + n(h − 1) + 1
für h ≥ 2
(5.5)
56
KAPITEL 5. AVL-BÄUME
Dabei wird zu der Anzahl der Knoten der beiden Teilbäume noch ein Knoten für die Wurzel addiert.
Aus der Rekursionsformel 5.5 folgt dann dieses Lemma:
Lemma 5.1 Für die minimale Knotenzahl n(h) eines AVL-Baumes der Höhe h, (h ≥ 0) gilt:
h
n(h) ≥ 2 2
(5.6)
Beweis: (vollständige Induktion nach h)
Induktionsanfang:
h
h = 0 =⇒ n(h) = 1 = 20 = 2 2
√
h
h = 1 =⇒ n(h) = 2 ≥ 2 = 2 2
h
h = 2 =⇒ n(h) = 4 ≥ 2 = 2 2
Induktionsschritt von h − 1 auf h:
Sei also das Lemma bewiesen für 1, 2, . . . , h − 1. Dann folgt:
5.5
n(h) = n(h − 1) +n(h − 2) + 1
| {z }
5.5
= n(h − 2) + n(h − 3) + 1 + n(h − 2) + 1
> 2n(h − 2)
IV
≥ 2·2
= 2
h−2
2
h−2
2 +1
h
= 22
h
h
h
Zwar ist hier n(h) echt größer als 2 2 , aber für h = 0 ist n(h) gleich 2 2 . Deswegen ist n(h) ≥ 2 2 .
4
Aus dem Lemma folgt dann durch Logarithmieren:
h
2
⇒ h ≤ 2 log n(h)
log n(h) ≥
⇒ h ≤ 2 log n(T )
für jeden AVL-Baum T mit n(T ) Knoten und der Höhe h(T ).
Um zu beweisen, dass h ≤ 1, 44 . . . log n ist, benutzt man diesen Ansatz: Die Rekursionsformel 5.5
ähnelt der der Fibonacci-Folge:
f0 = 0, f1 = 1
fi = fi−1 + fi−2
57
5.2. ROTATIONEN
i
0
1
2
3
4
5
6
7
..
.
n(i) fi
1
0
L
2 L 1
4 LLL 1
L
7 LLL 2
L
L
12 L 3
L
20L LL 5
L
33L LL 8
L
54 LL 13
.. L ..
.
.
Abbildung 5.5: Zusammenhang zwischen Rekursionsformel 5.5 und der Fibonacci-Folge.
Daher nennt man extremale AVL-Bäume auch Fibonacci-Bäume. Den genauen Zusammenhang zwischen diesen beiden Folgen zeigt die Tabelle in Abbildung 5.5.
Es ist leicht zu sehen, dass fi = n(i − 3) + 1. Damit kann man n(h) analog zu der Fibonacci-Folge in
geschlossener Form abschätzen. Die Formel für die Fibonacci-Zahlen lautet:
√
√
n
Φn − Φ
1+ 5
1− 5
fi = √
, wobei Φ =
und Φ =
2
2
5
Daraus folgt dann (mit etwas Arbeit) die obige genauere Abschätzung. Wir verzichten hier auf den
genauen Beweis.
5.2
Rotationen
Um die schon bei Suchbäumen angesprochenen Basisoperationen durchführen zu können, ohne den
AVL-Baum außer Balance zu bringen, benötigt man Rotationen. Im letzten Kapitel über Suchbäume
wurden bereits die einfachen Rotationen RotLinks(v, w) und RotRechts(v, w) erklärt. Neben diesen
benutzt man bei AVL-Bäumen noch die Doppelrotationen. Zur Erklärung beschränken wir uns auf
eine anschauliche Darstellung in den Abbildungen 5.6 und 5.7:
Die Doppelrotationen haben diese Eigenschaften:
1. Der nach der Doppelrotation resultierende binäre Baum T 0 ist wiederum ein Suchbaum.
2. Die Doppelrotationen lassen sich durch die einfachen Rotationen RotLinks und RotRechts ausdrücken:
DRotLinks (v, v 0 , v 00 ) = RotLinks (v, v 00 ) ◦ RotRechts (v 0 , v 00 ) und
DRotRechts (v, v 0 , v 00 ) = RotRechts (v, v 00 ) ◦ RotLinks (v 0 , v 00 ) .
Rotationen und Doppelrotationen reichen aus, um AVL-Bäume nach dem Löschen bzw. Einfügen
eines Knotens zu rebalancieren. Basis hierfür ist das folgende Lemma:
58
KAPITEL 5. AVL-BÄUME
v 00
v
2
v0
v 00
v0
v
=⇒
1
T1
T4
T2
T1
T2
T3
T4
T3
Abbildung 5.6: Doppellinksrotation DRotLinks(v, v 0 , v 00 ).
v 00
v
2
v0
v0
=⇒
1
v 00
T4
T1
T1
T2
v
T2
T3
T4
T3
Abbildung 5.7: Doppelrechtsrotation DRotRechts(v, v 0 , v 00 ).
Lemma 5.2 (Rotationslemma für AVL-Bäume) Sei T ein Baum mit Wurzel v. Der linke und der
rechte Teilbaum T` , Tr von T seien AVL-Bäume. Die Wurzel v sei geringfügig außer Balance, d.h.
|β (v)| = 2. Dann folgt:
a) T kann durch eine Rotation bzw. Doppelrotation in einen AVL-Baum T 0 überführt werden mit
h(T 0 ) ≤ h(T ).
b) Die Art der Rotation (einfach links, . . . , doppelt rechts) kann mit O(1) Aufwand ermittelt werden.
c) Die Rotation kann in O(1) Aufwand durchgeführt werden.
d) Alle veränderten Balancen in T 0 können mit O(1) Aufwand aus denen in T berechnet werden.
59
5.2. ROTATIONEN
Beweis: Wir unterscheiden 4 Fälle:
1.) Der Weg von v in die größte Tiefe geht links – links (LL)
T
T0
v
v0
=⇒
v0
v
T3
2
T1
≤1
T1
T2
T3
T2
Abbildung 5.8: Rechtsrotation bei dem Fall LL.
Aus Abbildung 5.8 folgt: β (v) = −2 und β (v 0 ) ∈ {0, −1}. Durch Anwenden der Rechtsrotation
RotRechts(v, v 0 ) auf T entsteht T 0 :
Für die Balancen β 0 in T 0 gilt offenbar:
0
β (v) =
0
−1 falls β (v 0 ) = 0
0 falls β (v 0 ) = −1
(5.7)
1 falls β (v 0 ) = 0
0 falls β (v 0 ) = −1
(5.8)
0
β (v ) =
β 0 (u) = β (u) , für alle u 6= v, v 0
(5.9)
Ferner ist
0
h(T ) =
h(T )
falls β (v 0 ) = 0
h(T ) − 1 falls β (v 0 ) = −1
(5.10)
Da für alle u ∈ V gilt, dass |β 0 (u)| ≤ 1 ist und h(T 0 ) ≤ h(T ) ist, folgt sofort Aussage a). Die beiden
Balancen für v und w bestimmen nun die Art der Rotation. Die Balancen können leicht in O(1)
ermittelt werden. Also folgt Aussage b). Dass die Rotation mit einem Aufwand von O(1) durchführbar
ist und somit Aussage c) stimmt, wurde bereits im letzten Kapitel bewiesen. Aussage d) stimmt, weil
sich die veränderten Balancen mit den obigen Gleichungen in O(1) bestimmen lassen.
60
KAPITEL 5. AVL-BÄUME
T
T
v
V0
v 00
V0
v
=⇒
v 00
T4
2
1
T1
T2
T3
T4
T1
T2
T3
Abbildung 5.9: Doppelrechtsrotation bei dem Fall LR und nicht LL.
2.) Weg von v in die größte Tiefe geht links – rechts und nicht links – links (LR)
Für diesen Fall gilt: β (v) = −2, β (v 0 ) = 1 und β (v 00 ) ∈ {−1, 0, 1}. Wendet man die Doppelrechtsrotation DRotRechts(v, v 0 , v 00 ) an, entsteht T 0 . Für ihn gilt:

 0 falls β (v 00 ) = 0
0 00
0 falls β (v 00 ) = 1
β (v ) =

0 falls β (v 00 ) = −1
(5.11)

 0 falls β (v 00 ) = 0
−1 falls β (v 00 ) = 1
β 0 (v 0 ) =

0 falls β (v 00 ) = −1
(5.12)

 0 falls β (v 00 ) = 0
0
0 falls β (v 00 ) = 1
β (v) =

1 falls β (v 00 ) = −1
(5.13)
β 0 (u) = β (u) , für alle u 6= v, v 0 , v 00
(5.14)
h(T 0 ) = h(T ) − 1
(5.15)
Ferner ist
Daraus ergeben sich analog zum 1. Fall die Aussagen a), b), c) und d).
3.) Weg von v in die größte Tiefe geht rechts - rechts (RR)
Dieser Fall ist symmetrisch zum 1. Fall mit einer Linksrotation und wird analog bewiesen.
61
5.3. DIE BASISOPERATIONEN IN AVL-BÄUMEN
4.) Weg von v in die größte Tiefe geht rechts - links und nicht rechts - rechts (RL)
Dieser Fall ist symmetrisch zum 2. Fall mit einer Doppellinksrotation und wird analog bewiesen.
Hierbei ist unbedingt zu beachten: Geht der Weg in die größte Tiefe sowohl LL als auch LR (1. Fall),
so erzeugt eine Doppelrotation DRotRechts im Allgemeinen keinen AVL-Baum, wie das Beispiel in
Abbildung 5.10 zeigt. Dies gilt natürlich ebenso für den symmetrischen Fall.
T
T0
6
4
DRotRechts(6, 3, 4)
3
2
7
=⇒
3
2
4
5
1
1
6
5
7
β 0 (3) = −2
⇒ kein AVL-Baum
=⇒
RotRechts(6, 3)
T 00
3
2
1
6
4
⇒ AVL-Baum
7
5
Abbildung 5.10: Die unterschiedlichen Rotationen bei LL und LR.
5.3
Die Basisoperationen in AVL-Bäumen
Zur effizienten Durchführung der Basisoperationen Suchen, Einfügen und Löschen muss jeder Knoten
u seine Balance β (u) kennen“, d.h., wir setzen voraus, dass sie in jedem Knoten u ∈ V abgespeichert
”
ist. Die Basisoperationen werde zunächst wie in allgemeinen Suchbäumen durchgeführt, danach wird
ggf. durch Rotationen die AVL-Eigenschaft wieder hergestellt und werden die neuen Balancen für die
62
KAPITEL 5. AVL-BÄUME
Knoten mit veränderter Balance ermittelt und abgespeichert.
5.3.1
Suchen eines Knotens v
Die Suchen eines Knotens in einem AVL-Baum entspricht der normalen Suche in einem Suchbaum,
da die Struktur des Baumes unverändert bleibt. Die AVL-Eigenschaft bleibt also erhalten. Da der
Aufwand für diese Basisoperation im Suchbaum in O(h(T )) liegt, folgt für den AVL-Baum, dass das
Suchen in O(log n(T )) arbeitet.
5.3.2
Einfügen eines neuen Knotens v
Zunächst muss der Platz gesucht werden, an dem der neue Knoten entsprechend der Suchbaumeigenschaft eingefügt werden soll. Den dabei gegangenen Weg von der Wurzel bis zu der Einfügestelle
nennen wir die Spur. Dann wird der Knoten an der entsprechenden Stelle eingefügt und es ergibt sich
ein neuer Baum T 0 . Der Aufwand hierfür liegt in O(h(T )) = O(log n(T )).
Beim Übergang von T zum neuen Baum T 0 kann allerdings die AVL-Eigenschaft verloren gehen, aber
nur entlang der Spur zur Wurzel. Alle anderen Teilbäume bleiben gleich und es gilt β 0 (u) = β (u) für
alle Knoten u, die nicht auf der Spur liegen, wobei β 0 und β die Balancen in den Bäumen T 0 und
T sind. Also sind nur die Balancen entlang der Spur zu prüfen und gegebenenfalls in Ordnung zu
bringen. Wie dies geschieht, zeigt der folgende Algorithmus.
Algorithmus 5.1 Rebalancieren beim Einfügen:
(i) Setze β 0 (v) = 0, da v ein Blatt ist.
(ii) Akualisiere die Balance β 0 (u) des Vaters u von v.
(iii) Gilt |β 0 (u)| ≤ 1 und u 6= root, so setze v := u und gehe zu (ii).
(iii) Ist |β 0 (u)| = 2, so folgt:
// Beide Teilbäume sind noch AVL-Bäume
// Die Situation aus dem Rotationslemma (Lemma 5.2) liegt vor.
⇒ Führe eine entsprechende Rotation bzw. Doppelrotation durch, die die Balance wieder herstellt und aktualisiere die Balancen.
// War w der Vater von u vor der Rotation, so setze v := w und gehe zu (ii).
Dabei gilt:
Satz 5.2 Wird beim Einfügen von v eine Rotation bzw. Doppelrotation in v1 ausgeführt, so erfüllen
danach alle Vorgänger u von v1 auf der Spur die Balancebedingung |β 0 (u)| ≤ 1.
63
5.3. DIE BASISOPERATIONEN IN AVL-BÄUMEN
T
T0
v2
v2
v1
w
=⇒
v1
w
2
2
v
1
v
Abbildung 5.11: Nach dem Einfügen genügt es einmal zu rotieren (doppelt oder einfach), um die
AVL-Eigenschaft wieder herzustellen.
Beweis: Angenommen, das Einfügen von v erzeugt einen Baum T 0 mit den folgenden Balancen in v1
und v2 , die auf der Spur von T 0 liegen: β 0 (v1 ) = β 0 (v2 ) = −2, wobei v1 der erste Knoten (von unten)
ist, der außer Balance geraten ist.
In T musste also |β (v1 )| = 1 und |β (w)| = 0 gelten, sonst wäre v1 nicht der erste Knoten mit |β 0 (v1 )| =
2. Abhängig davon, ob v am linken oder am rechten Teilbaum eingefügt wurde, ist entweder eine Doppelrechtsrotation oder eine einfache Rechtsrotation notwendig, um v1 in Balance zu bringen. Durch
Anwendung der Rechtsrotation RotRechts(v1 , w) verringert sich die Höhe des Teilbaums, der nun die
Wurzel w hat, um 1. Vergleiche dazu den 1. Fall des Rotationslemmas (Lemma 5.2). Doppelrotationen machen nach dem 2. Fall des Rotationslemmas (Lemma 5.2) den entstehenden Teilbaum immer
um 1 weniger hoch. Also wird der Teilbaum mit der Wurzel v1 , unabhängig davon, ob eine einfache
Rechtsrotation oder eine Doppelrechtsrotation durchgeführt wird, danach durch einen um 1 weniger
hohen Teilbaum ersetzt. Nun ist die Balancebedingung in v2 nicht mehr verletzt, da der Teilbaum ja
wieder dieselbe Höhe hat wie vor dem Einfügen. Die symmetrischen Fälle folgen entsprechend.
Daraus folgt insgesamt: Um nach dem Einfügen den Baum zu rebalancieren, genügt es, entlang der
Spur soweit nach oben zu gehen, bis zum ersten Mal eine Balanceverletzung (|β (u)| = 2) auftritt, und
dort die entsprechende Rotation anzuwenden.
Also ist nur eine Rotation bzw. eine Doppelrotation nötig, aber die Stelle ist möglicherweise erst an
der Wurzel. Das bedeutet, dass der Aufwand in O(h(T 0 )) = O(h(T )) = O(log n(T )) liegt.
Beispiel 5.2 Betrachte das sukzessive Einfügen von 4, 5, 7, 2, 1, 3, 6 ausgehend von einem leeren Baum:
64
KAPITEL 5. AVL-BÄUME
0
4
∅
1
5
4
2
7
4
0
RR
4
5
RotLinks(4, 5)
0
1
5
0
5
0
4
7
0
7
−1
2
−2
1
5
−1
LL
5
5
RotRechts(4, 2)
−1
−2
0
4
7
4
0
0
7
2
−1
0
2
0
7
0
0
1
2
4
0
1
−2
3
0
LR
5
4
DRotRechts(5, 2, 4)
1
0
2
0
7
−1
0
1
1
2
5
0
4
0
1
0
3
7
0
3
1
6
0
RL
4
4
DRotLinks(5, 7, 6)
2
0
2
0
2
−1
0
0
1
0
5
3
7
6
0
1
0
3
0
5
0
6
Abbildung 5.12: Einfügen von Knoten in einen AVL-Baum
0
7
65
5.3. DIE BASISOPERATIONEN IN AVL-BÄUMEN
5.3.3
Löschen eines Knotens v
Das Löschen (ohne rebalacierende Rotationen) funktioniert wie bei allgemeinen Suchbäumen. Es soll
hier aber trotzdem kurz wiederholt werden:
(i) Suche v. Falls v ein Blatt ist oder nur einen Sohn hat, so lösche v und
hänge gegebenenfalls den einen Sohn an den Vater von v.
Die
(ii) Falls v zwei Söhne hat, so suche im linken Teilbaum den am weitesten rechts stehenden
Knoten u. Tausche die Inhalte von u und v und lösche v.
Schritte (i), (ii) sind in O(h(T )) = O(log n(T )) durchführbar.
Sei T 0 der neue Baum. Das Löschen des Knotens v kann die Balancen seiner Vorfahren (und zwar
nur der Vorfahren) ändern. Also müssen alle Knoten auf dem Pfad zur Wurzel (der Spur) ausgehend
vom gelöschten Knoten untersucht undihre Balancen aktualisiert werden. Tritt dabei in einem Knoten
u eine gestörte Balance auf, ist β 0 (u) = 2 und die Teilbäume sind AVL-Bäume. Das Rotationslemma
(Lemma 5.2) kann also angewendet werden. Dies wird entlang der gesamten Spur durchgeführt.
Beispiel 5.3 Betrachte das schrittweise Löschen der Knoten 4, 8, 6, 5, 2, 1, 7 ausgehend von einem
AVL-Baum:
5
3
8
2
4
2
7
1
10
6
9
3
7
11
2
10
9
9
1
11
11
5
7
6
10
6
6
2
3
8
1
5
8
1
5
4
10
3
7
11
9
66
KAPITEL 5. AVL-BÄUME
3
5
7
2
2
10
1
3
7
11
10
1
9
11
9
7
1
3
10
9
10
7
3
11
11
9
Abbildung 5.13: Löschen von Elementen im ausgeglichenen Baum
Das Löschen des Knoten mit dem Schlüssel 4 selbst ist einfach, da dieser ein Blatt ist. Es führt aber
zu einem unausgeglichenen Baum, denn β (3) = −2. Die Ausgleichsoperation bedingt eine einfache
Rechtsrotation. Ausgleichen wird erneut nach dem Löschen von Knoten 6 notwendig. Dieses Mal
kann der Teilbaum mit der Wurzel 7 durch eine einfache Linksrotation ausgeglichen werden. Löschen
von Knoten 2 ist zwar selbst eine direkte Operation, da nur ein Nachfolger existiert, es impliziert
aber eine Doppellinksrotation. Um den Knoten mit dem Schlüssel 7 zu löschen, wird zunächst die 7
mit dem größten Element seines linken Teilbaumes, d.h. mit der 3, vertauscht und dann wird der neue
Knoten mit der 7 gelöscht. Schließlich wird durch das Löschen dieses Knotens noch eine Linksrotation
verursacht.
Im Allgemeinen muss nach dem Löschen, im Unterschied zum Einfügen, mehrmals rebalanciert werden. 1 Da aber jede Rotation bzw. Doppelrotation in konstanter Zeit durchführbar ist, folgt, dass das
Löschen in O(h(T )) = O(log n(T )) möglich ist. Im schlimmsten Fall kann das Löschen eines Knotens sogar eine Rotation für jeden Knoten entlang der Spur verursachen. Diese Situation ist jedoch ein
eher unglückliches Zusammentreffen von Zufällen. Wie wahrscheinlich sind nun Rotationen im Allgemeinen? Das überraschende Resultat empirischer Tests ist, dass zwar eine Rotation bei etwa jedem
zweiten Einfügen, aber nur bei jedem fünften Löschen erforderlich ist. Löschen in AVL-Bäumen ist
daher im Mittel etwa gleich aufwändig wie Einfügen.
Insgesamt zeigt sich, dass die Klasse der AVL-Bäume balanciert zur Höhe f (n) = 2 log n ist.
1 Aber
auch beim Löschen gibt es einige Spezialfälle in denen man mit maximal einer Rotation auskommt (Übung).
5.4. LITERATURHINWEISE
67
Abschließend ist zu bemerken, dass es auch noch andere Klassen balancierter Suchbäume gibt, die
zur Höhe 2 log n ausgeglichen sind. Bei ihnen sind sowohl beim Einfügen als auch beim Löschen nur
O(1) Rotationen durchzuführen. Dazu gehören zum Beispiel die Red-Black-Trees.
5.4
Literaturhinweise
Balancierte Suchbäume werden in jedem Buch über Datenstrukturen behandelt. Für AVL-Bäume und RedBlack-Trees sei auf [CLRS01] verwiesen.
68
KAPITEL 5. AVL-BÄUME
Kapitel 6
Optimale statische Suchbäume
6.1
Statische Suchbäume allgemein
Bisher haben wir den dynamischen Fall betrachtet, bei dem sich die Schlüsselmenge über die Zeit
ändert und deshalb die Operationen Löschen und Einfügen nötig sind. Jetzt behandeln wir den statischen Fall. Hier gibt es eine feste Schlüsselmenge, für die wir die Zugriffswahrscheinlichkeiten
kennen. Dazu kann man sich vor dem Speichern die richtige Gestalt des Suchbaumes auswählen bzw.
konstruieren, um die mittlere Zugriffszeit klein zu halten. In der Praxis werden optimale statische
Suchbäume beispielsweise für das Speichern auf einer CD-Rom verwendet.
Beispiel 6.1 Für die folgenden Schlüssel mit den entsprechenden Zugriffshäufigkeiten wurden die
beiden statischen Suchbäume T1 und T2 in Abbildung 6.1 zum Abspeichern konstruiert.
Schlüssel si
Häufigkeit βi in %
T1
1
20
2
5
3
10
4
25
T2
3
1
4
6
30
6
2
5
2
5
10
6
1
3
4
5
Abbildung 6.1: Die beiden statischen Suchbäume T1 und T2 zu Bsp. 6.1
69
70
KAPITEL 6. OPTIMALE STATISCHE SUCHBÄUME
Die mittleren Zugriffszeiten der beiden Suchbäume (in Anzahl von Vergleichen):
Für T1
1 · 10 für 3
2 · 20 für 1
2 · 10 für 5
3 · 5 für 2
3 · 25 für 4
3 · 30 für 6
250
Für T2
1 · 30 für 6
2 · 5 für 2
3 · 20 für 1
3 · 10 für 3
4 · 25 für 4
4 · 10 für 5
270
Das ergibt also im Mittel 2, 5 Vergleiche bzgl. T1 und 2, 7 Vergleiche bzgl. T2 . Beim optimalen statischen Suchbaum T3 (Abbildung 6.2) aber werden im Mittel nur 1, 95 Vergleiche benötigt.
T3
4
1
6
3
5
2
Abbildung 6.2: Der optimale statische Suchbaum für Bsp. 6.1.
Wie ermittelt man nun den optimalen statischen Suchbaum? Um das zu beantworten, müssen wir erst
klären, was optimal überhaupt heißen soll.
6.2
Optimalität statischer Suchbäume
Zunächst benötigen wir einige Definitionen. Sei S = {s1 , . . . , sn }, mit s1 < s2 < · · · < sn , die Schlüsselmenge,
die in einem Suchbaum T abgespeichert werden soll. Seien β1 , . . . , βn die Zugriffshäufigkeiten, mit
denen auf die Schlüssel si der Schlüsselmenge S zugegriffen wird. Dann ist
n
C(T ) := ∑ βi · (1 + hT (si ))
| {z }
i=1
(6.1)
(∗)
die Gesamtzugriffszeit für T . Das C steht dabei für cost“. Der Ausdruck (∗) entspricht der Anzahl
”
der Vergleiche, die benötigt werden, um si zu finden. Sei nun
βn
β1
,...,
p=
∑ βi
∑ βi
6.2. OPTIMALITÄT STATISCHER SUCHBÄUME
71
die Häufigkeitsverteilung. Dann ist
C(T )
∑ni=1 βi
E p (T ) :=
(6.2)
die mittlere Zugriffszeit pro Schlüssel. Nun können wir uns der Definition der Optimalität zuwenden:
T heißt optimaler statischer Suchbaum für S und β1 , . . . βn , falls
C(T ) ≤ C(T 0 )
⇔ E p (T ) ≤ E p (T
für alle Suchbäume T 0
zu S und β1 , . . . , βn
(6.3)
0)
Satz 6.1 Sei T ein Suchbaum für S und T 0 ein Teilbaum von T . Dann gilt:
1. Die Schlüsselmenge S 0 der Schlüssel in T 0 bildet eine konsekutive Teilsequenz (Intervall) si <
si+1 < · · · < s j von s1 < s2 < · · · < sn .
2.
Prinzip der optimalen Substruktur“
”
Ist T optimal für S und β1 , . . . , βn , so ist T 0 optimal für die zugehörige Schlüsselmenge S 0 =
{si , si+1 , . . . , s j } und die Häufigkeiten βi , βi+1 , . . . , β j ist.
Beweis:
zu 1.:
T
Spur zu Wurzel
sk von T 0
sk
T0
Abbildung 6.3: Statischer Suchbaum T mit einem Teilbaum T 0 .
Seien si der kleinste Schlüssel in T 0 , s j der größte Schlüssel in T 0 und sk der Schlüssel in der Wurzel
von T 0 . Sei s` ∈ S mit si < s` < s j . Dann ist zu zeigen, dass s` aus der Schlüsselmenge S 0 ist, der
Knoten mit dem Schlüssel s` also im Teilbaum T 0 liegt. Angenommen, das sei nicht so. Dann sind
zwei Fälle zu unterscheiden.
72
KAPITEL 6. OPTIMALE STATISCHE SUCHBÄUME
Fall (i): s` liegt auf der Spur zu sk .
s`
Sei o.b.d.A. sk im linken Teilbaum von s` . Dann würde aber
folgen, dass s j < s` ist, was einen Widerspruch zur Voraussetzung ergibt.
sk
T0
Fall (ii): s` liegt außerhalb der Spur zu sk .
sr
sk
s`
Betrachte den kleinsten gemeinsamen Vorfahren sr von sk
und s` . Sei o.b.d.A. T 0 links von sr . Dann folgt: s j < sr <
s` , da s` rechts von sr liegen muss. damit ergibt sich allerdings ein Widerspruch zur Voraussetzung.
T0
zu 2.:
T
`
`i = ` + `i 0
sk
`i 0
si
T0
Abbildung 6.4: Der statische Suchbaum T zur Veranschaulichung von `, `i und `i 0
Sei `i = hT (si ) + 1 = ` + `i 0 , wobei `i 0 = hT 0 (si ) + 1 ist und ` = hT (sk ). Dann gilt:
6.3. KONSTRUKTION EINES OPTIMALEN STATISCHEN SUCHBAUMES
C(T ) = β1 `1 + · · · + βn `n
= β1 `1 + · · · + βi−1 `i−1
+ βi `i + · · · + β j ` j
+ β j+1 ` j+1 + · · · + βn `n
=
∑ vorher
73
(Schlüssel vor S 0 =: ∑ vorher)
(Schlüssel in S 0 )
(Schlüssel nach S 0 =: ∑ nachher)
+ (`i 0 + `)βi + · · · + (` j 0 + `)β j
+
=
∑ nachher
∑ vorher
+ `i 0 βi + · · · + ` j 0 β j + `βi + · · · + `β j
+
=
∑ nachher
∑ vorher + ∑ nachher
+ `i 0 βi + · · · + ` j 0 β j +` · (βi + · · · + β j)
{z
}
|
=C(T 0 )
=
vorher + ∑ nachher + ` · (βi + · · · + β j) +C(T 0 )
∑
|
{z
}
unabhängig von T 0
0
⇒ C(T ) = Konstante +C(T )
Ist also T optimal, folgt, dass auch T 0 optimal ist.
Der Satz zeigt, dass sich jeder optimale statische Suchbaum aus optimalen statischen Suchbäumen für
eine kleinere Schlüsselmenge (mit den entsprechenden Häufigkeiten) zusammensetzt. Das bedeutet,
dass optimale statische Suchbäume rekursiv aus kleineren optimalen statischen Suchbäumen aufgebaut werden können.
6.3
Konstruktion eines optimalen statischen Suchbaumes
Dazu zunächst etwas Notation. Sei Ti j ein Suchbaum für die Schlüssel si+1 , . . . , s j . Wenn Ti j die Wurzel
sk hat, so hat Ti j die Teilbäume Ti,k−1 und Tk, j , vgl. Abbildung 6.5.
Dann seien Ci j := C(Ti j ) die Kosten von Ti j und ωi j := βi+1 + · · · + β j die Summe der Häufigkeiten der
Schlüssel in Ti j . Sei `irj = hTi j (sr ) + 1 die Anzahl der Vergleiche, um den Knoten mit dem Schlüssel sr
im Teilbaum Ti j zu finden. Damit gilt:
j
+ · · · + β j · `ijj
Ci j = βi+1 · `ii+1
j
j
j
= βi+1 · `ii+1
+ · · · + βk−1 · `ik−1
+ βk · (0 + 1) + βk+1 · `ik+1
+ · · · + β j · `ijj
74
KAPITEL 6. OPTIMALE STATISCHE SUCHBÄUME
Ti, j
Ti,k−1
Sk
Tk, j
Abbildung 6.5: Der statische Suchbaum Ti j mit der Wurzel sk
Da die Teilbäume Ti,k−1 und Tk, j den gemeinsamen Vater sk und daher alle Knoten in ihnen eine um 1
geringere Höhe als im Baum Ti, j haben, gilt dann:
kj
kj
i,k−1
Ci j = βi+1 · (`i,k−1
i+1 + 1) + · · · + βk−1 · (`k−1 + 1) + βk + βk+1 · (`k+1 + 1) + · · · + β j · (` j + 1)
i,k−1
= βi+1 · (`i,k−1
i+1 ) + βi+1 + · · · + βk−1 · (`k−1 ) + βk−1 + βk
j
+βk+1 · (`kk+1
) + βk+1 + · · · + β j · (`kj j ) + β j
i,k−1
= βi+1 · (`i,k−1
i+1 + · · · + βk−1 +βk
i+1 ) + · · · + βk−1 · (`k−1 ) + β
{z
}
{z
} |
|
ωi,k−1
Ci,k−1
j
) + · · · + β j · (`kj j )
+ βk+1 + · · · + β j + βk+1 · (`kk+1
{z
} |
|
{z
}
ωk j
Ck j
= Ci,k−1 + ωi,k−1 + βk + ωk j +Ck, j
|
{z
}
ωi j
Also ist Ci j = ωi j +Ci,k−1 +Ck j . Die Kosten für den Teilbaum Ti j mit der Wurzel sk ergeben sich also
aus der Summe der Häufigkeiten in Ti j , den Kosten für den linken Teilbaum Ti,k−1 und den Kosten für
den rechten Teilbaum Tk j .
Seien nun Ci j opt die Kosten eines optimalen statischen Suchbaumes für die Schlüssel si+1 , . . . , s j . Sie
lassen sich wegen Satz 6.1 folgendermaßen berechnen:
Ci j opt = min ωi j +Ci,k−1 opt +Ck j opt
(6.4)
k=i+1,..., j
Jeder Schlüsselwert der Schlüsselmenge wird als Wurzel ausprobiert und es werden jeweils die Kosten
der optimalen Teilbäume ermittelt, um sie zu der Summe der Häufigkeiten zu addieren. Der optimale
Baum hat dann den Schlüsselwert als Wurzel, für den die Kosten am geringsten sind. Diese Rekursionsformel wird dann zur Konstruktion von optimalen statischen Suchbäumen verwendet. Sie ähnelt
der Bellman-Gleichung des Kürzeste-Wege-Problems. Das folgende Beispiel illustriert die Konstruktion.
Beispiel 6.2 Konstruktion eines optimalen statischen Suchbaums:
i
si
βi
1
1
0, 1
2
2
0, 1
3
3
0, 2
4
4
0, 05
5
5
0, 3
6
6
0, 25
(∑ βi = 1)
6.3. KONSTRUKTION EINES OPTIMALEN STATISCHEN SUCHBAUMES
75
Intervalllänge 0:
Cii opt = 0
für alle i
Intervalllänge 1:
C0,1 opt
C1,2 opt
C2,3 opt
C3,4 opt
C4,5 opt
C5,6 opt
=
=
=
=
=
=
ω0,1 +C0,0 opt +C1,1 opt
ω1,2 +C1,1 opt +C2,2 opt
ω2,3 +C2,2 opt +C3,3 opt
ω3,4 +C3,3 opt +C4,4 opt
ω4,5 +C4,4 opt +C5,5 opt
ω5,6 +C5,5 opt +C6,6 opt
=
=
=
=
=
=
ω0,1
ω1,2
ω2,3
ω3,4
ω4,5
ω5,6
=
=
=
=
=
=
β1
β2
β3
β4
β5
β6
=
=
=
=
=
=
0, 1
0, 1
0, 2
0, 05
0, 3
0, 25
Der zugehörige Baum besteht jeweils nur aus der Wurzel.
Intervalllänge 2:
C0,2 opt
min ω0,2 +C0,k−1 opt +Ck,2 opt
k=1,2
= min 0, 2 +C0,0 opt +C1,2 opt ; 0, 2 +C0,1 opt +C2,2 opt
=
= min [0, 2 + 0 + 0, 1; 0, 2 + 0, 1 + 0]
= 0, 3
(für k = 1 oder k = 2)
Da der Baum für beide k optimal ist, kann zwischen den beiden Möglichkeiten gewählt werden. Der
zugehörige Baum T0,2 für k = 1 sieht dann so aus:
s1
T0,2
=
=
s2
T0,0
C1,3 opt
s1
T1,2
min ω1,3 +C1,k−1 opt +Ck,3 opt
k=2,3
= min 0, 3 +C1,1 opt +C2,3 opt ; 0, 3 +C1,2 opt +C3,3 opt
=
= min [0, 3 + 0 + 0, 2; 0, 3 + 0, 1 + 0]
= 0, 4
(für k = 3)
s3
T1,3
s3
=
=
s2
T1,2
T3,3
76
KAPITEL 6. OPTIMALE STATISCHE SUCHBÄUME
C2,4 opt
min ω2,4 +C2,k−1 opt +Ck,4 opt
k=3,4
= min 0, 25 +C2,2 opt +C3,4 opt ; 0, 25 +C2,3 opt +C4,4 opt
=
= min [0, 25 + 0 + 0, 05; 0, 25 + 0, 2 + 0]
= 0, 3
(für k = 3)
s3
T2,4
=
=
s4
T2,2
C3,5 opt
s3
T3,4
min ω3,5 +C3,k−1 opt +Ck,5 opt
k=4,5
= min 0, 35 +C3,3 opt +C4,5 opt ; 0, 35 +C3,4 opt +C5,5 opt
=
= min [0, 35 + 0 + 0, 3; 0, 35 + 0, 05 + 0]
= 0, 4
(für k = 5)
s5
T3,5
=
=
s4
T3,4
C4,6 opt
s5
T5,5
min ω4,6 +C4,k−1 opt +Ck,6 opt
k=5,6
= min 0, 55 +C4,4 opt +C5,6 opt ; 0, 55 +C4,5 opt +C6,6 opt
=
= min [0, 55 + 0 + 0, 25; 0, 55 + 0, 3 + 0]
= 0, 8
(für k = 5)
s5
T4,6
s5
=
=
s6
T4,4
T5,6
Bei den zweielementigen Bäumen ist das Ergebnis klar, da von den beiden Knoten der mit der größeren
Wahrscheinlichkeit die Wurzel bildet.
6.3. KONSTRUKTION EINES OPTIMALEN STATISCHEN SUCHBAUMES
Intervalllänge 3:
C0,3 opt
min ω0,3 +C0,k−1 opt +Ck,3 opt
k=1,2,3
= ω0,3 + min C0,k−1 opt +Ck,3 opt
k=1,2,3
= ω0,3 + min C0,0 opt +C1,3 opt ; C0,1 opt +C2,3 opt ; C0,2 opt +C3,3 opt
=
= 0, 4 + min [0 + 0, 4; 0, 1 + 0, 2; 0, 3 + 0]
= 0, 4 + 0, 3 = 0, 7 (für k = 2 oder k = 3)
In der Abbildung des entsprechenden Baumes wurde k = 2 gewählt:
s2
T0,3
=
=
s1
T0,1
C1,4 opt
s2
s3
T2,3
= ω1,4 + min C1,k−1 opt +Ck,4 opt
k=2,3,4
= ω1,4 + min C1,1 opt +C2,4 opt ; C1,2 opt +C3,4 opt ; C1,3 opt +C4,4 opt
= 0, 35 + min [0 + 0, 3; 0, 1 + 0, 05; 0, 4 + 0]
= 0, 35 + 0, 15 = 0, 5
(für k = 3)
s3
T1,4
=
=
s2
T1,2
C2,5 opt
s3
s4
T3,4
= ω2,5 + min C2,k−1 opt +Ck,5 opt
k=3,4,5
= ω2,5 + min C2,2 opt +C3,5 opt ; C2,3 opt +C4,5 opt ; C2,4 opt +C5,5 opt
= 0, 55 + min [0 + 0, 4; 0, 2 + 0, 3; 0, 3 + 0]
= 0, 55 + 0, 4 = 0, 5
(für k = 5)
s5
T2,5
=
s5
=
T2,4
T5,5
s3
s4
77
78
KAPITEL 6. OPTIMALE STATISCHE SUCHBÄUME
C3,6 opt
= ω3,6 + min C3,k−1 opt +Ck,6 opt
k=4,5,6
= ω3,6 + min C3,3 opt +C4,6 opt ; C3,4 opt +C5,6 opt ; C3,5 opt +C6,6 opt
= 0, 6 + min [0 + 0, 8; 0, 05 + 0, 25; 0, 4 + 0]
= 0, 6 + 0, 3 = 0, 9 (für k = 5)
s5
T3,6
=
s5
=
s4
T3,4
s6
T5,6
Intervalllänge 4:
C0,4 opt
= ω0,4 + min C0,k−1 opt +Ck,4 opt
k=1,2,3,4
= ωo,4 + min C0,0 opt +C1,4 opt ; C0,1 opt +C2,4 opt ; C0,2 opt +C3,4 opt ; C0,3 opt +C4,4 opt
= 0, 45 + min [0 + 0, 5; 0, 1 + 0, 3; 0, 3 + 0, 05; 0, 7 + 0]
= 0, 45 + 0, 35 = 0, 8
(für k = 3)
s3
T0,4
=
=
T0,2
C1,5 opt
s3
s1
s4
s2
T3,4
= ω1,5 + min C1,k−1 opt +Ck,5 opt
k=2,3,4,5
= ω1,5 + min C1,1 opt +C2,5 opt ; C1,2 opt +C3,5 opt ; C1,3 opt +C4,5 opt ; C1,4 opt +C5,5 opt
= 0, 65 + min [0 + 0, 85; 0, 1 + 0, 4; 0, 4 + 0, 3; 0, 5 + 0]
= 0, 65 + 0, 5 = 1, 15
(für k = 3 oder k = 5)
In der Abbildung ist k = 3:
s3
T1,5
=
s3
=
T1,2
T3,5
s2
s5
s4
6.3. KONSTRUKTION EINES OPTIMALEN STATISCHEN SUCHBAUMES
C2,6 opt
= ω2,6 + min C2,k−1 opt +Ck,6 opt
k=3,4,5,6
= ω2,6 + min C2,2 opt +C3,6 opt ; C2,3 opt +C4,6 opt ; C2,4 opt +C5,6 opt ; C2,5 opt +C6,6 opt
= 0, 8 + min [0 + 0, 9; 0, 2 + 0, 8; 0, 3 + 0, 25; 0, 85 + 0]
= 0, 8 + 0, 55 = 1, 35
(für k = 5)
s5
T2,6
s5
=
=
T2,4
s6
s3
s4
T5,6
Intervalllänge 5:
C0,5 opt
min C0,k−1 opt +Ck,5 opt
k=1,2,3,4,5
= ω0,5 + min C0,0 opt +C1,5 opt ; C0,1 opt +C2,5 opt ; C0,2 opt +C3,5 opt ; C0,3 opt +C4,5 opt ;
C0,4 opt +C5,5 opt
= ω0,5 +
= 0, 75 + min [0 + 1, 15; 0, 1 + 0, 85; 0, 3 + 0, 4; 0, 7 + 0, 3; 0, 8 + 0]
= 0, 75 + 0, 7 = 1, 45
(für k = 3)
s3
T0,5
=
=
T0,2
C1,6 opt
s3
s1
s5
s2
T3,5
s4
min C1,k−1 opt +Ck,6 opt
k=2,3,4,5,6
= ω1,6 + min C1,1 opt +C2,6 opt ; C1,2 opt +C3,6 opt ; C1,3 opt +C4,6 opt ; C1,4 opt +C5,6 opt ;
C1,5 opt +C6,6 opt
= ω1,6 +
= 0, 9 + min [0 + 1, 35; 0, 1 + 0, 9; 0, 4 + 0, 8; 0, 5 + 0, 25; 1, 15 + 0]
= 0, 9 + 0, 75 = 1, 65
(für k = 5)
s5
T1,6
=
s5
=
T1,4
T5,6
s3
s2
s6
s4
79
80
KAPITEL 6. OPTIMALE STATISCHE SUCHBÄUME
Intervalllänge 6:
C0,6 opt
min
C0,k−1 opt +Ck,6 opt
k=1,2,3,4,5,6
= ω0,6 + min C0,0 opt +C1,6 opt ; C0,1 opt +C2,6 opt ; C0,2 opt +C2,6 opt ; C0,3 opt +C4,6 opt ;
C0,4 opt +C5,6 opt ; C0,5 opt +C6,6 opt
= ω0,6 +
= 1 + min [0 + 1, 65; 0, 1 + 1, 35; 0, 3 + 0, 9; 0, 7 + 0, 8; 0, 8 + 0, 25; 1, 45 + 0]
= 1 + 1, 05 = 2, 05
(für k = 5)
So ergibt sich dann schließlich der optimale statische Suchbaum für die gegebene Schlüsselmenge mit
den zugehörigen Häufigkeiten zu
s5
T0,6
s5
=
=
T0,4
s3
s1
T5,6
s4
s2
Das Prinzip dieses Algorithmus heißt auch dynamische Optimierung oder dynamisches Programmieren.
Aufwand
Abschließend wollen wir uns noch ansehen, wie aufwändig der Algorithmus ist. Dazu betrachten wir
die folgende Tabelle, die in Zeile i die Größenordnung des Aufwands für die Ermittlung des optimalen
Baums mit Intervalllänge i angibt.
n
n−1
n−2
..
.
Bäume mit
Bäume mit
Bäume mit
1
2
3
..
.
Knoten
Knoten
Knoten
⇒
⇒
⇒
n·1
(n − 1) · 2
(n − 2) · 3
..
.
2
1
Bäume mit
Bäume mit
n−1
n
Knoten
Knoten
⇒
⇒
2 · (n − 1)
1·n
n−1
⇒
∑
i=0
|{z}
(n − i) (i + 1)
| {z } | {z }
≤n
∈ Θ(n3 )
≤n
n Summen
Der Aufwand des Gesamtalgorithmus beträgt für n Schlüssel also Θ(n3 ) elementare Schritte; er kann
aber durch geschicktere Bestimmung der besten Wurzel auf O(n2 ) elementare Schritte reduziert werden. Da der Aufwand sehr hoch ist, findet der Algorithmus eigentlich nur dort Anwendung, wo es sich
wirklich lohnt, wenn also auf den statischen Suchbaum wirklich sehr oft zugegriffen werden soll.
6.4. LITERATURHINWEISE
6.4
81
Literaturhinweise
Der hier dargestellte Algorithmus wird u.a. in [CLRS01] behandelt. Die Verbesserung auf O(n2 ) geht auf
[Knu71] zurück.
82
KAPITEL 6. OPTIMALE STATISCHE SUCHBÄUME
Kapitel 7
B-Bäume
7.1
Definition und Eigenschaften
Im letzten Kapitel zum Thema Bäume soll es um die sogenannten B-Bäume gehen. Es gibt unterschiedliche Ansichten darüber, wofür das B in B-Baum steht. Die häufigste Interpretation ist, dass es
für balanciert steht, da alle Blätter auf der gleichen Ebene im Baum stehen. Eine weitere Interpretation
ist, dass das B nach dem Namen seines Erfinders Rudolf Bayer für Bayer steht.
Die bisher betrachteten Suchbäume wurden nur im Hauptspeicher, also nur zur Laufzeit, genutzt. Jetzt
wollen wir große Datenmengen im Hintergrundspeicher, wie zum Beispiel auf Festplatten, Bändern,
etc., abspeichern. Das Problem hierbei ist, das der Hintergrundspeicher längere Zugriffszeiten auf
einen bestimmten Speicherort hat. Das wirkt sich natürlich auf die Lese- und Schreibzeit aus. Eine
Lösung für dieses Problem ist es, große Blöcke von Daten auf einmal zu lesen bzw. zu schreiben. Dabei
sind Blockgrößen von 211 − −214 Byte, also 2 – 16 kB, üblich. Die Blöcke werden in Suchbäumen
organisiert, wobei ein Block mehrere Datensätze mit ihrem entsprechenden Schlüssel enthält. Diese
Suchbäume nennt man dann B-Bäume. Da nun mehrere Datensätze mit unterschiedlichen Schlüsseln
in einem Knoten gespeichert sind, werden mehrere Teilbäume in den Knoten benötigt, um zu den
richtigen Suchschlüsseln verzweigen zu können. Die Anzahl der Teilbäume hängt von der Anzahl der
Datensätze ab, die in einem Knoten gespeichert sind. In Abbildung 7.1 sind zur Veranschaulichung
zwei Datensätze in einem Knoten mit drei Teilbäumen gespeichert.
D H
Schlüssel
≤D
Schlüssel
≥ D aber ≤ H
Schlüssel
≥H
Abbildung 7.1: Dieser B-Baum hat drei Teilbäume
83
84
KAPITEL 7. B-BÄUME
Beispiel 7.1 In Abbildung 7.2 sieht man ein Beispiel eines B-Baumes, bei dem Buchstaben den Datensätzen entsprechen. Diese stellen auch gleichzeitig den Suchschlüssel dar.
M
DH
BC FG
QTX
JKL
NP
RS
VW YZ
Abbildung 7.2: Beispiel für einen B-Baum
Betrachten wir jetzt die genaue Definition von B-Bäumen der Ordnung t:
1. Jeder Knoten x
• kennt die Anzahl n[x] seiner Datensätze
• kennt die n[x] Datensätze selbst, die aufsteigend sortiert sind
key1 [x] ≤ key2 [x] ≤ · · · ≤ keyn[x] [x]
• weiß durch die Boolesche Variable lea f [x], ob er ein Blatt ist (true) oder nicht (false)
2. Ist der Knoten x kein Blatt, so enthält er n[x] + 1 Zeiger (c1 [x], . . . cn[x]+1 [x]) auf seine n[x] + 1
Kinder
3. Die n[x] + 1 Teilbäume enthalten nur Daten mit Schlüsseln, die die Suchbaum-Eigenschaft“
”
nicht verletzen, d.h.:
x
key1 [x]
k1
k2
...
...
keyn[x] [x]
kn[x]
kn[x]+1
k1 ≤ key1 [x] ≤ k2 ≤ · · · ≤ keyn[x] [x] ≤ kn[x]+1
4. Alle Blätter haben dieselbe Höhe.
5. Jeder Knoten enthält mindestens t − 1 Datensätze. Die Wurzel kann auch weniger Datensätze,
aber mindestens einen, besitzen. Jeder Knoten enthält höchstens 2t − 1 Datensätze.
Punkt 5 der Definition zeigt, dass die Speicherausnutzung bei fester Blockgröße und bei fester Größe
der Datensätze zwischen 50% und 100% liegt. Es wird Speicherplatz geopfert, damit die Basisoperationen Suchen, Einfügen und Löschen schnell durchgeführt werden können. Bevor wir uns die Basisoperationen genauer ansehen, schätzen wir erst einmal ab, wieviele Datensätze n in einen B-Baum
fester Höhe mindestens passen. Dazu folgender Satz:
85
7.1. DEFINITION UND EIGENSCHAFTEN
Satz 7.1 Für einen extremalen (knotenminimalen) B-Baum der Ordnung t mit n Datensätzen und einer
Höhe h gilt:
n+1
(7.1)
n ≥ 2t h − 1 bzw. h ≤ logt
2
Beweis: Betrachte einen extremalen B-Baum der Ordnung t mit n Datensätzen und einer Höhe h. Da
in diesem extremalen B-Baum die Knotenzahl minimal zur Höhe ist und diese aber von der Anzahl der
Datensätze pro Knoten abhängt, müssen in jedem Knoten so wenig Datensätze wie möglich stehen.
Deswegen ist in der Wurzel nur 1 Datensatz und in allen übrigen Knoten sind t − 1 Datensätze. Damit
hat die Wurzel 2 Söhne und die anderen Knoten haben jeweils t Söhne.
1
t −1
t −1
0
1 Datensatz in der Wurzel
1
1
t − 1 Datensätze pro Knoten
+2 · (t − 1)
2
t − 1 Datensätze pro Knoten
+2 · t · (t − 1)
3
t − 1 Datensätze pro Knoten
+2 · t 2 · (t − 1)
t Söhne
t −1
t −1
t −1
t −1
...
h
t − 1 Datensätze pro Knoten
...
t −1
...
...
...
t −1
+2 · t h−1 · (t − 1)
⇒ n ≥ 1 + 2 · (t − 1) + 2 · t · (t − 1) + 2 · t 2 · (t − 1) + · · · + 2 · t h−1 · (t − 1)
h−1
= 1 + 2 · (t − 1) · ∑ t i
= 1 + 2 · (t − 1) ·
i=0
th − 1
t −1
= 2 · th − 1
n+1
⇒ h ≤ logt
2
Beispiel 7.2 Illustration der Speicherkapazität von B-Bäumen
Speichert man in einem Block ungefähr 2 kB, dann kann man bei einer üblichen Datensatzgröße von
Telefonbuchdatensätzen von 100 Byte pro Datensatz maximal 20 Datensätze pro Block speichern.
Das entspricht der Ordnung t = 10. In einem B-Baum der Höhe h = 5 kann man bereits 2 · 105 −
1 ≈ 200000 Datensätze speichern. Das entspricht dem Telefonbuch einer mittleren Stadt. Da eine
sehr große Datenmenge bei kleiner Höhe in B-Bäumen abgespeichert werden kann, bilden sie die
Standardtechnik für große Datenbanken und für die File-Organisation auf der Festplatte.
86
KAPITEL 7. B-BÄUME
7.2
Basisoperationen in B-Bäumen
7.2.1
Suchen
Das Suchen entspricht im Großen und Ganzen dem Suchen in binären Suchbäumen. Der einzige Unterschied besteht darin, dass die einzelnen Knoten noch durchsucht werden müssen, bis der gesuchte
Schlüssel oder die Stelle zum Absteigen in den Teilbaum gefunden wurde.
7.2.2
Einfügen
In B-Bäumen wird ein neuer Datensatz nur in einem Blatt eingefügt. Das entsprechende Blatt muss
erst gesucht werden. Das Problem ist, dass das Blatt voll sein könnte, also 2t − 1 Datensätze in diesem
Blatt stehen könnten. Um dieses Problem zu vermeiden, wollen wir eine vorsichtige Strategie fahren.
Beim Absteigen soll nämlich dafür gesorgt werden, dass alle Vorfahren des entsprechenden Blattes
nicht voll sind. Dazu werden unterwegs volle Knoten gesplittet. Dies ist möglich, da die Vorgänger
der Knoten auf der Spur nicht voll sind, weil sie wegen eben dieser Strategie sonst ja schon gesplittet
worden wären.
y
x
...
y
=⇒
... k ...
... k ...
...
x0
...
x 00
Abbildung 7.3: Splitten eines Knoten x
Abbildung 7.3 zeigt, wie das Splitten funktioniert. Es wird in der folgenden Situation beim Einfügen
angewendet.
Seien die Knoten x und y auf der Spur zu dem Blatt, in das eingefügt werden soll, und sei y der Vater
von x. Wir wissen, dass y nicht voll ist, denn, wenn er voll gewesen war, wurde er im vorherigen
Schritt gesplittet. Es kann also noch mindestens ein Datensatz dazukommen.
Sei nun der Knoten x voll. Er hat also 2t − 1 Datensätze. Dann wird der mittlere Datensatz k nach oben
an die entsprechende Stelle in Knoten y verschoben. Alle Datensätze aus x, die kleiner gleich k sind,
kommen nun in den Knoten x 0 , der links von k steht und alle, die größer als k sind, kommen in den
Knoten x 00 , der rechts von k steht. Die beiden Knoten x 0 und x 00 sind nun nicht mehr voll, sondern haben
t − 1 Datensätze. Danach wird weiter nach dem Blatt gesucht, in dass der neue Datensatz eingefügt
werden soll und gegebenenfalls wieder gesplittet.
Diese vorsichtige Vorgehensweise bewirkt, dass man in einem Durchlauf einfügen kann. Ist speziell
die Wurzel voll, muss diese gesplittet werden. Die neue Wurzel hat dann nur einen Knoten. So wächst
der B-Baum in seiner Höhe.
Beispiel 7.3 Einfügen in einen B-Baum
87
7.2. BASISOPERATIONEN IN B-BÄUMEN
In den folgenden B-Baum der Ordnung t = 3 sollen nacheinander die Datensätze B, Q, L, und F
eingefügt werden. In jedem Knoten müssen sich dann mindestens 2 Datensätze befinden und dürfen
maximal 5 Datensätze sein. Dabei wird die Position, an der der Knoten eingefügt werden soll, durch
einen blauen Pfeil angezeigt. Der Datensatz, der durch Splitten eine Ebene nach oben geht, ist blau
eingekreist.
GMPX
ACDE
JK
NO
RSTUV
YZ
B einfügen
Das B kann einfach zwischen A und C eingefügt werden, ohne das ein Knoten gesplittet werden muss.
GMPX
ABCDE
JK
NO
RSTUV
YZ
Q einfügen
Da der Knoten (RSTUV) voll ist, muss er, bevor das Q vor das R eingefügt werden kann, gesplittet
werden. Dabei kommt das T einen Knoten nach oben, also in die Wurzel.
GMPTX
ABCDE
JK
NO
QRS
UV YZ
L einfügen
Das L wird hinter das K eingefügt. Allerdings muss vorher die Wurzel gesplittet werden. Dabei kommt
das P in die neue Wurzel und der Baum wird höher.
P
GM
ABCDE
F einfügen
JKL
TX
NO
QRS
UV
YZ
88
KAPITEL 7. B-BÄUME
Um das F hinter dem E einfügen zu können, muss der Knoten (ABCDE) erst gesplittet werden, wobei
das C nach oben vor das G wandert.
P
CGM
AB
DEF
TX
JKL
NO
QRS
UV
YZ
Da man in einem Durchlauf einfügen kann, hat das Einfügen von einem Datensatz einen Aufwand von
O(h).
7.2.3
Löschen
Beim Löschen verwenden wir eine ähnlich vorsichtige Strategie, wie beim Einfügen. Sind nämlich
Knoten fast leer, d.h. haben sie t − 1 Datensätze, so wird die Anzahl der Datensätze erhöht, indem
man von einem Nachbarn einen Datensatz borgt“ oder zwei Knoten miteinander verschmilzt“.
”
”
Algorithmus 7.1 (Löschen eines Schlüssels s)
Starte die Suche nach dem zu löschenden Schlüssel s.
1. Der Schlüssel s ist im momentanen Knoten x und x ist ein Blatt. Dann wird s einfach gelöscht.
(Durch entsprechende Vorbereitung, die weiter unten erklärt wird, wurde dafür gesorgt, dass x
genügend Datensätze enthält.)
2. Der Schlüssel s ist im momentanen Knoten x und x ist ein innerer Knoten.
(a) Das Kind y von x vor dem Schlüssel s hat mindestens t Datensätze. Finde den Vorgänger
s 0 von s in dem Teilbaum mit der Wurzel y. Tausche s und s 0 . Lösche s rekursiv.
x
... s ...
y
...
≥t
s0
(b) Das Kind z von x nach dem Schlüssel s hat mindestens t Datensätze. Gehe entsprechend
(a) vor.
(c) Die Knoten y und z haben beide t − 1 Schlüssel. Verschmelze y, s, z zu y 0 , der dann 2t − 1
Datensätze enthält. Diese Verschmelzung ist möglich, da x mehr als t Schlüssel enthält.
Lösche s nun rekursiv.
89
7.2. BASISOPERATIONEN IN B-BÄUMEN
x
x
... s ...
...
=⇒
y
z
...
...
t −1
t −1
...
...
s
y0
2t − 1
3. Ist der Schlüssel s nicht im momentanen Knoten x, so bestimme die Wurzel y des Teilbaumes, der
s enthält. Falls y nur t − 1 Datensätze enthält, so wende (a) oder (b) an, um mehr als t Schlüssel
sicherzustellen. Suche dann rekursiv in y weiter.
(a) y hat nur t −1 Schlüssel, aber einen Bruder z mit t Schlüsseln. Mache einen Schlüsseltransfer
von z zu y und hänge den entsprechenden Teilbaum um:
x
x
... k ...
...
...
=⇒
y
...
...
t −1
≥t
y
z
z
...
k
...
≥ t −1
t
(b) Haben y und alle seine Brüder nur t − 1 Schlüssel, verschmelze y mit einem Bruder z.
x
x
... k ...
...
=⇒
y
z
...
...
t −1
t −1
...
k
...
y0
2t − 1
4. Sonderfall: Wenn die Wurzel leer wird, lösche die Wurzel.
Beispiel 7.4 In den folgenden B-Baum der Ordnung t = 3 sollen nacheinander die Datensätze F, M,
G, und D gelöscht werden. In jedem Knoten müssen sich dann mindestens 2 Datensätze befinden und
dürfen maximal 5 Datensätze sein. Der Pfeil zeigt immer auf den zu löschenden Datensatz. Blaue
Knoten werden jeweils zusammen mit dem eingekreisten Datensatz verschmolzen.
P
CGM
AB
F löschen
DEF
JKL
TX
NO
QRS
UV
YZ
90
KAPITEL 7. B-BÄUME
Hier muss der 1. Fall des Algorithmus angewendet werden. Da F in einem Blatt steht, kann es einfach
gelöscht werden.
P
CGM
AB
DE
JKL
TX
NO
QRS
UV
YZ
M löschen
Es trifft Fall (2a) zu. M steht in dem inneren Knoten (CGM). Da der Knoten (JKL) 3 Datensätze
enthält, wird M mit seinem Vorgänger L (hier blau eingekreist) ausgetauscht. Dann wird M nach dem
1. Fall gelöscht.
P
CGL
AB
DE
JK
TX
NO
QRS
UV
YZ
G löschen
G steht im inneren Knoten (CGL). Da die beiden Söhne vor und nach dem G (Knoten (DE) und
(JK)) jeweils nur 2 Datensätze enthalten, müssen sie zusammen mit dem G entsprechend Fall (2c)
verschmolzen werden. Dann steht G in einem Blatt und kann einfach wie üblich gelöscht werden.
P
TX
CL
AB
DEJK
NO
QRS
UV
YZ
D löschen
D steht im Blatt (DEJK). Aber auf der Suche danach stellen wir fest, dass die Knoten (CL) und
(TX) jeweils nur 2 Datensätze enthalten. Deswegen müssen sie zusammen mit dem P aus der Wurzel
entsprechend Fall (3b) verschmolzen werden. Damit tritt der 4. Fall auf, da die Wurzel leer wird. Sie
wird dann einfach gelöscht. Nun kann D aus seinem Blatt gelöscht werden.
91
7.3. LITERATURHINWEISE
CLPTX
AB
EJK
NO
QRS
UV
YZ
Auch das Löschen kann also in einem Durchlauf entlang des Weges bis zum Blatt geschehen, also in
O(h) Operationen mit Knoten. Pro Knoten sind dabei maximal 2t − 1 Operationen mit Datensätzen
durchzuführen. Dies ergibt insgesamt den Aufwand von O(h ·t) für die Operationen Suchen, Einfügen
und Löschen in B-Bäumen.
7.3
Literaturhinweise
Die hier gewählte Darstellung lehnt sich eng an [CLRS01] an. Von dort sind auch die Beispiele entnommen.
92
KAPITEL 7. B-BÄUME
Kapitel 8
Hashing
Hashing ist ein anderes Vorgehen, das auch ohne Baumstrukturen ein effizientes Suchen ermöglicht.
Wie bei Bucketsort ist auch hier eine der grundsätzlichen Eigenschaften, dass Hashing nicht auf
paarweisen Vergleichen beruht. Beim Hashing werden die Datensätze in einem Array mit Indizes
0, . . . , m − 1, der sogenannten Hash-Tabelle gespeichert.
Wenn wir nun wüssten, dass als Schlüssel nur die Zahlen 0, . . . , N, N ∈ N vorkommen, könnten wir die
Schlüssel direkt als Array-Indizes verwenden. Das ist aber im Allgemeinen nicht erfüllt. Viel öfter tritt
der Fall auf, dass die Anzahl der tatsächlichen Schlüssel viel kleiner ist als die Anzahl der möglichen
Schlüssel. Daher berechnet man sich beim Hashing einen Array-Index aus dem Schlüssel:
Bezeichne U das Universum aller möglichen Schlüssel und K die Menge der Schlüssel, die auch
tatsächlich vorkommen. Dann verwendet man eine Hash-Funktion h, h : U → {0, . . . , m−1}, die jedem
Schlüssel k einen Index h(k) zuordnet, seine Hash-Adresse, und speicher es dort ab. Wenn man das
Element dann in der Hash-Tabelle wiederfinden möchte, so berechnet man nach Vorschrift der HashFunktion die Hash-Adresse und findet es dann (idealerweise) in O(1) wieder.
Leider ist es im Allgemeinen nicht ganz so einfach: Wenn das Universum der möglichen Schlüssel
mehr Elemente enthält als die Hash-Tabelle, kann die Funktion nach dem Schubfachprinzip nicht
injektiv sein. Dann gibt es also zwei verschiedene Schlüssel k1 und k2 , so dass h(k1 ) = h(k2 ). Das
bezeichnet man beim Hashing als Kollision.
Der zweite Teil beim Hashing besteht also in der Kollisionsbehandlung, die entweder darin bestehen
kann, es zu erlauben, auf einer Hash-Adresse mehrere Elemente zu speichern (Chaining) oder sich bei
einer Kollision eine andere Hash-Adresse zu berechnen (Offene Addressierung).
Hashing ist ein gutes Beispiel für einen Kompromiss zwischen den beiden Gütefaktoren eines Algorithmus (Speicherplatzbedarf und Ausführungszeit). Wenn wir beliebig viel Zeit zur Verfügung hätten,
könnten wir einfach alle Elemente sequentiell durchsuchen; und hätten wir beliebig viel Speicherplatz,
könnten wir einfach den Schlüsselwert als Array-Index nehmen (beziehungsweise das Ergebnis einer
injektiven Hash-Funktion).
Wir wenden uns nun dem Problem zu, eine gute Hash-Funktion zu finden.
93
94
KAPITEL 8. HASHING
0
U
h(k1 )
h(k4 )
k3
h(k2 ) = h(k5 )
k4
k1
K
k2
k5
h(k3 )
m−1
Abbildung 8.1: Hier werden die Elemente mit den Schlüsseln k2 und k5 auf den gleichen Eintrag in
der Hash-Tabelle abgebildet.
8.1
Hash-Funktionen
Was zeichnet also eine gute Hash-Funktion aus? Sie sollte
• leicht und schnell berechenbar sein
• die Datensätze möglichst gleichmäßig verteilt auf die Hash-Tabelle abbilden
• deterministisch sein (sonst findet man seine Schlüssel ja nicht wieder)
• Kollisionen vermeiden
Es kann ziemlich schwierig sein, eine Hash-Funktion zu finden, die injektiv ist, selbst wenn die Zielmenge kleiner ist als die Ausgangsmenge.
Beispiel 8.1 Es gibt 4131 ≈ 1050 verschiedene Funktionen, die von einer 31-elementigen Menge in
eine 41-elementige Menge abbilden; injektiv sind davon aber nur 41 · 40 · 39·. . . ·11 = 41!/10! ≈ 1043 ,
also nur ungefähr jede 10millionste!
Beispiel 8.2 [Geburtstagsparadoxon] Die Frage beim Geburtstagsparadoxon ist: Wieviele Leute müssen
in einem Raum sein, damit die Wahrscheinlichkeit, dass zwei von ihnen am gleichen Tag Geburtstag
haben, höher als 21 ist? (Das Jahr hat dabei 365 Tage, wir berücksichtigen keine Schaltjahre.)
Das lässt sich auf folgende Art berechnen:
Sei m die Anzahl der möglichen Tage und k die Anzahl der Personen. Die Wahrscheinlichkeit q, dass
es keine Kollision gibt (also, dass alle Personen an verschiedenen Tagen Geburtstag haben), ist dann
q(m, k) =
m · (m − 1) · (m − 2) · (m − 3) · · · · (m − k + 1)
m!
=
m·m·m·m····m
(m − k)! · mk
95
8.1. HASH-FUNKTIONEN
Dann gilt
1
2
Es genügen also schon 23 Leute, damit die Wahrscheinlichkeit, dass zwei von ihnen am gleichen Tag
Geburtstag haben, größer als 12 ist.
q(365, 23) ≈ 0, 49270276567601459277458277 <
Beispiel 8.2 übertragen auf Hash-Funktionen bedeutet folgendes: Haben wir ein Universum von 23
Schlüsseln, eine Hash-Tabelle mit 365 Einträgen und eine zufällige Hash-Funktion, so ist die Wahrscheinlichkeit, dass zwei Schlüssel auf den gleichen Eintrag in der Hash-Tabelle abgebildet werden,
23
größer als 12 . Und dabei haben wir nur eine Auslastung der Hash-Tabelle von 365
≈ 6, 301%.
Wir nehmen im Folgenden an, dass die Schlüsselwerte natürliche Zahlen sind; andernfalls kann man
eine bijektive Funktion finden, die von den Schlüsselwerten in eine Teilmenge der natürlichen Zahlen
abbildet. (Wir nehmen also an, dass U höchstens abzählbar viele Elemente enthält.)
8.1.1
Divisionsmethode
Bei der Divisionsmethode wird für festes m ∈ N folgende Hash-Funktion verwendet:
h : U → {0, . . . , m − 1},
h(k) := k mod m
In diesem Fall sind einige Werte von m aber besser als andere. Wenn zum Beispiel m gerade ist, so
wird h(k) genau dann gerade sein, wenn k schon gerade war. Außerdem sollte man berücksichtigen,
dass wir im Binärsystem rechnen. Daher ist es ungünstig, wenn m eine Potenz von 2 ist (m = 2 p ), weil
dann bei der Berechnung des Hash-Werts von k nur die letzten p Bits berücksichtigt werden (natürlich
kann man diese Hash-Funktion verwenden, wenn man weiß, dass alle 1-0-Verteilungen in den letzten
p Bits gleich wahrscheinlich sind).
Es wird empfohlen, für m eine Primzahl zu verwenden, die keine der Zahlen ri ± j teilt, wobei i, j ∈ N
kleine Zahlen und r die Basis des Zahlensystems ist. Das ist im Allgemeinen eine gute Wahl.
8.1.2
Multiplikationsmethode
Sei 0 < A < 1 beliebig, aber fest. Dann benutzt man bei der Multiplikationsmethode folgende HashFunktion:
h : U → {0, . . . , m − 1},
h(k) := bm(kA mod 1)c = bm(kA − bkAc)c
Es wird also k mit einer Konstante A zwischen 0 und 1 multipliziert, die Vorkommastellen werden
abgeschnitten, das Ergebnis wird mit m multipliziert (der dabei entstehende Wert q ist ∈ R und es gilt
0 ≤ q ≤ m). Dann wird abgerundet und wir erhalten einen ganzzahligen Wert q0 mit 0 ≤ q0 ≤ m − 1.
Dabei ist die Wahl von m nicht so entscheidend wie bei der Divisionsmethode.
Nach [Knu98] führt dabei eine Wahl von
√
A ≈ ( 5 − 1)/2 = 0.6180339887 . . .
96
KAPITEL 8. HASHING
zur gleichmäßigsten Verteilung von allen Zahlen zwischen 0 und 1.
8.2
Kollisionsbehandlung
8.2.1
Chaining
Beim Chaining steht in der Hash-Tabelle an jeder Stelle eine Liste. Tritt nun beim Einfügen eine
Kollision auf, so werden beide Elemente in die Liste an dieser Stelle gehängt. Beim Suchen muss
dann — nachdem die Hash-Funktion die Stelle berechnet hat, an der das gesuchte Element zu finden
ist — die dortige Liste noch durchsucht werden.
0
U
k1
k4
k6
k2
k5
k7
k3
k4
k6
k1
K
k2
k7
k3
k5
m−1
Abbildung 8.2: Beim Chaining enthält jeder Eintrag der Hash-Tabelle eine Liste der Elemente, die
auf diese Stelle gehasht wurden.
Sei h die benutzte Hash-Funktion. Wir betrachten die drei Operationen, die wir durchführen wollen:
Einfügen Beim Einfügen können wir annehmen, dass das Element nicht bereits in der Hash-Tabelle
T vorhanden ist; falls es nötig sein sollte, das zu überprüfen, können wir vorher das Element
suchen. Die Einfügen-Methode hat also die Form
chainedHashInsert(T,x)
füge x am Kopf der Liste T[h(x.key)] ein
Die Worst-Case-Laufzeit für das Einfügen ist also O(1).
Suchen chainedHashSearch(T,k)
suche in der Liste T[h(k)] nach einem Element x mit x.key==k
Die Worst-Case-Laufzeit vom Suchen ist also von der Länge der Liste abhängig.
8.2. KOLLISIONSBEHANDLUNG
97
Löschen chainedHashDelete(T,x)
lösche x aus der Liste T[h(x.key)]
Wenn wir das Element bereits gesucht haben und eine doppelt verkettete Liste verwenden, so
können wir es nach der Suche in O(1) löschen.
Wir betrachten jetzt den Aufwand für das Suchen etwas genauer. Dazu geben wir erst einmal eine
Definition:
Sei T eine Hash-Tabelle mit m ∈ N Plätzen, die gerade N 3 n > 0 Elemente speichert. Dann definieren
wir den Auslastungsfaktor α der Tabelle als
n
α=
m
Weil beim Chaining auch mehr Elemente gespeichert werden können als die Hash-Tabelle Einträge
hat, kann hier α auch größer als 1 sein. Das Worst-Case-Verhalten von Hashing mit Chaining tritt
dann auf, wenn alle Elemente auf den gleichen Eintrag der Hash-Tabelle abgebildet werden. Dann hat
das Suchen die gleiche Laufzeit wie bei einer einfach verketteten Liste, also Θ(n). Wir wollen nun das
Durchschnittsverhalten von Hashing analysieren. Dazu treffen wir folgende Annahme:
Gleichverteilungsannahme: Die Wahrscheinlichkeit, dass ein Schlüssel k auf die Adresse i in der
Hash-Tabelle abgebildet wird, ist unabhängig von i stets gleich m1 und hängt nicht davon ab, welche
Elemente bereits in der Tabelle gespeichert sind.
Wir nehmen an, dass der Wert der Hash-Funktion in O(1) berechnet werden kann und die Suchdauer
daher von der Länge der Liste dominiert wird. Der Erwartungswert für die Länge der Liste ist unter
der Gleichverteilungsannahme offenbar gleich α.
Satz 8.1 Beim Hashing mit Chaining ist unter der Gleichverteilungsannahme der Aufwand für das
Suchen im Mittel O(1 + α).
Beweis: Wegen der Gleichverteilungsannahme wird Schlüssel k mit gleicher Wahrscheinlichkeit auf
jeden der Einträge gehasht. Wir können den Wert der Hash-Funktion in O(1) berechnen. Wir speichern die Elemente in der Hash-Tabelle in Listen. Diese haben eine mittlere Länge von α. Daher ist
der Aufwand für eine Suche im Mittel α und damit der Aufwand für das Suchen O(1 + α).
Korollar 8.1 Falls m proportional zu n ist, also n = O(m) gilt, ist der Aufwand für das Suchen im
Mittel konstant. Da das Einfügen und Löschen (nach dem Suchen) in O(1) gehen, können daher alle
drei Operationen im Mittel in konstanter Zeit ausgeführt werden.
8.2.2
Offene Adressierung
Bei der offenen Adressierung werden alle Elemente in der Hash-Tabelle selbst gespeichert, ohne dass
Listen verwendet werden. Wenn die Hash-Tabelle m Plätze hat, können dann natürlich auch nur maximal m Elemente gespeichert werden.
98
KAPITEL 8. HASHING
Die Kollisionsbehandlung erfolgt so, dass für jeden Schlüssel k auf eine bestimme Weise eine Sondierungssequenz zur Suche nach einer Ersatzadresse angegeben wird, die nacheinander abgearbeitet
wird, wenn das Element mit Schlüssel k eingefügt beziehungsweise gesucht werden soll.
Falls wir ein Element suchen, das nicht in der Tabelle enthalten ist, suchen wir dabei entweder die
ganze Tabelle durch oder können abbrechen, sobald wir in der Sondierungssequenz auf einen Eintrag
in der Hash-Tabelle stoßen, der leer ist (wäre das Element schon gespeichert, so wäre es ja an dieser
Stelle, weil die Sondierungssequenz für festes k ja jedesmal die gleiche ist).
Wir erweitern also die Hash-Funktion zu einer Funktion
h : U × {0, 1, . . . , m − 1} → {0, 1, . . . , m − 1}
wobei das zweite Argument die Anzahl der schon erfolgten Sondierungen (beginnend bei 0) sein soll.
Dabei muss gelten:
Permutationsbedingung
(h(k, 0), h(k, 1), . . . , h(k, m − 1)) ist eine Permutation von (0, 1, . . . , m − 1).
Folgende Methode fügt ein Element in eine Hash-Tabelle T ein. Dabei sei int h(int, int) die
Hash-Funktion und T die Hash-Tabelle, realsiert als Array von Integer Objekten.
boolean hashInsert(Integer[] T, int k) {
// returns true, if element is successfully inserted, else false
int i = 0;
do {
int j = h(k,i);
if (T[j] == NULL) {
T[j] = new Integer(k);
return true;
}
i++;
} while (i!=T.length);
return false;
}
Folgender Algorithmus sucht dann den Datensatz mit Schlüssel k:
int hashSearch(Integer[] T, int k) {
// returns index of hash table entry, if element is found, else -1
int i = 0;
do {
99
8.2. KOLLISIONSBEHANDLUNG
int j = h(k,i);
if (T[j].equals(k))
return j;
i++;
}
while (T[j]!=NULL && i!=m);
return -1;
}
Aus einer Hash-Tabelle zu löschen, die offene Adressierung verwendet, ist im Allgemeinen schwierig.
Wir können ein Element nicht einfach löschen, weil dann an dieser Stelle beim Suchen von anderen
Elementen die Sondierungssequenz abbrechen könnte und daher andere Elemente nicht mehr gefunden werden, obwohl sie noch in der Tabelle enthalten sind.
Eine mögliche Lösung besteht darin, einen Spezialwert GEL ÖSCHT zu definieren, den wir statt NULL
in die Hash-Tabelle schreiben, damit die Sondierungssequenz nicht abbricht. Man müsste dann HashInsert ein bisschen modifizieren, damit dieser Wert dann so behandelt wird, als wäre die Tabelle an
dieser Stelle leer.
Das Problem ist, dass beim Verwenden von GEL ÖSCHT die Suchzeit länger wird und nicht mehr nur
vom Auslastungsfaktor α abhängt. Daher wird, falls gelöscht werden muss, meistens Chaining als
Kollisionsbehandlung gewählt.
Wir betrachten jetzt drei verschiedene Hash Verfahren, die auf offener Adressierung basieren.
Lineare Sondierung / Linear Probing
Sei h0 : U → {0, 1, . . . , m − 1} eine gewöhnliche Hash-Funktion. Dann benutzt man beim Linear Probing folgende Hash-Funktion:
h(k, i) := h0 (k) + i mod m
Die davon generierte Sondierungssequenz hat folgende Form:
h0 (k), h0 (k) + 1, h0 (k) + 2, . . . , m − 1, 0, 1, . . . , h0 (k) − 1
Ein Problem beim Linear Probing besteht darin, dass sich leicht Ketten von schon belegten Feldern
bilden, weil die Wahrscheinlichkeit, dass ein Element an Stelle k eingefügt wird, wobei vor Stelle k
schon i belegte Felder sind, gleich i+1
m ist. Das bezeichnet man als Primäres Clustering. Dadurch steigt
die Durchschnittszeit für das Einfügen und Suchen stark an, wenn sich der Auslastungsfaktor α der 1
nähert.
Das Problem wird besonders drastisch, wenn als Hash-Funktion h0 dabei die Divisionsmethode verwendet wird und direkt aufeinanderfolgende Schlüssel {k, k + 1, k + 2, . . . } eingefügt werden, weil
diese dann auch auf Felder gehasht werden, die direkt aufeinanderfolgen.
100
KAPITEL 8. HASHING
h0 (k)
Abbildung 8.3: Lineare Sondierung
Beim Linear Probing gibt es noch eine Möglichkeit, das Löschen so unterzubringen, dass der Aufwand nicht anwächst, wenn man das Löschen geringfügig abändert (siehe [Knu98]). Beim folgenden
Verfahren ist das aber nicht mehr praktikabel:
Quadratische Sondierung / Quadratic Probing
Beim Quadratischen Sondieren wird eine Hash-Funktion der Form
h(k, i) := h0 (k) + c1 i + c2 i2 mod m
verwendet.
h0 (k) + 12
h0 (k) + 20
h0 (k) + 2
h0 (k) + 6
h0 (k)
Abbildung 8.4: Quadratische Sondierung mit c1 = c2 = 1
Dabei ist die Permutationsbedingung kompliziert zu erfüllen, denn sie hängt von c1 , c2 und m ab. Dieses Hashing-Verfahren vermeidet primäres Clustering, führt aber zu sogenanntem sekundärem Clus-
101
8.2. KOLLISIONSBEHANDLUNG
tering, denn wenn Schlüssel k1 , k2 den gleichen Hash-Wert haben (h(k1 , 0) = h(k2 , 0)), so haben sie
auch die gleiche Sondierungssequenz.
Doppelhash / Double Hashing
Beim Double-Hashing werden zwei Hash-Funktionen verwendet. Dies ist eine der besten Arten, Kollisionen durch offene Addressierung zu behandeln, weil die vom Double Hashing erzeugten Permutationen viele Charakteristika von zufälligen Permutationen besitzen. Die Hash-Funktion hat also die
Form
h(k, i) = (h1 (k) + ih2 (k)) mod m
wobei h1 und h2 einfache Hash-Funktionen sind. Die Verwendung der zweiten Hash-Funktion behebt
dabei das Problem des sekundären Clustering.
Satz 8.2 (Doppelhashing) Die Permutationsbedingung ist beim Doppelhashing genau dann erfüllt,
wenn die Länge m der Hash-Tabelle und h2 (k) für alle k relativ prim sind, also gilt:
∀k :
ggT (m, h2 (k)) = 1
Beispiel 8.3 Wir machen erst einmal plausibel, dass die Permutationsbedingung nicht erfüllt ist wenn
m und h2 (k) nicht relativ prim sind. Dazu geben wir ein Gegenbeispiel explizit an.
Seien also h2 (k) = 8, m = 12. Dann ist ggT (h2 (k), m) = 4 =: d. Sei h1 (k) = 1. Die Folge der Adressen
lautet dann 1, 9, 5, 1. Es werden dann also nur md = 12
4 = 3 Adressen in der Hash-Tabelle besucht.
Wir betrachten diesen Sachverhalt nun genauer.
Beweis: (von Satz 8.2)
Definiere h2 (k) =: w und sei ggT = (m, w) =: d ∈ N. Dann gibt es p, q ∈ N mit
p·d = w
(8.1)
q·d = m
(8.2)
und
Sei o.B.d.A. h1 (k) = 0. Die Folge der durchlaufenen Adressen hat dann die Form
8.1
8.2
0, w, 2w, 3w, . . . , q · w = q · p · d = q · d ·p = m · p
|{z} |{z}
w
(8.3)
m
Ein Vielfaches von m modulo m ist aber wieder 0. Für die Zykellänge `, also die Anzahl der Schritte,
bis man zum ersten Mal wieder am Ausgangspunkt angelangt ist, gilt also:
`≤q
Dann sind zwei Fälle zu unterscheiden:
102
KAPITEL 8. HASHING
(1) ` = q
Es werden also q Adressen besucht. Da nach Voraussetzung m = q · d gilt, folgt
d = 1 ⇔ q = m,
also die Aussage des Satzes.
(2) ` < q
Nach ` Sprüngen wird das erste Mal der Ausgangspunkt wieder besucht. Aber nach q Sprüngen wird
auch der Ausgangspunkt wieder besucht, also muss der Zykel der Länge ` mehrfach durchlaufen
worden sein und damit ist q ein Vielfaches von `. Es gilt also:
q = s · `,
s>1
(8.4)
Weil nach ` Sprüngen der Weite w wieder der Ausgangspunkt erreicht wird, ist also ` · w ein Vielfaches
von m. Es gilt also:
` · w = r · m,
r≥1
(8.5)
Also gilt
8.3
8.4
8.5
p·m = q·w = s·`·w = s·r·m
⇒ p = s·r
und
q = s·`
Es folgen
8.2
m = d ·q = d ·s·`
und
8.1
w = d · p = d ·s·r
Aber weil s > 1, ist d · s > d und damit teilt nicht nur d, sondern auch ds schon m und w und ist ein
größerer Teiler als d. Aber nach Voraussetzung war d der größte gemeinsame Teiler von m und w.
Widerspruch! Also tritt (2) nicht auf.
Analyse unter Gleichverteilungsannahme
Die Gleichverteilungsannahme bedeutet hier, dass die nächste Sondierung gleichverteilt unter den m
Adressen eine auswählt, also jede mit Wahrscheinlichkeit m1 .
Satz 8.3 Bei Auslastungsfaktor α =
1
gleich 1−α
.
Beispiel 8.4 α =
Mittel).
1
2
⇒
1
1−α
n
m
< 1 ist die erwartete Anzahl der Sondierungen beim Einfügen
= 2 Sondierungen (im Mittel), α = 0, 8 ⇒
1
1−α
= 5 Sondierungen (im
103
8.2. KOLLISIONSBEHANDLUNG
Der Beweis beruht auf einer allgemeinen wahrscheinlichkeitstheoretischen Aussage.
Satz 8.4 (Urnenmodell) Gegeben seien m Kugeln in einer Urne, davon sind w weiß und s schwarz,
w + s = m. Es werde aus der Urne gleichverteilt mit Zurücklegen gezogen. Dann ist die erwartete
Anzahl von Ziehungen bis zur Ziehung einer weißen Kugel m
w , also
E(# Ziehungen bis zur ersten weißen Kugel) =
m
.
w
Beispiel 8.5 Ein Spezialfall des Urnenmodells ist die mittlere Zahl von Würfeln eines Würfels bis zur
ersten 6. Dabei entspricht die 6 einer weißen Kugel und die Zahlen 1 bis 5 schwarzen Kugeln. Also
ist w = 1 und s = 5. Jede Zahl (Kugel) wird mit derselben Wahrscheinlichkeit von 16 gezogen und das
Urnenmodell ergibt
E(# Würfe, bis das erste mal eine 6 geworfen wird) =
6
= 6.
1
Entsprechend ergibt sich
E(# Würfe, bis das erste mal eine 1 oder eine 2 geworfen wird) =
6
= 3.
2
Bevor wir Satz 8.4 beweisen, rekapituliern wir kurz die Definition des Erwartungswertes für abzählbar
diskrete Zufallsvariable.
X bezeichne eine diskrete Zufallsgröße, die die Werte x1 , x2 , . . . , xn annehmen kann, wobei diese Werte mit den Wahrscheinlichkeiten p1 , p2 , . . . , pn auftreten. Der Erwartungswert (Mittelwert) von X ist
definiert als
n
E(X) := ∑ xi · pi
i=1
∞
Im abzählbar unendlichen Fall ist der Erwartungswert definiert als E(X) = ∑ xi · pi , falls diese Reihe
i=1
konvergiert.
Im Urnenmodell entspricht zw dem Ziehen einer weißen, zs dem Ziehen einer schwarzen Kugel und
die Ereignisse treten mit den Wahrscheinlichkeiten p, q ein, wobei
p + q = 1.
(8.6)
Sei X die Anzahl der Ziehungen, bis zw eintritt. X ist dann eine abzählbar unendlich diskrete Zufallsvariable mit den Werten x1 = 1, x2 = 2, . . . , xi = i, . . .. Diese Werte treten mit folgenden Wahrscheinlichkeiten auf:
∧
x1 = 1 = Folge zw
∧
x2 = 2 = Folge zs zw
..
.
xi = i
∧
mit Wahrscheinlichkeit: p
mit Wahrscheinlichkeit: q · p
= Folge zs zs . . . zs zw mit Wahrscheinlichkeit: qi−1 · p =: pi
| {z }
(i−1)−mal
104
KAPITEL 8. HASHING
Zur Kontrolle addieren wir die Wahrscheinlichkeiten noch einmal auf:
∞
∞
∞
i=1
i=1
i=0
1
1
∑ pi = ∑ qi−1 · p = p · ∑ qi = p · 1 − q = p · p = 1
8.6
Wir stellen nun die Verbindung zwischen dem Sondieren beim Doppelhash und dem Urnenmodell her.
Urnenmodell
Ziehen einer weißen Kugel
Ziehen einer schwarzen Kugel
Ziehen mit Zurücklegen
Hashing
Hash-Funktion wählt eine leere Stelle in der Hash-Tabelle
Hash-Funktion wählt eine besetzte Stelle in der Hash-Tabelle
blindes Wählen eines neuen Platzes
Dann folgt mit Satz 8.4:
E(# Sondierungen) ≤ E(# Sondierungen bei Blindwahl) =
m
1
1
=
,
n =
m−n 1− m
1−α
also der Beweis von Satz 8.3.
Wir müssen daher noch die wahrscheinlichkeitstheoretische Aussage in Satz 8.4 zeigen.
Beweis: (von Satz 8.4)
Dazu betrachten wir Folgen von Ziehungen, bis das erste Mal eine weiße Kugel gezogen wird:
1. Mal:
2. Mal:
3. Mal:
..
.
w
Wahrscheinlichkeit : p mit p = m
Wahrscheinlichkeit : q · p mit q =
Wahrscheinlichkeit : q2 · p
..
.
(w)
(s, w)
(s, s, w)
..
.
s
m
i-tes Mal: (s, s, . . . , s, w) Wahrscheinlichkeit : qi−1 · p
| {z }
(i−1)−mal
..
.
..
.
..
.
Der Erwartungswert für die Anzahl der Ziehungen ergibt sich dann aus der Definition, indem man
jeden Wert xi = i der Zufallsvariablen
X := # Ziehungen bis zur ersten weißen Kugel
mit der zugehörigen Wahrscheinlichkeit pi multipliziert und diese Produkte alle aufsummiert. Er ist
also
E(X) = 1 · p + 2 · qp + 3 · q2 p + · · · + i · qi−1 p + . . .
∞
=
∑ i · qi−1 · p
i=0
∞
= p · ∑ i · qi−1
i=1
| {z }
:=S
105
8.2. KOLLISIONSBEHANDLUNG
Wir dürfen die Potenzreihe S umordnen, weil sie absolut konvergent ist. Also ist
S = 1 · q0 + 2q1 + 3q2 + 4q3 + . . .
∞
= q0 + q1 + q2 + q3 + . . .
+q1 + q2 + q3 . . .
+q2 + q3 + . . .
=
i=0
∞
q · ∑ qi )
(→
i=0
∞
i
q2 · ∑ q )
(→
∞
+q3 + . . .
(→
∑ qi )
(→
i=0
q3 · ∑ qi )
∞
∞
∞
i=0
i=0
i=0
∞
i=0
∑ qi + q · ∑ qi + q2 · ∑ qi + . . .
= (1 + q + q2 + q3 + . . . ) ∑ qi
i=0
∞
=
∑ qi
i=0
=
=
=
∞
!
∑ qi
!
i=0
1
1
·
1−q 1−q
1 1
·
p p
1
p2
Also gilt für die erwartete Anzahl E(X) der Ziehungen bis zur ersten weißen Kugel
E(X)
=
=
w
p= m
=
p·S
1
p
m
.
w
Korollar 8.2 Die Anzahl der Sondierungen ist im Mittel also gleich
gehen Einfügen und Suchen in Hash-Tabellen im Mittel in O(1).
1
1−α
und damit konstant. Also
Man könnte sich nun die Frage stellen, warum wir nicht Bäume statt Hashtabellen verwenden und
auch dort den Aufwand im Mittel betrachten, da sich doch das Löschen bei Bäumen sehr viel einfacher
gestaltet, und wir den Vorteil der Sortierung gemäß Inorder-Durchlauf haben. Es gilt jedoch:
106
KAPITEL 8. HASHING
Satz 8.5 Der mittlere Aufwand für das Einfügen und Suchen ist bei Bäumen O(log n).
Beweis: Zum Beweis dieses Satzes betrachten wir das Suchen in einem Suchbaum mit n Datensätzen.
Der bestmögliche (weil höhenminimale) Baum für diese Suche ist ein voller Baum, also ein Baum,
bei dem alle Schichten bis auf die letzte voll sind (falls n = 2 p − 1 mit p ∈ N, so ist auch die letzte
voll). Betrachte dabei einen Baum T , der auf der untersten Schicht nur ein Element enthält, so dass
n−1
n=
∑ 2i + 1 = 2h − 1 + 1 = 2h
i=0
gilt. Wir analysieren das Suchen im Baum T unter der Gleichverteilungsannahme, in diesem Fall also
der Annahme, dass jeder Schlüssel mit der gleichen Wahrscheinlichkeit n1 gesucht wird.
Dann gilt:
E(# Anzahl Vergleiche zum Suchen) =
≥
=
=
=
∈
Summe der Vergleiche zum Suchen aller Knoten
n
h · #Knoten auf Schicht (h-1)
n
1
· h · 2h−1
n
1
n
· log n ·
n
2
1
log n
2
Ω(log n)
Hash-Tabellen haben also den Vorteil eines konstanten mittleren Aufwandes für die Basisoperationen
gegenüber Suchbäumen. Suchbäume haben dagegen einen Worst Case Aufwand von O(log n) für die
Basisoperationen und beinhalten per Inorder-Durchlauf eine Sortierung der Datensätze. Eshängt also
sehr von der Anwendung ab, ob Hash-Tabellen oder Suchbäume geeigneter sind.
8.3
Literaturhinweise
Hashtabellen werde in nahezu allen Büchern über Datenstrukturen und Algorithmen behandelt. Die hier gewählte
Darstellung lehnt sich an [CLRS01] an.
Kapitel 9
Schaltkreistheorie und
Rechnerarchitektur
9.1
Schaltfunktionen und Schaltnetze
Wir wollen nun die logischen Grundlagen der Rechnerarchitektur besprechen. In starker Vereinfachung ist ein Rechner eine Blackbox, die zu einer bestimmen Eingabe ein bestimmte Ausgabe liefert,
also abstrakt gesehen:
n
Blackbox
m
Es werden n Input-Bits in m Output-Bits transformiert. Ein Rechner entspricht also gewissermaßen
einer Schaltkreisfunktion F : Bn → Bm , B = {0, 1}.
Beispiel 9.1 Bei der Addition von zwei k-stelligen Binärzahlen benutzt man eine Schaltkreisfunktion
F : B2k → Bk+1 . Sie bildet also 2k-Bitvektoren auf k + 1-Bitvektoren ab.
F
(yk−1 , yk−2 , . . . , y1 , y0 , xk−1 , xk−2 , . . . , x1 , x0 ) → (zk , zk−1 , . . . , z1 , z0 )
|
{z
}
|
{z
}
∈B2k
Bk+1
Ein Beispiel mit k = 2 ist in Abbildung 9.1 dargestellt.
Beispiel 9.2 Beim Sortieren von 30 16-stelligen Binärzahlen kann man eine Schaltkreisfunktion S :
B480 → B480 verwenden.
107
108
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
y1
0
0
0
0
0
0
0
0
1
1
1
1
1
1
1
1
Input
y0 x1
0 0
0 0
0 1
0 1
1 0
1 0
1 1
1 1
0 0
0 0
0 1
0 1
1 0
1 0
1 1
1 1
x0
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
Output
z2 z1 z0
0 0 0
0 0 1
0 1 0
0 1 1
0 0 1
0 1 0
0 1 1
1 0 0
0 1 0
0 1 1
1 0 0
1 0 1
0 1 1
1 0 0
1 0 1
1 1 0
Interpretation
y+x = z
0+0=0
0+1=1
0+2=2
0+3=3
1+0=1
1+1=2
1+2=3
1+3=4
2+0=2
2+1=3
2+2=4
2+3=5
3+0=3
3+1=4
3+2=5
3+3=6
Abbildung 9.1: Addition von 2 k-stelligen Dualzahlen als Schaltkreisfunktion.
Beispiel 9.3 Primzahltest für 480-stellige Binärzahlen: p : B480 → B1 (einstelliger Output)
0 x ist keine Primzahl
p(x) :=
1 x ist eine Primzahl
Schaltkreisfunktionen mit m = 1 heißen Boolesche Funktionen und werden mit kleinen Buchstaben
bezeichnet, Schaltkreisfunktionen mit m > 1 werden mit Großbuchstaben bezeichnet.
Satz 9.1 Jede Schaltkreisfunktion F : Bn → Bm mit m > 1 kann äquivalent durch m Boolesche Funktionen f1 , f2 , . . . , fm mit fi : Bn → B beschrieben werden.
Beweis: Als Schaltkreisfunktion hat F die Form
F(x1 , x2 , . . . , xm ) = (y1 , y2 , . . . , ym )
Setze yi =: fi (x1 , x2 , . . . , xn ). Dann ist F durch
F(x1 , x2 , . . . , xn ) = ( f1 (x1 , x2 , . . . , xn ), f2 (x1 , x2 , . . . , xn ), . . . , fn (x1 , x2 , . . . , xn ))
vollständig dargestellt.
109
9.1. SCHALTFUNKTIONEN UND SCHALTNETZE
Zum Verstehen von Schaltkreisfunktionen reicht es also aus, Boolesche Funktionen zu verstehen. Wir
werden zeigen, dass jede Boolesche Schaltkreisfunktion bereits durch die Negation, das O DER und das
U ND darstellbar ist und es daher ausreicht, diese einfachen Schaltelemente in Hardware bereitzustellen
um damit jede Schaltkreisfunktion bauen zu können.
Wir betrachten zunächst Boolesche Funktionen mit wenigen Argumenten:
n=1:
f0 (x)
0
0
false
x
0
1
f1 (x)
0
1
Identität
f2 (x)
1
0
Negation
f6
0
1
1
0
f7
0
1
1
1
f8
1
0
0
0
f9
1
0
0
1
XOR
OR
NOR
x⇔y
f3 (x)
1
1
true
n=2:
x
0
0
1
1
y
0
1
0
1
f0
0
0
0
0
f1
0
0
0
1
FALSE
AND
f2
0
0
1
0
f3
0
0
1
1
f4
0
1
0
0
f5
0
1
0
1
f10
1
0
1
0
f11
1
0
1
1
f12
1
1
0
0
f13
1
1
0
1
f14
1
1
1
0
f15
1
1
1
1
x⇒y
NAND
TRUE
Die wichtigsten fünf dieser Operationen sind
• f1 : Konjunktion (∧, AND)
• f7 : Disjunktion (∨, OR)
• f13 : Implikation (⇒, IMPL)
• f9 : Äquivalenz (⇔, EQUIV)
• f6 : Antivalenz, ausschließende Disjunktion (=, XOR)
Außerdem haben eigene Namen
• f8 : Piercescher Pfeil (↓, NOR)
• f14 : Shefferscher Strich (↑, NAND)
Zur Vorbereitung des Darstellungssatzes benötigen wir erst einige Definitionen.
Zunächst werden wir Boolesche Ausdrücke im Folgenden in arithmetischer Schreibweise schreiben,
also x · y oder xy für x ∧ y, x + y für x ∨ y und x für ¬x. Damit bringen wir die arithmetischen Eigenschaften von B = {0, 1} zum Ausdruck, wobei 1 + 1 = 1 gilt.
110
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
Sei f : Bn → B eine Boolesche Funktion mit n Variablen x1 , . . . , xn . Belegungen dieser Variablen mit
Werten 0 und 1 entsprechen Bitvektoren der Länge n, also allen 2n Dualzahlen von 0 bis 2n − 1. Ist
i1 , . . . , in ein solcher Bitvektor, so heißt die Zahl i, deren Dualdarstellung gleich i1 , . . . , in ist, der Index
zu i1 , . . . , in .
Ein einschlägiger Index i von f ist ein Index, für den f die Dualdarstellung (i1 , . . . , in ) des Index auf
die 1 abbildet, also f (i1 , . . . , in ) = 1 gilt.
Beispiel 9.4 Sei f die durch Tabelle 9.1 definierte Boolesche Funktion. Die einschlägigen Indizes
dieser Funktion sind dann 1, 2 und 5.
Index i
0
1
2
3
4
5
6
7
x1
0
0
0
0
1
1
1
1
x2
0
0
1
1
0
0
1
1
x3
0
1
0
1
0
1
0
1
f (x1 , x2 , x3 )
0
1
1
0
0
1
0
0
Tabelle 9.1: Boolesche Funktion zu Beispiel 9.4
Sei i ein Index von f : Bn → B und (i1 , i2 , . . . , in ) seine Dualdarstellung. Dann heißt die Boolesche
Funktion
mi : Bn → B
mit
mi (x1 , x2 , . . . , xn ) := x1i1 · x2i2 · · · · · xnin
und
i
x jj
:=
x j wenn i j = 1
x j wenn i j = 0
j = 1, . . . , n
der i-te Minterm. Ein Beispiel: für n = 3 und i = 5 ergibt sich als Dualdarstellung (i1 , i2 , i3 ) von i der
Bitvektor (1, 0, 1) und damit als 5-ter Minterm die Boolesche Funktion m5 (x1 , x2 , x3 ) = x1 x2 x3 .
Satz 9.2 (Darstellungssatz für Boolesche Funktionen) Jede Boolesche Funktion f : Bn → B mit f 6=
0 ist eindeutig darstellbar als Summe der Minterme ihrer einschlägigen Indizes.
Beispiel 9.5 Nach Satz 9.2 gilt für Beispiel 9.4
f (x1 , x2 , x3 ) = m1 (x1 , x2 , x3 ) + m2 (x1 , x2 , x3 ) + m5 (x1 , x2 , x3 )
= x1 x2 x3 + x1 x2 x3 + x1 x2 x3
9.1. SCHALTFUNKTIONEN UND SCHALTNETZE
111
Wir überprüfen das an einigen Eingaben:
(x1 , x2 , x3 ) = (1, 0, 0)
f (1, 0, 0) = 1̄0̄0 + 1̄00̄ + 10̄0
= 010 + 001 + 110
= 0
(x1 , x2 , x3 ) = (0, 1, 0)
f (0, 1, 0) = 0̄1̄0 + 0̄10̄ + 01̄0
= 100 + 111 + 000
= 1
Beweis: Aus der Definition von Mintermen folgt
mi (x1 , . . . , xn ) = 1
Wir müssen zeigen, dass
⇔
(x1 , . . . , xn ) ist die Dualdarstellung (i1 , . . . , in ) von i.
(9.1)
f (x1 , x2 , . . . , xn ) = ∑ mi (x1 , x2 , . . . , xn )
i∈I
gilt, wobei I die Menge der einschlägigen Indizes bezeichnet. Wegen f 6= 0 ist I 6= 0/ und damit ∑i∈I mi
wohldefiniert. Wir zeigen die Gleichheit argumentweise für jeden Bitvektor, indem wir eine Fallunterscheidung durchführen. Sei dazu (k1 , . . . , kn ) ein konkreter Bitvektor und k der zugehörige Index.
Fall I k ist einschlägiger Index von f .
Dann ist f (k1 , . . . , kn ) = 1. Wegen (9.1) ist mk (k1 , . . . , kn ) = 1 und damit wegen k ∈ I auch ∑i∈I mi (k1 , . . . , kn ) =
1.
Fall II k ist kein einschlägiger Index von f .
Dann ist f (k1 , . . . , kn ) = 0 und wegen (9.1) mi (k1 , . . . , kn ) = 0 für alle i ∈ I. Daher ist auch ∑i∈I mk (k1 , . . . , kn ) =
0.
Bleibt noch die Eindeutigkeit der Darstellung zu zeigen. Wir nehmen dazu an, dass f auf zwei verschiedene Weisen als Summen von Mintermen darstellbar ist und führen dies zum Widerspruch.
Seien also
f = ∑ mi = ∑ m j
i∈I
j∈J
zwei Darstellungen von f als Summe von Mintermen mit I 6= J. O.B.d.A. existiere ein k ∈ I \ J (sonst
vertausche I und J, wegen f 6= 0 gibt es für einen der beiden Fälle ein solches k). Sei dann (k1 , . . . , kn )
die Dualdarstellung von k. Wegen (9.1) gilt dann mk (k1 , . . . , kn ) = 1 und daher ∑i∈I mi (k1 , . . . , kn ) = 1.
Weil aber k nicht in J enthalten ist, folgt aus (9.1), dass m j (k1 , . . . , kn ) = 0 für alle j ∈ J, und daher
112
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
auch ∑ j∈J m j (k1 , . . . , kn ) = 0, was ein Widerspruch zu ∑i∈I mi = ∑ j∈J m j ist.
Die in Satz 9.2 konstruierte Darstellung nennt man disjunktive Normalform (DNF) einer Booleschen
Funktion.
Korollar 9.1 Jede Boolesche Funktion ist schon durch die logischen Operationen AND, NOT und
darstellbar. Die Von Satz 9.2 nicht erfasste Ausnahme f = 0 kann zum Beispiel durch
OR
f = x1 x1
dargestellt werden.
Der Darstellungssatz bildet die Grundlage für den Bau von Schaltkreisen. Dazu müssen nur die drei
logischen Operationen AND, NOT und OR als Hardware-Bausteine vorliegen. Dies ist Aufgabe der
Elektrotechnik und wir werden darauf nicht weiter eingehen sondern annehmen, dass diese Bausteine
zur Verfügung stehen. Wir verwenden dafür die in Abbildung 9.2 dargestellten Symbole.
Abbildung 9.2: Inverter, UND-Gatter und ODER-Gatter (von links nach rechts).
Als Beispiel für die Anwendung des Darstellungssatzes betrachten wir die Konstruktion eines Addiernetzes auf Basis der stellenweise Addition von zwei k-stelligen Dualzahlen.
Beispiel 9.6 Halbaddierer
Ein Halbaddierer hat zwei Inputs und zwei Outputs. Er dient zur Addition der niedrigstwertigsten
Bits x0 und y0 von zwei Binärzahlen x, y und liefert als Output das niedrigstwertigste Bit z0 der
Ergebniszahl z und den Übertrag u1 für die 1-te Stelle. Seine Schaltkreisfunktion ist in Tabelle 9.2
dargestellt. Abbildung 9.3 gibt die Black Box“ Darstellung des Halbaddierers an.
”
x0
0
0
1
1
y0
0
1
0
1
u1
0
0
0
1
z0
0
1
1
0
Tabelle 9.2: Die Schaltkreisfunktion des Halbaddierers.
113
9.1. SCHALTFUNKTIONEN UND SCHALTNETZE
x0
y0
u1
HA
z0
Abbildung 9.3: Halbaddierer als Black Box
Der Halbaddierer besteht aus den Booleschen Funktionen u1 und z0 . Die Funktion u1 hat nur einen
einschlägigen Index, nämlich i = 3. Ihre disjunktive Normalform ist also
u1 (x0 , y0 ) = x0 y0
Die Funktion z0 dagegen hat zwei einschlägige Indizes, nämlich i = 1 und i = 2. Die Minterme dazu
sind x0 y0 und x0 y0 und damit ergibt sich als disjunktive Normalform
z0 (x0 , y0 ) = x0 y0 + x0 y0
Wir können mit diesen Bausteinen den Halbaddierer wie in Abbildung 9.4 dargestellt als Hardware
realisieren. Diese Darstellungsart bezeichnet man als Schaltnetz.
x0
z0
y0
u1
Abbildung 9.4: Halbaddierer als Schaltnetz.
Beispiel 9.7 Volladdierer
Nun betrachten wir den sogenannten Volladdierer, der zwei Ziffern xi , yi an Position i und den Übertrag
ui von der vorigen Position addiert und als Ergebnis die Ziffer zi der Summe sowie den Übertrag ui+1
an der nächsten Position ausgibt. Sein Verhalten ist in Tabelle 9.3 angegeben.
Für die beiden Booleschen Funktionen zi und ui+1 ergeben sich dann folgende Darstellungen als DNF:
zi (xi , yi , ui ) = xi yi ui + xi yi ui + xi yi ui + xi yi ui
114
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
xi
0
0
0
0
1
1
1
1
yi
0
0
1
1
0
0
1
1
ui
0
1
0
1
0
1
0
1
zi
0
1
1
0
1
0
0
1
ui+1
0
0
0
1
0
1
1
1
Tabelle 9.3: Die Schaltkreisfunktion des Volladdierers.
ui+1 (xi , yi , ui ) = xi yi ui + xi yi ui + xi yi ui + xi yi ui
Wir geben auch hierzu den entsprechenden Schaltkreis an, siehe Abbildung 9.5. Dabei verwenden wir
erweiterte Gatter, die eine erhöhte Zahl von Eingängen erlauben. Es ist klar, dass sich ein Gatter mit `
Eingängen aus ` − 1 Gattern mit je 2 Eingängen zusammensetzen lässt.
Die Art und Weise, in der der Volladdierer in Abbildung 9.5 konstruiert wird, ist nicht die einzige
mögliche Art. Es können durchaus auch verschiedene Varianten von Innenelektronik“ dasselbe Input”
Output-Verhalten haben und damit die gleiche Schaltkreisfunktion realisieren.
Eine andere Möglichkeit, den Volladdierer zu konstruieren, besteht in der Verwendung des schon
konstruierten Halbaddierers als Baustein wie in Abbildung 9.6.
Mit Hilfe der nun bekannten Halb- und Volladdierer konstruieren wir jetzt ein sogenanntes asynchrones Paralleladdierwerk. Es heißt asynchron, weil der Schaltkreis nicht getaktet wird und es heißt
Paralleladdierwerk, weil alle Inputbits nicht sequentiell, sondern parallel zur Verfügung stehen. Die
Realisierung als Schaltnetz ist in Abbildung 9.7 angegeben.
Die Zeit bis zum Vorliegen des gesamten Ergebnisses hängt von der Signallaufzeit ab. Diese ist für
zk und zk−1 sehr lang, da alle Zwischenüberträge uk−1 , uk−2 , . . . , u2 , u1 vorher berechnet werden
müssen. Dies funktioniert aber nur sequentiell. Bei Binärzahlen der Länge n haben wir also einen
Schaltungsweg der Länge n. Für die Praxis ist das nicht tauglich.
Eine Beschleunigung ist möglich durch sogenannte Carry-Look-Ahead“-Techniken. Das ist eine Zu”
satzschaltung, die den Übertrag (zum Beispiel u5 ) bereits sofort aus y4 , x4 , y3 , x3 , y2 , x2 , y1 , x1 , y0 und
x0 berechnet, in diesem Fall also eine Funktion
u5 : B10 → B1
Dieses Prinzip kann dann rekursiv angewendet werden.
115
9.2. VEREINFACHUNG VON SCHALTNETZEN
yi
zi
xi
ui+1
ui
Abbildung 9.5: Schaltnetz des Volladdierers.
9.2
Vereinfachung von Schaltnetzen
Wie wir beim Volladdierer gesehen haben, gibt es verschiedene Möglichkeiten, die gleiche Schaltkreisfunktion in Hardware zu realisieren, so dass der dabei entstehende Schaltkreis das gleiche Verhalten zeigt. Die Frage, der wir uns nun zuwenden wollen, ist: Was ist eine gute Art, eine Schaltkreisfunktion in Hardware zu realisieren? Wie können die vorhandenen Freiheitsgrade optimal ausgenutzt
werden?
Eine Antwort auf diese Frage ist die Miniaturisierung: Wenn wir nur wenige Gatter verwenden,
benötigen wir weniger Fläche und damit werden die Schaltkreise kleiner und die Signallaufzeiten
kürzer.
Wir wollen zwei Verfahren betrachten:
• Verfahren von Karnaugh
• Verfahren von Quine-McCluskey
116
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
u
yi
ui+1
HA
z
xi
u
HA
z
ui
zi
Abbildung 9.6: Der Volladdierer mit Halbaddierern konstruiert.
yk−1
xk−1
y2
...
VA
uk−1
zk = uk zk−1
x2
y1
x1
VA
VA
z2
z1
y0
x0
HA
u3
Abbildung 9.7: Schaltnetz des asynchronen Paralleladdierwerks.
z0
117
9.2. VEREINFACHUNG VON SCHALTNETZEN
Die beiden Verfahren beruhen logisch gesehen auf der Resolution, also der Tatsache, dass
x+x = 1
gilt.
Beispiel 9.8
f (x1 , x2 , x3 ) = x1 x2 x3 + x1 x2 x3
= (x1 + x1 )x2 x3
| {z }
1
= x2 x3
Die dabei erreichte Vereinfachung veranschaulichen wir durch die Schaltnetze. Ohne die Vereinfachung würden wir das Schaltnetz in Abbildung 9.8 benutzen, mit Vereinfachung das aus Abbildung 9.9.
x1
f (x1 , x2 , x3 )
x2
x3
Abbildung 9.8: Nicht vereinfachtes Schaltnetz.
9.2.1
Das Verfahren von Karnaugh
Das Verfahren von Karnaugh ist nur für Boolesche Funktionen f : Bn → B1 mit wenigen Argumenten
geeignet (n ≤ 4), illustriert aber gut das Prinzip der Resolution.
Für n = 3 benutzt man das Diagramm aus Abbildung 9.10. Zeilen- beziehungsweise Spaltennachbarn
unterscheiden sich dabei zyklisch um genau ein Bit in ihrer Variablenbelegung. Für n = 4 nutzt man
ein entsprechendes Diagramm mit 4 Spalten und Zeilen.
Bei allen einschlägigen Indizes schreibt man dann eine 1 in das Karnaugh-Diagramm. Ein Beispiel
einer Funktion für n = 4 ist in Abbildung 9.11 angegeben. Zum Beispiel ist 1011 ein einschlägiger
Index und x1 x2 x3 x4 der dazugehörige Minterm. Sein Nachbar 1111 ist ebenfalls einschlägiger Index
118
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
x1
x2
f (x1 , x2 , x3 )
x3
Abbildung 9.9: Vereinfachtes Schaltnetz.
x1 x2
x3
00
01
11
10
0
1
Abbildung 9.10: Anordnung der Variablen im Karnaugh Diagramm für n = 3.
mit Minterm x1 x2 x3 x4 . Offenbar kann auf im Karnaugh-Diagramm benachbarte Einsen wegen der
speziellen Anordnung der Variablen Resolution angewendet werden. Hier wäre
x1 x2 x3 x4 + x1 x2 x3 x4 = x1 x3 x4
Die Resolution kann dabei sowohl in Spalten als auch in Zeilen angewendet werden. Es würde zum
Beispiel auch gelten:
x1 x2 x3 x4 + x1 x2 x3 x4 = x1 x2 x4
Produktterme dürfen wegen der Regel x + x = x in der Resolution auch mehrfach verwendet werden:
f (x1 , x2 , x3 , x4 ) = x1 x3 x4 + x1 x2 x4 + x1 x2 x4
= x1 x3 x4 + x2 x4
Wir geben noch ein Beispiel für eine weitreichende mehrfache Resolution an.
g(x1 , x2 , x3 , x4 ) = x1 x2 x3 x4 + x1 x2 x3 x4 + x1 x2 x3 x4 + x1 x2 x3 x4 + x1 x2 x3 x4
= x2 x3 x4 + x2 x3 x4 + x1 x2 x4 + x1 x3 x4
= x2 x4 + x1 x2 x4 + x1 x3 x4
119
9.2. VEREINFACHUNG VON SCHALTNETZEN
x1 x2
x3 x4
00
01
11
10
00
01
1
11
1
1
1
1
10
Abbildung 9.11: Karnaugh Diagramm einer Booleschen Funktion für n = 4.
Das Karnaugh-Verfahren lässt sich auf größere Blöcke erweitern. Betrachten wir zum Beispiel das
partielle Karnaugh Diagramm in Abbildung 9.12. Wir können es mit Resolution vereinfachen:
f (x1 , x2 , x3 , x4 ) = x1 x2 x3 x4 + x1 x2 x3 x4 + x1 x2 x3 x4 + x1 x2 x3 x4
= x1 x3 x4 + x1 x3 x4
= x1 x3
x1 x2
x3 x4
00
01
00
1
1
01
1
1
Abbildung 9.12: Blöcke im Karnaugh Diagramm.
Allgemein müssen die Blöcke als Seitenlängen 2er-Potenzen haben. Das allgemeine Vorgehen lautet
also:
Wähle möglichst große Blöcke von 2k · 2l , k, l ∈ N, die nur aus Einsen bestehen und deren Vereinigung alle Einsen enthält, und schreibe die zugehörigen Produktterme auf. Die zugehörige Boolesche
Funktion ergibt sich dann als Summe dieser Produktterme,
Die Funktion f in Abbildung 9.11 lässt sich mit einem 4×4 Block und einem 2×1 Block überdecken“
”
und es folgt f (x1 , . . . , x4 ) = x2 x4 + x1 x3 x4 .
120
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
Manchmal werden nicht alle Eingabe-Bitvektoren in der Booleschen Funktion benötigt und man kann
dann frei wählen, ob man für diese bitvektoren eine 1 oder eine 0 als Ergebnis haben möchte (die
sogenannten don’t-cares“). Dadurch ist unter Umständen eine bessere Vereinfachung möglich. Wir
”
erläutern dies an einem Beispiel.
Beispiel 9.9 Wir wollen eine Schaltkreisfunktion für die folgende Funktion konstruieren:
f : {0, 1, 2, . . . , 9} → {0, 1},
f (x) :=
1 falls x ∈ {1, 5, 8, 9}
0 sonst
Wir modellieren diese Funktion als Boolesche Funktion, wobei wir die Eingaben auf natürliche Weise
durch ihre Bitvektoren Kodieren: Die Schaltkreisfunktion bezeichnen wir ebenfalls mit f , gesucht ist
also f : B4 → B1 , deren Output nur für folgende Bitvektoren
Eingabe
0
1
2
3
4
5
6
7
8
9
Bitvektor
0000
0001
0010
0011
0100
0101
0110
0111
1000
1001
Der Rest der möglichen Inputs wird nicht benötigt. Das zugehörige Karnaugh-Diagramm ist in Abbildung 9.13 angegeben, wobei das Ergebnis für nicht benötigte Inputs mit einem D für don’t-care“
”
gekennzeichnet ist. Indem wir alle don’t-cares auf 1 setzen, sehen wir, dass bereits folgende Schaltfunktion ausreicht:
f (x1 , x2 , x3 , x4 ) = x1 + x3 x4
9.2.2
Das Verfahren von Quine und McCluskey
Eine Boolesche Funktion f : Bn → B liegt in disjunktiver Form vor, wenn f als Summe von Termen
k
f = ∑ Mi
i=1
dargestellt ist. Ein Term Mi ist ein Produkt der Form
`
∏ xj
j=1
mit xα ∈ {x, x}.
αj
`
,
`≥1
121
9.2. VEREINFACHUNG VON SCHALTNETZEN
x1 x2
x3 x4
00
00
01
1
11
01
1
10
D
1
D
1
11
D
D
10
D
D
Abbildung 9.13: Karnaugh Diagramm mit Don’t Cares
Beispiel 9.10
f : B4 → B,
f := x1 + x3 x4
liegt in disjunktiver Form vor.
Ein Spezialfall der disjunktiven Form ist die sogenannte disjunktive Normalform; bei ihr sind alle
Terme Minterme.
Zur Bewertung einer Miniaturisierung führen wir jetzt ein Kostenmaß ein. Inverter betrachten wir als
kostenlos. Gatter kosten eine Einheit und haben nur zwei Eingänge.
Den Kosten einer Booleschen Funktion in disjunktiver Normalform entspricht die Anzahl der benötigten
Gatter (mit zwei Eingängen) zum Bau eines Schaltkreises auf Basis der disjunktiven Normalform. Die
Boolesche Funktion f : B4 → B, die wir mit dem erweiterten Verfahren von Karnaugh vereinfacht
haben, hat in der Darstellung als Summe von 4 Mintermen Kosten 15 und in der vereinfachten Form
f (x1 , x2 , x3 , x4 ) = x1 x3 x4 nur noch Kosten 2.
Definition (Vereinfachungsproblem für Boolesche Funktionen) Bestimme zu einer gegebenen Booleschen Funktion f : Bn → B, die als Tabelle oder in disjunktiver Normalform gegeben ist, eine Darstellung in disjunktiver Form mit minimalen Kosten.
Bei der Lösung dieses Problems spielt der Begriff des Implikanten eine große Rolle, den wir nun
einführen:
Sei f : Bn → B eine Boolesche Funktion. Ein Term M heißt Implikant von f (in Zeichen (M ≤ f ), wenn
gilt:
M(x1 , x2 , . . . , xn ) = 1 ⇒ f (x1 , x2 , . . . , xn ) = 1
Ein Implikant M von f heißt Primimplikant, falls keine echte Verkürzung (das heißt Streichung eines
xi bzw. eines xi ) von M Implikant von f ist.
122
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
Beispiel 9.11 Wir betrachten die Boolesche Funktion f (x1 , x2 , x3 ) = x1 x2 x3 + x1 x2 x3 + x1 x2 x3 .
x1 x2 x3 ist ein Implikant von f , denn aus x1 x2 x3 = 1 folgt f (x1 , x2 , x3 ) = 1. x1 x2 x3 ist jedoch kein
Primimplikant von f , denn wegen der Resolution hat f die Darstellung f (x1 , x2 , x3 ) = x1 x3 + x1 x2 und
daher ist x1 x3 ein Implikant von f und eine echte Verkürzung von x1 x2 x3 .
x1 x3 ist ein Implikant von f , denn f lässt sich als f (x1 , x2 , x3 ) = x1 x3 + x1 x2 darstellen. Weil die
einzigen möglichen echten Verkürzungen x1 und x3 keine Implikanten von f sind, ist x1 x3 ein Primimplikant von f .
x1 x2 x3 ist ein Implikant, jedoch kein Primimplikant, denn die echte Verkürzung x1 x2 ist ein Implikant
von f .
Satz 9.3 Sei f : Bn → B eine Boolesche Funktion ( f 6≡ 0). Ist d = M1 + M2 + · · · + Mk eine disjunktive
Darstellung von f mit minimalen Kosten, so ist jeder Term Mi ein Primimplikant von f .
Beweis: Zum Beweis nehmen wir das Gegenteil an, sei also o.B.d.A. M1 kein Primimplikant, aber
M1 + M2 · · · + Mk eine kostenminimale Darstellung der Booleschen Funktion f . Weil M1 kein Primimplikant ist, hat M1 eine echte Verkürzung M 1 , die Implikant von f ist. Damit hat f eine Darstellung
d = M 1 + M2 + · · · + Mk
Aber dann sind die Kosten von d geringer als die von d, ein Widerspruch zur Annahme.
Das Verfahren von Quine-McCluskey arbeitet wie folgt:
1. Bestimmung aller Primimplikanten von f
2. Auswahl einer kostenminimalen Untermenge, die schon f darstellt
Für Schritt 1 können wir Resolution systematisch verwenden, Teil 2 entspricht dem (weiter unten
behandelten) Überdeckungsproblem.
Zu Schritt 1: Schritt 1 wird wie folgt durchgeführt:
• Teile die Minterme in Gruppen mit gleicher Anzahl von Negationen.
• Wende Resolution auf benachbarte Gruppen an.
• Iteriere, bis keine Verkürzung mehr möglich ist.
• Die nicht verkürzbaren Terme sind die Primimplikanten (wird unten bewiesen).
Abbildung 9.14 illustriert diese Vorgehensweise.
Zu Schritt 2: Die Auswahl der Primimplikanten erfolgt nach folgenden Kriterien:
123
9.2. VEREINFACHUNG VON SCHALTNETZEN
Gruppen
1
2
3
4
gegeben
x1 x2 x3 x4
x1 x2 x3 x4
x1 x2 x3 x4
x 1 x2 x3 x 4
x1 x2 x3 x4
x 1 x2 x 3 x 4
x1 x2 x3 x4
Runde 1
x1 x2 x4
x1 x2 x3
x2 x3 x4
x1 x2 x4
x2 x3 x4
x1 x3 x4
x2 x 4
Abbildung 9.14: Ermittlung der Primimplikanten nach Quine-McCluskey. Die Terme in den Kästen
sind die Primimplikanten.
• Bestimme eine Auswahl von Primimplikanten, so dass jeder Minterm durch einen Primimplikanten verkürzt wird (aber nicht unbedingt echt verkürzt).
• Treffe die Auswahl so, dass die Kosten minimal werden.
Dazu erstellen wir eine Tabelle, bei der die Spalten den Mintermen und die Zeilen den Primimplikanten entsprechen. Ist dann ein Primimplikant eine Verkürzung eines Minterms, so schreiben wir an die
entsprechende Stelle der Tabelle eine 1, ansonsten eine 0. Abbildung 9.15 gibt ein Beispiel. Gesucht
ist nun eine Auswahl von Zeilen, also Primimplikanten, so dass die zugehörigen Einsen alle Spalten,
also Minterme, überdecken. Dies ist ein als algorithmisch schwierig bekanntes allgemeines Problem,
das sogenannte Überdeckungsproblem. Doch dazu später mehr.
x1 x 2 x3 x4
x1 x2 x3
x1 x3 x4
x2 x 4
x1 x2 x3 x4
1
x1 x2 x3 x4
x1 x2 x3 x4
x1 x2 x3 x4
1
x1 x2 x3 x4
x 1 x2 x 3 x 4
x
1
1
1
1
1
1
1
Abbildung 9.15: Die Tabelle für das Überdeckungsproblem zum Beispiel aus Abbildung 9.14.
In Abbildung 9.15 gibt es nur eine mögliche Auswahl von Primimplikanten (nämlich alle). Im Allgemeinen sind jedoch mehrere Auswahlen von Mengen von Primimplikanten möglich. Ein Beispiel
dazu im Karnaugh-Diagramm ist in Abbildung 9.16 angegeben.
Wir zeigen nun, dass die fortgesetzte Resolution in Schritt 1 tatsächlich alle Primimplikanten ergibt.
Satz 9.4 Seien M1 , M2 , . . . , Mk die Terme, die durch fortgesetzte Resolution, bis keine echte Verkürzung
mehr möglich ist, aus der disjunktiven Normalform von f entstehen (dabei ist Mi 6= M j für alle i, j).
Dann sind M1 , . . . , Mk genau die Primimplikanten von f .
Beweis: Wir müssen zeigen:
124
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
Abbildung 9.16: Ein Überdeckungsproblem mit zwei verschiedenen Lösungen (aber gleichen Kosten).
1. Jeder Term Mi ist Primimplikant von f .
2. Es gibt keine weiteren Primimplikanten von f .
Zu 1: Weil jedes Mi durch Resolution entsteht, ist jedes Mi Implikant von f . Wir nehmen nun an,
dass Mi ein Implikant, aber kein Primimplikant ist. Dann gibt es eine echte Verkürzung Mi0 von Mi .
Diese ist gekennzeichnet durch den Wegfall eines x j beziehungsweise eines x j . Weil Mi Implikant
war, ist Mi0 ebenfalls Implikant. Betrachten wir nun folgenden Fall: Das x j beziehungsweise x j , das in
Mi , aber nicht in Mi0 vorkommt, sei so mit einem Wert belegt, dass Mi = 0 wird. Belege alle anderen
Variablen mit Werten so, dass Mi0 = 1 wird und gleichzeitig alle anderen Terme M j = 0 werden. (Das ist
möglich, da sich die anderen Terme nach Voraussetzung von Mi unterscheiden). Sei y = (y1 , y2 , . . . , yn )
die entstehende Belegung der xi mit 0 bzw. 1. Dann gilt
Mi0 (y) = 1,
M` (y) = 0, ` = 1, . . . , k,
und es folgt
k
f (y) =
∑ Mi (y) = 0.
`=1
Aber nach Voraussetzung war Mi0 Implikant von f , so dass aus Mi0 (y) = 1 auch f (y) = 1 folgt. Dies ist
ein Widerspruch.
Zu 2: Wir nehmen also an, dass noch ein weiterer Primimplikant M von f existiert. Wir betrachten in
der disjunktiven Normalform alle Minterme mi , für die M eine Verkürzung ist. Sei I die Indexmenge
dieser i. Dann können wir aus der Summe über alle diese mi , i ∈ I den Term M ausklammern:
∑ mi = M(∑ m0i )
i∈I
i∈I
125
9.2. VEREINFACHUNG VON SCHALTNETZEN
Falls nun ∑i∈I m0i = 1 gilt, so ist nichts mehr zu zeigen, denn dann entsteht M durch Resolution und
wir sind fertig, denn die Terme, die durch Resolution entstehen, sind genau die Terme, die wir im
Verfahren von Quine-McCluskey erhalten. Sei also ∑i∈I m0i 6= 1.
Dann gibt es eine Belegung y0 der Variablen in ∑i∈I m0i , die den Wert 0 ergibt. Wir können die restlichen
Variablen so belegen, dass M(y) = 1 gilt und dass mi (y) = 0 für alle i 6∈ I, i einschlägiger Index von f ,
erfüllt ist. Es folgt f (y) = 0, aber M(y) = 1. Dies ist ein Widerspruch dazu, dass M ein Implikant von
f ist.
9.2.3
Das Überdeckungsproblem
Das Überdeckungsproblem ist von übergeordneter Bedeutung und tritt an vielen Stellen in der angewandten Mathematik auf. Zum Beispiel bei der Zuordnung von Mitarbeitern i (Zeilen der Tabelle) zu
Aufgaben j (Spalten der Tabelle), wobei eine 1 bedeutet, dass der Mitarbeiter i die Aufgabe j bearbeiten kann. Zusätzlich kann man eine Gewichtung wi j einführen, die angibt, wie gut Mitarbeiter i
Aufgabe j bearbeiten kann. Das Überdeckungsproblem besteht in der ungewichteten Version darin,
eine Auswahl von Mitarbeitern zu finden, so dass alle Aufgaben bearbeitet werden. In der gewichteten
Version möchte man zusätzlich die Qualität maximieren, also die Summe aller Gewichte wi j der einer
Aufgabe zugeordneten Mitarbeiter möglichst groß machen.
Das Überdeckungsproblem ist fundamental schwerer als alle Probleme, die wir bisher in der CoMa
behandelt haben. Alle bisherigen Aufgaben waren effizient (lies: in polynomialer Zeit) lösbar. Zum
Beispiel:
Optimaler Präfixcode
Kürzeste Wege
Optimaler statischer Suchbaum
Sortieren
..
.
O(n log n) ≤ O(n2 )
O(n4 )
O(n3 )
O(n log n) ≤ O(n2 )
..
.
Die Klasse der Probleme, für die ein solcher polynomialer Algorithmus existiert, bezeichnet man mit
P. Für das Überdeckungsproblem ist offen, ob es einen solchen Algorithmus gibt. Nach der Klassifizierung der Schwierigkeit von Problemen gehört das Überdeckungsproblem zu der Klasse NP besonders
schwieriger Probleme (das wird in den Vorlesungen ADM I oder Effiziente Algorithmen präzisiert).
Abbildung 9.17 gibt eine Übersicht über einige Komplexitätsklassen.
Es ist ein offenes Problem, ob P = NP oder P 6= NP gilt. Würde man einen effizienten Algorithmus
für das Überdeckungsproblem finden, würde das P = NP implizieren. Für die Lösung dieses Problems
hat das Clay Institute of Mathematics 1.000.000$ ausgeschrieben.
126
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
Universum aller Probleme
Halteproblem
berechenbare Probleme
NP
P
exponentielle Laufzeit
effizient lösbar (zum Beispiel Kürzeste-Wege-Problem)
Abbildung 9.17: Komplexitätsklassen.
9.3
Schaltungen mit Delays
Wir haben bisher nur azyklische Schaltwerke betrachtet und haben keine Rückkopplung zugelassen.
Das schränkt die Konstruktionsmöglichkeiten aber sehr ein und wir möchten jetzt dazu übergehen, in
unseren Schaltungen bereits berechnete Ergebnisse erneut als Input zu verwenden.
Beispiel 9.12 (Ringzähler) (siehe Übung)
F(xn−1 , xn−2 , . . . , x0 ) = (
|
{z
}
Binärdarstellung von x
y , y , . . . , y0
|n−1 n−2
{z
}
)
Binärdarstellung von x+1 mod z
Einen Ringzähler benutzt man dazu, Zeitschritte im Rechner zu zählen. Die herkömmlich Realisierung
als Schaltfunktion ist zwar möglich, aber nicht sinnvoll, weil der Ringzähler die Zeitschritte ja zyklisch
zu zählen hat. Dafür ist es offensichtlich nötig, die Ausgabe des Zählers als neue Eingabe aufzufassen.
Eine solche Rückkopplung ist jedoch nicht so ohne Weiteres möglich und führt zu undefiniertem
Outputverhalten, wie die Flimmerschaltung in Abbildung 9.18 zeigt.
Zur Realisierung der Rückkopplung benötigt man ein neues Schaltelement, das sogenannte Delay.
Dieses ist in der Lage, ein Bit zu speichern und gemäß eines äußeren, getakteten Signals auszugeben.
Es ist schematisch in Abbildung 9.19 dargestellt.
Bei einem Delay werden zwei Phasen unterschieden:
127
9.3. SCHALTUNGEN MIT DELAYS
0/1
0
1/0
Abbildung 9.18: Flimmerschaltung. Der Output flimmert“ zwischen 0 und 1.
”
xi
V
S
yi
Fan-Out
Takt
Abbildung 9.19: Delay mit Fanout.
• Arbeitsphase: In der Rechenphase wird der Inhalt von S als Signal yi abgegeben und das Signal
xi in V abgelegt.
• Setzphase: In der Setzphase wird durch einen Taktgeber die Sperre zwischen V und S aufgehoben und dadurch der Inhalt von S durch den Inhalt von V ersetzt. Die Setzphase erfolgt zentral
gesteuert durch Signalleitungen für alle Delays gleichzeitig; die Zeit zwischen zwei Signalen
wird die Taktzeit genannt.
Fan-Out bedeutet dabei, dass der Ausgang an mehrere Schaltelemente gleichzeitig weitergereicht
wird.
Delays sind auf elektronischer Ebene durch sogenannte Latches oder Flipflops realisierbar, uns interessiert aber nur die logische Seite.
Wir können die Flimmerschaltung nun so modifizieren, dass sie abwechselnd 0 und 1 pro Takt ausgibt,
siehe Abbildung 9.20.
Ein Delay kann so aber nur ein Bit speichern. Begrifflich fasst man mehrere Delays, genauer 2erPotenzen von Delays, zu einem sogenannten Register zusammen, siehe Abblildung 9.21.
Inhalte von Registern werden als Oktal- oder Hexadezimalziffern dargestellt. Der Inhalt entspricht
dabei dem Wort im Register.
Operationen im Rechner erfordern i. A. mehrere Takte. Um den richtigen Takt für das Ablesen des
Ergebnisses einer mehrtaktigen Operation festzustellen, wird ein Ringzähler benutzt.
128
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
0
...
0/1
Abbildung 9.20: Flimmerschaltung mit Delay.
...
...
...
...
Wortlänge
Abbildung 9.21: Register.
9.3.1
Addierwerke
Wir haben schon Addierwerke kennengelernt, die in der Lage sind, für Binärzahlen einer festen Stellenzahl n zu addieren. Dabei haben wir nicht berücksichtigt, woher die Summanden kommen und
wohin das Ergebnis dann geht. Darum wollen wir uns jetzt kümmern. Dazu treffen wir folgende Vereinbarung: Ein Addierwerk soll zwei Register enthalten, einen Akkumulator, der am Beginn den einen
Summanden enthält, und einen Puffer, der den anderen Summanden enthält. Das Ergebnis der Rechnung soll dann wieder im Akkumulator stehen.
Wir kennen schon das asynchrone (ohne Takt arbeitende) Paralleladdierwerk, das nach der gerade
getroffenen Vereinbarung die in Abbildung 9.22 angegebene Form hat.
Wir erweitern dieses Addierwerk jetzt mit einem Delay, das den Übertrag aufnimmt (bei Addition von
zwei n-stelligen Binärzahlen kann eine n + 1-stellige Binärzahl entstehen). Unser synchrones, also
getaktetes, 4-Bit-Addierwerk hat die in Abbildung 9.23 angegebene Form. Die Schaltung zeigt, dass
einige Schaltelemente mehrere Inputs haben (Fan-In), die je nach Takt Input von außen oder Zwischenergebnis einer internen Rechnung sein können. Wir gehen später darauf ein, wie dieses Problem
gelöst wird.
Ein Nachteil dieses Synchronaddierwerks besteht darin, dass die Signallaufzeiten vergleichsweise lang
sind, da die Volladdierer auf den Übertrag der vorigen Stufe“ warten müssen. Wir müssen also ent”
weder lange Taktzeiten verwenden oder mehrere Takte lang auf das Ergebnis warten, also eine Zusatzschaltung mit Ringzähler verwenden.
129
9.3. SCHALTUNGEN MIT DELAYS
Akkumulator (1. Input)
...
(in Register)
Addierwerk
...
Puffer (2. Input)
(in Register)
Abbildung 9.22: Asynchrones Paralleladdierwerk.
x3
x2
x1
x0
VA
VA
VA
HA
y3
y2
y1
y0
Akkumulator
Übertrag an
Stelle n + 1
Puffer
Abbildung 9.23: Synchrones Paralleladdierwerk.
Abhilfe schafft ein serielles Addierwerk, das wir jetzt betrachten wollen (siehe Abbildung 9.24. Dabei
sind Akkumulator und Puffer Schieberegister. Wir verwenden Rechts-Verschieben, bei jedem Schiebevorgang wird die am weitesten linke Stelle frei und der Inhalt des am weitesten rechts stehenden
Delays kommt nach dem Verschieben im Register nicht mehr vor.
Das serielle Addierwerk ist leicht auf mehr Stellen erweiterbar, man benötigt lediglich größere Register, aber keine zusätzliche Logik, die beim Paralleladdierwerk erforderlich wäre. Die Signallaufzeit ist
jetzt kurz, aber das serielle Addierwerk liefert das Ergebnis erst nach n Schritten.
Nun stellt sich natürlich die Frage, welches von den beiden Addierwerken in der Praxis eingesetzt
wird. Die Antwort darauf lautet: Es wird keines der beiden Addierwerke eingesetzt. Man kann nämlich
die Vorteile der beiden Addierwerke verknüpfen. Dazu kombiniert man die beiden Addierwerke zu einem Addierwerk, dessen Schrittzahl von den Summanden abhängt – zum sogenannten von-NeumannAddierwerk. Der logischen Aufbau dieses Addierwerkes ist in Abbildung 9.25 dargestellt.
Der Witz beim von Neumann Addierwerk besteht darin, Überträge erst in späteren Runden zu verarbeiten. Im Puffer sind dabei Überträge, im Akkumulator das Zwischenergebnis gespeichert. Das
Delay S gibt an, ob die Rechnung beendet ist. Sie ist genau dann beendet, wenn keine Überträge mehr
130
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
A3
A2
A1
A0
x3
x3
x3
x3
0
y3
y2
y1
y0
P3
P3
P3
P3
VA
Abbildung 9.24: Synchrones serielles Addierwerk.
vorhanden sind. Daher hängt die Anzahl der Runden von den entstehenden Überträgen und damit vom
Input ab. Tabelle 9.26 zeigt ein Beispiel.
Bzgl. der Anzahl der Runden gilt folgender Satz:
Satz 9.5 Die Anzahl der Runden bei bei der Addition von zwei n-stelligen Dualzahlen mit dem vonNeumann-Addierwerk beträgt im Mittel log n .
Der Grund dafür ist, dass sich die erwartete Anzahl von Einsen in der Überträgen pro Runde halbiert.
Wir geben keinen genauen Beweis für diesen Sachverhalt an, sondern wenden uns dem noch nicht
geklärten Problem des Fan-In zu.
9.3.2
Das Fan-In-Problem
Ein Delay kann also mehrere Eingänge haben, von denen taktabhängig aber nur einer berücksichtigt
werden soll. Abbildung 9.27 zeigt den Standardfall. Abhängig vom Takt soll entweder I (neuer Input)
oder R (Rechenergebnis) die Eingabe für das Delay sein.
Wir überlegen uns eine Boolesche Funktion, die das Problem löst. Eine solche Boolesche Funktion
muss folgendes Verhalten zeigen:
f (S, I, R) :=
I falls S = 0
R falls S = 1
Die Boolesche Variable S unterscheidet dabei, ob der Input von I oder von R kommt. Die Wertetabelle
dieser Booleschen Funktion ist in Abbildung 9.28 dargestellt. Eine Realisierung als Schaltznetz zeigt
Abbildung 9.29.
131
9.4. PLAS UND DAS PRINZIP DER MIKROPROGRAMMIERUNG
A3
A2
A1
A0
Akkumulator
U
HA
HA
HA
HA
P3
P2
P1
P0
Puffer
s
Abbildung 9.25: von Neumann Addierwerk.
9.4
PLAs und das Prinzip der Mikroprogrammierung
Als Programmierbares Logisches Array (PLA) bezeichnet man einen standardisierten Baustein zur
Realisierung von Schaltfunktionen (auf Basis von Booleschen Funktionen in disjunktiver Form). Die
Schaltfunktion kann durch Mikroprogrammierung“ eingestellt beziehungsweise verändert werden.
”
Wir zeigen nun, wie sich solche Bausteine mit den bisherigen Überlegungen realisieren lassen.
9.4.1
Aufbau eines PLAs
Ein PLA hat prinzipiell die in Abbildung 9.30 angegebene Form. Auf jedem Knotenpunkt kann genau
eins von vier verschiedenen Schaltelementen benutzt werden. Dies sind in Abbildung 9.31 dargestellt.
Typischerweise haben PLAs zwei Ebenen, die UND- und die ODER-Ebene. In der UND-Ebene werden Terme für erzeugt und anschließend in der ODER-Ebene zu disjunktiven Formen addiert. Dies
ist schematisch in Abblildung 9.32 illustriert. Wir erläutern die Realsierung einer Schaltfunktion mit
einem PLA exemplarisch an der Schaltfunktion F(x, y, z) = (yz + xyz, xz + xyz). Die entsprechende
Belegung des PLA mit Bausteinen ist in Abbildung 9.33 angegeben.
Kommen wir nun zur Realisierung der PLA-Schaltelemente. Die vier Bausteine lassen sich direkt aus
132
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
Summand I
Summand II
Ergebnis ohne Überträge
Überträge
Ergebnis ohne Überträge
Überträge
Ergebnis ohne Überträge
Überträge
Ergebnis
1
1
0
1
0
1
1
1
0
0
0
1
1
0
1
0
1
1
1
0
0
0
0
0
1
1
0
0
0
0
0
1
1
1
0
1
0
1
1
0
0
0
0
1
1
1
0
0
0
0
0
1
1
0
0
0
0
0
0
0
Abbildung 9.26: Eine Rechnung im von Neumann Addierwerk.
R
Delay
I
Abbildung 9.27: Fan-In bei einem Delay.
ihrer disjunktiven Normalform als Schaltkreise realisieren. Diese sind in Abbildung 9.34 angegeben.
Auf einem PLA mit n Inputs, m Outputs und k Spalten können also alle Schaltfunktionen realisiert
werden, die höchstens n Inputs haben, höchstens m Outputs und insgesamt höchstens k Terme (gegebenenfalls mit Quine-McCluskey reduzieren). Es gibt eine Kurzschreibweise für PLAs, in der nur die
Typen 1, 2, 3, 4 der Bausteine notiert werden, vgl. Abbildung 9.35 für das letzte Beispiel.
Es gibt natürlich i. A. verschiedene Möglichkeiten zur Realisierung einer Schaltkreisfunktion auf dem
einem PLA. Für die bereits betrachtete Schaltkreisfunktion
F(x, y, z) = (yx + xyz, xz + xyz)
| {z } | {z }
u
v
können wir die Komponenten u und v auch folgendermaßen ausdrücken
u = yz + xyz = (x + x)yz + xyz = xyz + xyz + xyz
v = xz + xyz = x(y + y)z + xyz = xyz + xyz + xyz,
und damit das PLA alternative wie in Abbildung 9.36 belegen. Dies realisiert die gleiche Schaltfunktion in nicht optimierter disjunktiver Normalform auf einem PLA derselben Größe. Wenn also das PLA
groß genug ist, muss man die disjunktive Form nicht optimieren.
9.4. PLAS UND DAS PRINZIP DER MIKROPROGRAMMIERUNG
S
0
0
0
0
1
1
1
1
I
0
0
1
1
0
0
1
1
R
0
1
0
1
0
1
0
1
133
Output
0
0
1
1
0
1
0
1
Abbildung 9.28: Fan-In Schaltfunktion.
Delay
0
0
S
I R
1 0 0
Abbildung 9.29: Fan-In Schaltkreis.
9.4.2
Zur Programmierung von PLAs
In der bisherigen Betrachtung haben wir nur logische Arrays verwendet, aber noch nicht ihre Programmiermöglichkeiten berücksichtigt. Diese ergibt sich dadurch, dass man den Bausteintyp durch
eine zusätzliche Steuerung verändern kann.
Dazu bringt man an jedem Baustein eines PLAs Steuerleitungen an, die bestimmen, ob dieser Baustein
gerade Typ 0, 1, 2 oder 3 realisiert. Da 4 verschiedene Zustände angesteuert werden sollen, benötigt
man dafür zwei Steuerleitungen. Bezeichnet man die Inputs dieser Steuerleitungen mit s und t, so kann
das Verhalten eines Bausteins durch folgende Tabelle beschrieben werden.
Bausteintyp
0
1
2
3
s
0
0
1
1
t
0
1
0
1
u
y
x+y
y
y
v
x
x
xy
xy
Hieraus berechnet man sofort u = y + stx, v = sx + stxy + stxy und erhält die Schaltung aus Abbildung 9.37. Diese Bauweise erlaubt also die Steuerung des Bausteintyps über die Steuerleitungen s
und t.
134
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
auf jedem Gitterpunkt
4 Typen von Schaltelementen
Abbildung 9.30: Schema eines PLA.
x
y
y
0
x
1. Identer
x
x
y
x+y
1
x
2. Addierer
y
x
y
2
xy
3. Multiplizierer
y
y
3
xy
4. Negatmultiplizierer
Abbildung 9.31: Die vier Schaltelemente eines PLA.
Auf dem gesamten PLA benötigt man 2 Signale pro Baustein, also insgesamt einen Vektor mit 2(n +
m) · k Bits zur Steuerung aller Bausteine des PLA. Dieser Vektor ist sehr lang; zur Vereinfachung
werden verschiedene Steuervektoren daher über ein ROM (read-only memory) verwaltet. Das ROM
selbst wird wieder durch ein PLA realisiert, dessen Bauweise in Abbildung 9.38 angegeben ist.
Ein solches ROM hat in der UND-Ebene die üblichen PLA-Bausteine, in der ODER-Ebene stehen pro
Spalte die Werte eines Signalvektors zur Steuerung eines anderen PLAs. Die UND-Ebene des ROMs
dient zur Ansteuerung einer ganz bestimmen Spalte, woraufhin der Inhalt der Spalte der ODER-Ebene
ausgelesen wird (also der Inhalt der Steuersignale). Dazu werden in der UND-Ebene in Spalte i genau
die Bausteine benutzt, so dass an die ODER-Ebene eine 1 genau dann weitergegeben wird, wenn
als Input die Binärdarstellung von i vorliegt. In Spalte i der UND-Ebene stehen also die Bausteine
b1 , b2 , . . . , b` mit
2 wenn Bitdarstellung von i an Stelle j den Wert 1 hat
bj =
3 wenn Bitdarstellung von i an Stelle j den Wert 0 hat
135
9.5. LITERATURHINWEISE
Termerzeugung
UND-Ebene
Addition von Termen
ODER-Ebene
Abbildung 9.32: UND/ODER Ebenen eines PLA.
Die Eingabe der Bitfolge einer Addresse bewirkt also, dass genau die zugehörige Spalte der ODEREbene am Output erzeugt wird und keine andere. Ein konkretes Beispiel ist in Abbildung 9.39 angegeben.
Abbildung 9.40 zeigt diesen Vorgang schematisch. Das Einlesen einer Adresse bewirkt die Ausgabe genau eines Vektors von Steuersignalen an ein PLA, das dann entsprechend eine Schaltfunktion
realisiert.
Diese Idee kann noch erweitert werden, indem Steuersignale nicht aus einem ROM abgerufen werden, sondern (teilweise) direkt aus Schaltkreisen heraus erzeugt werden. Dadurch kann der Rechner
also das Verhalten seiner PLAs und damit sein eigenes Verhalten auf der Hardware-Ebene ändern!
Diese Möglichkeit bezeichnet man als Mikroprogrammierung. Sie ist schematisch in Abbildung 9.41
dargestellt.
9.5
Literaturhinweise
Dieses Kapitel folgt der Darstellung in [OV03].
136
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
1
1
x
0
1
1
2
2
2
2
0
2
2
2
3
1
y
3
y
z
2
yz
xyz
xyz
1
1
0
0
0
1
1
u = yz + xyz
yz
ODEREbene
0
v = xz + xyz
Abbildung 9.33: PLA Belegung für die Schaltfunktion F(x, y, z) = (yz + xyz, xz + xyz).
x
y
x
y
x
y
y
y
x
y
x·y
1. Identer
2. Addierer
3. Multiplizierer
x·y
4. Negatmultiplizierer
Abbildung 9.34: Schaltkreise für die Bausteine eines PLA.
137
9.5. LITERATURHINWEISE
0
2
2
2
3
2
0
2
2
2
2
3
1
1
0
0
0
0
1
1
Abbildung 9.35: Kurzschreibweise für den PLA zur Schaltkreisfunktion F(x, y, z) = (yz + xyz, xz +
xyz).
x
2
3
2
2
y
3
3
2
2
z
2
2
2
3
xyz xyz xyz xyz
1
1
1
0
1
0
1
1
Abbildung 9.36: Alternative PLA Belegung zur Schaltkreisfunktion F(x, y, z) = (yz + xyz, xz + xyz).
s
x
t
y
u
v
u = y + stx
v = sx + stxy + stxy
Abbildung 9.37: PLA Baustein mit Steuerleitungen.
138
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
dlog Le Zeichen
für Bitkodierung der Spalten
Addresse der Spalte
U ND -E BENE
O DER -E BENE
L
jede Spalte des ROM entspricht einem Vektor von Steuersignalen zur
Erzeugung einer ganz bestimmten Schaltfunktion auf dem Ausgangs-PLA
Abbildung 9.38: Funktionsweise eines ROM als PLA.
Baustein-Typen
1
3
3
3
3
2
2
2
2
0
3
3
2
2
3
3
2
2
1
3
2
3
2
3
2
3
2
0
0
0
0
0
1
0
0
6
7
3
3
3
3
Addressen
0
1
2
3
4
5
ein Vektor von Steuersignalen an Addresse 5
Abbildung 9.39: Beispiel für ein ROM. Die Adresse (1, 0, 1) wird in der UND-Ebene dekodiert und
der Inhalt (der Steuervektor (3, 3, 3)) in der ODER-Ebene ausgegeben.
139
9.5. LITERATURHINWEISE
1
0
1
0
...
b1
b2
..
.
...
...
dlog Le Zeichen
b`
L Addressen/Vektoren
Steuersignal - Vektor an Addresse `
PLA
Steuersignale
Abbildung 9.40: Beispiel für ein ROM. Die Adresse (1, 0, 1) wird in der UND-Ebene dekodiert und
der Inhalt (der Steuervektor (3, 3, 3)) in der ODER-Ebene ausgegeben.
Worte, gewisse Steuerleitungen
x
Delays
PLA
ein Teil des Outputs des PLAs
ist Teil der Steuersignale für nächsten Takt
Abbildung 9.41: Das Prinzip der Mikroprogrammierung.
140
KAPITEL 9. SCHALTKREISTHEORIE UND RECHNERARCHITEKTUR
Literaturverzeichnis
[CLRS01] Thomas H. Cormen, Charles E. Leiserson, Ronald R. Rivest, and Clifford Stein. Introduction to Algorithms. The MIT Press, Cambridge, MA, second edition, 2001. 1.3, 2.3, 3.5,
4.2, 5.4, 6.4, 7.3, 8.3
[HU79]
John E. Hopcroft and Jeffrey D. Ullman. Introduction to Automata Theory, Languages,
and Computation. Addison-Wesley, Reading, NY, 1979. 1.3
[Knu71]
Donald E. Knuth. Optimum binary search trees. Acta Inform., 1:14–25, 1971. 6.4
[Knu98]
Donald. E. Knuth. The Art of Computer Programming, volume 3 Sorting and Searching.
Addison-Wesley, Reading, NY, second edition, 1998. 2.3, 4.2, 8.1.2, 8.2.2
[OV03]
Walter Oberschelp and Gottfried Vossen. Rechneraufbau und Rechnerstrukturen. Oldenburg Verlag, München, 9 edition, 2003. 9.5
[OW02]
Thomas Ottmann and Peter Widmayer. Algorithmen und Datenstrukturen. Spektrum Akademischer Verlag, 2002. 4. Auflage. 2.3, 4.2
[SS02]
Gunter Saake and Kai-Uwe Sattler. Algorithmen und Datenstrukturen: eine Einfürung mit
Java. dpunkt.verlag, Heidelberg, 2002. 2.3, 4.2
141
Index
Äquivalenz, 111
Überdeckungsproblem, 127
Addierer, 138
Addierwerk
asynchrones Parallel-, 131
serielles, 132
synchrones Parallel-, 131
Von-Neumann-, 133
AND, 111
Antivalenz, 111
Auslastungsfaktor, 99
AVL-Baum, 55
B-Baum, 85
Balance, 55
Basisoperationen
in AVL-Bäumen, 63
in B-Bäumen, 88
Bäume, 15
binäre, 16
Traversierung von, 22
Block, 85
Blockcode, 29
Boolesche Funktion, 110
Bucketsort, 1
einfaches, 2
Chaining, 98
Code, 29
adaptiver, 40
dynamischer, 40
statischer, 40
Codierung, 29
zeichenweise, 40
Delay, 129
Disjunktion, 111
disjunktive Normalform, 114
Divisionsmethode, 97
DNF, 114
Doppelrotation, 59
Double Hashing, 102
dynamische Optimierung, 82
echte Verkürzung, 123
Einfügen eines Knotens
in AVL-Bäumen, 64
in B-Bäumen, 88
Entropie, 43
Fan-In, 134
Flimmerschaltung, 129
Gleichverteilungsannahme, 99
Halbaddierer, 114
Hash-Funktion, 96
Hashing, 95
Double, 102
Häufigkeitsverteilung, 73
Huffman Code, 33
adaptiver, 41
Identer, 138
Implikant, 123
Prim-, 123
Implikation, 111
Index, 111
einschlägiger, 112
Inorder-Durchlauf, 22
Iterator, 25
142
143
INDEX
Karnaugh-Verfahren, 119
Kollision, 95
Kollisionsbehandlung, 95
Konjunktion, 111
Lempel-Ziv Code, 41
Löschen eines Knotens
in AVL-Bäumen, 67
in B-Bäumen, 90
Minterm, 112
Multiplikationsmethode, 97
Multiplizierer, 138
Negatmultiplizierer, 138
normierte Häufigkeit, 43
Offene Adressierung, 100
Open Addressing, 100
Optimale Substruktur, 38, 73
Optimalität, 37, 72
asymptotische, 44
OR, 111
Paralleladdierwerk
asynchrones, 131
synchrones, 131
Permutationsbedingung, 100
Pierce-Pfeil, 111
PLA, 135
Postorder-Durchlauf, 22
Präfixcode, 31
Preorder-Durchlauf, 22
Primimplikant, 123
Priority Queue, 25
Probing
linear, 101
quadratic, 102
Quine-McCluskey-Verfahren, 123
Redundanz, 43
Register, 130
Resolution, 118
Ringzähler, 129
Rotation, 49, 59
run length code, 41
Schaltkreisfunktion, 109
Schaltkreistheorie, 109
Schaltnetz, 116
Sheffer-Strich, 111
Sondierung
lineare, 101
quadratische, 102
Splitten eines Knoten, 88
Suchbaum, 45
-eigenschaft, 46
Einfügen im, 46
Löschen im, 47
optimaler statischer, 71
Suchen im, 46
Suchbaumeigenschaft, 46
Suchen eines Knotens
in AVL-Bäumen, 64
in B-Bauen, 88
Urnenmodell, 105
variable length code, 30
Verfahren von
Karnaugh, 119
Quine-McCluskey, 123
verlustfrei, 29, 40
Volladdierer, 116
XOR, 111
Zugriffshäufigkeit, 72
Zugriffszeit, 73