3. Suchbäume
§ Binäre Suchbäume
- Begriffe und Eigenschaften (Wiederholung PROG 2) - Traversierung (Wiederholung PROG 2)
- Binäre Suchbäume (Wiederholung PROG 2) - Traversierung von Bäumen mit Elternzeiger
§ AVL-Bäume
§ B-Bäume
§ 2-3-4-Bäume und Rot-Schwarzbäume
Begriffe und Eigenschaften
Blatt
Wurzel Kante
Knoten
§ Ein Baum besteht aus einer Menge von Knoten und
einer Menge von gerichteten Kanten (von Elternknoten zu Kindknoten)
§ Es gibt genau einen Knoten, der keine eingehende Kante hat: die sogenannte Wurzel.
§ Alle anderen Knoten haben genau eine eingehende Kante.
§ Zyklen sind nicht erlaubt.
§ Knoten ohne ausgehende Kante heißen Blätter.
§ Kanten sind meistens nach unten gerichtet. Kantenrichtung wird dann oft weggelassen.
§ Spezialfall Binärbaum: Jeder Knoten hat maximal 2 Kinder (linkes bzw. rechtes Kind) k ist Elternknoten von s und t.
k
s t
Höhe eines Baums
Höhe = Maximale Anzahl von Kanten von seiner Wurzel zu einem Blatt.
Beispiel
10
6
5 12
3 7 11 15
3
1
0 0
2
1 0
0
Höhe für jeden Teilbaum
Beachte
• Ein Baum, der nur aus einem Knoten besteht, besitzt die Höhe 0.
Vollständiger Binärbaum
Beispiele
Eigenschaften
• Ein vollständiger Binärbaum mit n Knoten hat die Höhe h = ëlog
2nû.
• Vollständige Binärbäume sind (bei einer gegebenen Knotenzahl) Binärbäume mit einer minimalen Höhe.
Ein vollständiger Binärbaum ist ein Binärbaum, bei der jede Ebene (bis auf die letzte)
vollständig gefüllt und die letzte Ebene von links nach rechts gefüllt ist.
Implementierung von Binärbäumen
§ Implementierung als verkettete Struktur:
Jeder Knoten hat jeweils eine Referenz für das linke und das rechte Kind.
A
G H
B C
D E F
class Node<K,V> { K key;
V value;
Node<K,V> left; // linkes Kind Node<K,V> right; // rechtes Kind }
I
A
C B
F E
D
H I G
root
Traversierung von Binärbäumen
Durchlaufreihenfolge
§ PreOrder: besuche Wurzel, besuche linken Teilbaum; besuche rechten Teilbaum;
§ PostOrder: besuche linken Teilbaum; besuche rechten Teilbaum; besuche Wurzel;
§ InOrder: besuche linken Teilbaum; besuche Wurzel; besuche rechten Teilbaum;
§ LevelOrder: besuche Knoten ebenenweise
Ziel
§ Das Besuchen aller Knoten in einer bestimmten Reihenfolge ist eine oft benötigte Operation.
Bemerkungen
§ Die Präfixe Pre, Post bzw. In bedeuten vorher, nachher und dazwischen.
Gemeint ist damit der Zeitpunkt, an dem die Wurzel besucht wird.
PreOrder-Durchlauf
Beispiel
*
- c
Durchlaufreihenfolge: * - a b c
static void preOrder(Node<K,V> p) {
if (p != null){
bearbeite(p);
preOrder(p.left);
preOrder(p.right);
} }
static class Node<K,V> { K key;
V value;
Node<K,V> left;
Node<K,V> right;
}
private Node<K,V> root;
//...
static void main(...) { preOrder(root);
}
PostOrder-Durchlauf
Beispiel
*
- c
a b
Durchlaufreihenfolge: a b – c *
(Entspricht der sog. Postfix-Notation für arithmetische Ausdrücke)
static void postOrder(Node<K, V> p) { if (p != null){
postOrder(p.left);
postOrder(p.right);
bearbeite(p);
} }
InOrder-Durchlauf für Binärbäume
Beispiel
*
- c
a b
Durchlaufreihenfolge: a - b * c
static void inOrder(Node<K, V> p) { if (p != null){
inOrder(p.left);
bearbeite(p);
inOrder(p.right);
} }
LevelOrder-Durchlauf
1
2 3
4 6
Durchlaufreihenfolge: 1, 2, 3, 4, 5, 6
static void levelOrder(Node<K,V> p) {
Queue<Node<K,V>> queue = new ArrayDeque<Node<K,V>>();
queue.add(p);
while (!queue.isEmpty()) {
Node<K,V> q = queue.remove();
if (q != null) { bearbeite(q);
queue.add(q.left);
queue.addd(q.right);
} } }
5
Die Knoten werden ebenenweise in einer Schlange gespeichert und in einer while-Schleife abgearbeitet.
Definition binärer Suchbäume
§ Ein binärer Suchbaum ist ein Binärbaum, bei dem für alle Knoten k folgende Eigenschaften gelten:
- Alle Schlüssel im linken Teilbaum sind kleiner als k - Alle Schlüssel im rechten Teilbaum sind größer als k
§ Beachte, dass die Zahlen hier eindeutig sein müssen. Es ist aber auch möglich, dass gleiche Zahlen mehrfach vorkommen dürfen, was kleine Änderungen in den Algorithmen erfordert.
Beispiele:
7
2 8
1 4
6
2 8
4
1
3
4
Degenerierter Suchbaum
Klasse BinarySearchTree
public class BinarySearchTree<K extends Comparable<? super K>, V> { private static class Node<K, V> {
private K key;
private V value;
private Node<K, V> left;
private Node<K, V> right;
private Node(K k, V v) { key = k;
value = v;
left = null;
right = null;
} }
private Node<K, V> root = null;
// ...
}
Suchen in binären Suchbäumen
Beispiel
3 7
2 8
1 4
6
searchR(3, root) liefert true
root
public V search(K key) {
return searchR(key, root);
}
private V searchR(K key, Node<K,V> p) { if (p == null)
return null;
else if (key.compareTo(p.key) < 0) return searchR(key, p.left);
else if (key.compareTo(p.key) > 0) return searchR(key, p.right);
else
return p.value;
}
Einfügen in binären Suchbäumen (1)
Idee
§ Um eine Zahl x einzufügen, wird zunächst nach x gesucht.
§ Falls x nicht bereits im Baum vorkommt,
endet die Suche erfolglos bei einer null-Referenz.
§ An dieser Stelle wird dann ein neuen Knoten mit Eintrag x eingefügt.
Beispiel 1: füge 6 ein
3 7
2 9
1 4
Suche von 6 endet
6 4 2
7
3
9
1
Ersetzte null durch neuen
Beispiel 2: füge 8 ein
9 2
4
3
7
1
Suche von 8 endet 6
9 7
8 2
4
3 1
Ersetzte null durch 6
Einfügen in binären Suchbäumen (2)
private V oldValue; // Rückgabeparameter public V insert(K key, V value) {
root = insertR(key, value, root);
return oldValue;
}
private Node<K,V> insertR(K key, V value, Node<K,V> p) { if (p == null) {
p = new Node(key, value);
oldValue = null;
}
else if (key.compareTo(p.key) < 0) p.left = insertR(key, value, p.left);
else if (key.compareTo(p.key) > 0)
p.right = insertR(key, value, p.right);
else { // Schlüssel bereits vorhanden:
oldValue = p.value;
p.value = value;
}
3 7
2 9
1 4
6 4 2
7
3
9
1
root = insert(6, root);
root root
Löschen in binären Suchbäumen (1)
§ Um eine Zahl x zu löschen, wird zunächst nach x gesucht.
Es sind dann 4 Fälle zu unterscheiden:
§ Fall „Nicht vorhanden“:
x kommt nicht vor. Dann ist nichts zu tun.
§ Fall „Keine Kinder“:
x kommt in einem Blatt vor (keine Kinder):
dann kann der Knoten einfach entfernt werden.
§ Fall „Ein Kind“:
Der Knoten, der x enthält, hat genau ein Kind:
s. nächste Folie
§ Fall „Zwei Kinder“:
Der Knoten, der x enthält, hat zwei Kinder:
s. übernächste Folie
Idee
Löschen in binären Suchbäumen (2)
Fall: der zu löschende Knoten k hat ein Kind
§ Überbrücke den Knoten k, indem der Elternknoten von k auf das Kind von k verzeigert wird (Bypass).
Beispiel: lösche Knoten k mit Inhalt 4
2 7
4
3
9
1 4
2 7
3
9
1
2 7
3 9
1
k k
Löschen in binären Suchbäumen (3)
Fall: der zu löschende Knoten k hat zwei Kinder
§ Ersetze den Knoten k durch den kleinsten Knoten k
minim rechten Teilbaum von k.
§ Lösche dann k
min.
§ Da der Knoten k
minkein linkes Kind haben kann, kann das Löschen von k
minwie im Fall „Ein Kind“ bzw. „Keine Kinder“ behandelt werden.
Beispiel: lösche Knoten k mit Inhalt 2
7 2
5 3
9 1
4 6
2 7
5 3
9 1
4 6
3 7
5 3
9 1
4 6
3 7
5 9 1
4 6
k k k
kmin kmin
Löschen in binären Suchbäumen (4)
private V oldValue; // Rückgabeparameter publicV remove(K key) {
root = removeR(k, root);
returnoldValue;
}
private Node<K,V> removeR(K key, Node<K,V> p) { if(p == null) { oldValue = null; }
else if(key.compareTo(p.key) < 0) p.left = removeR(key, p.left);
else if(key.compareTo(p.key) > 0) p.right = removeR(key, p.right);
else if(p.left == null || p.right == null) { // p muss gelöscht werden // und hat ein oder kein Kind:
oldValue = p.value;
p = (p.left != null) ? p.left : p.right;
} else{
// p muss gelöscht werden und hat zwei Kinder:
MinEntry<K,V> min = newMinEntry<K,V>();
p.right = getRemMinR(p.right, min);
oldValue = p.value;
p.key = min.key;
p.value = min.value;
private Node<K,V> getRemMinR(Node<K,V> p, MinEntry<K,V> min) { assertp != null;
if(p.left == null) { min.key = p.key;
min.value = p.value;
p = p.right;
} else
p.left = getRemMinR(p.left, min);
returnp;
}
private static classMinEntry<K, V> { private K key;
private V value;
}
§ getRemMinR löscht im Baum p den Knoten mit kleinstem Schlüssel und liefert Schlüssel und Daten des gelöschten Knotens über min zurück
§ MinEntry ist ein Hilfsdatentyp für den Rückgabe-
Analyse
Worst-Case
§ Im schlechtesten Fall kann ein binärer Suchbaum mit n Knoten zu einem Baum der Höhe n-1 entarten.
§ Damit: T
max(n) = O(n)
Average-Case
§ In [Ottmann und Widmayer 2002] werden zwei Ergebnisse hergeleitet, die sich darin unterscheiden, welche Verteilung der Bäume angenommen wird.
§ Bäume mit n Knoten entstehen durch eine Folge von Einfüge-Operationen von n unterschiedlichen Elementen. Es wird angenommen, dass jede der n! möglichen Anordnungen der Elemente gleich wahrscheinlich ist.
Damit: T
mit(n) = O(log
2n)
§ Es wird angenommen, dass alle strukturell verschiedenen binären Suchbäume mit n Knoten gleichwahrscheinlich sind.
Damit: T
mit(n) = O( ) n
3. Suchbäume
§ Binäre Suchbäume
- Begriffe und Eigenschaften (Wiederholung PROG 2) - Traversierung (Wiederholung PROG 2)
- Binäre Suchbäume (Wiederholung PROG 2) - Traversierung von Bäumen mit Elternzeiger
§ AVL-Bäume
§ B-Bäume
§ 2-3-4-Bäume und Rot-Schwarzbäume
Iterative Traversierung von Binärbäumen
Problem
§ In binären Suchbäumen gibt es für einen Knoten im allgemeinen keinen effizienten Zugriff auf seinen InOrder-Vorgänger bzw. -Nachfolger.
§ Daher ist mit der bisher besprochenen Datenstruktur für Suchbäume
keine effiziente Vorwärts- bzw. Rückwärtstraversierung mit Iteratoren machbar.
§ Wie erreicht man den Nachfolger von 8?
§ Wie erreicht man den Vorgänger von 6?
10
12 5
15 3 7
6 8 1
6 8 1
15
Bäume mit Elternzeigern
§ Erweitere jeden Knoten um einen Zeiger auf den Elternknoten.
10
5 12
7 3
Klasse BinarySearchTree mit Elternzeiger (1)
public class BinarySearchTree<K extends …, V> { private static class Node<K, V> {
private Node<K, V> parent; // Elternzeiger private K key;
private V value;
private Node<K, V> left;
private Node<K, V> right;
private Node(K k, V v) { key = k;
value = v;
left = null;
right = null;
parent = null;
} }
private Node<K, V> root = null;
// ...
§ Erweitere Klasse Node
um Elternzeiger parent:
Klasse BinarySearchTree mit Elternzeiger (2)
§ Überall, wo die Struktur des Baums geändert wird, wird zusätzlich der parent-Zeiger neu gesetzt:
expr1.left = expr2; expr1.left = expr2;
if (expr1.left != null)
expr1.left.parent = expr1;
10
5
...
...
...
expr1
expr2
...
...
...
10
5
expr1 und expr2 sind beliebige Ausdrücke vom Typ Node.
expr1
expr2
...
Klasse BinarySearchTree mit Elternzeiger (3)
§ Analog:
expr1.right = expr2; expr1.right = expr2;
if (expr1.right != null)
expr1.right.parent = expr1;
root = expr; root = expr;
if (root != null)
root.parent = null;
§ Beachte, dass der parent-Zeiger des root-Knotens den Wert null bekommt:
Beispiel: insert-Methode mit Elternzeiger
public V insert(K key, V value) { root = insertR(key, value, root);
if (root != null)
root.parent = null;
return oldValue;
}
private Node<K,V> insertR(K key, V value, Node<K,V> p) { if (p == null) {
p = new Node(key, value);
oldValue = null;
} else if (key.compareTo(p.key) < 0) { p.left = insertR(key, value, p.left);
if (p.left != null)
p.left.parent = p;
} else if (key.compareTo(p.key) > 0) { p.right = insertR(key, value, p.right);
if (p.right != null)
p.right.parent = p;
} else { // Schlüssel bereits vorhanden:
oldValue = p.value;
p.value = value;
}
return p;
Iterative Traversierung von Bäumen
§ Hier: nur Traversierung zum InOrder-Nachfolger.
Traversierung zum InOrder-Vorgänger geht analog.
§ Sei p der aktuelle Knoten. Gesucht ist der Nachfolger von p.
Wir unterscheiden zwei Fälle:
if (p.right != null)
p = leftMostDescendant(p.right);
else
p = parentOfLeftMostAncestor(p);
p
leftMostDescendant(p.right)
pparentOfLeftMostAncestor(p)
...
... ...
...
... ...
6 8 1
15
leftMostDescendant
10
5 12
7 3
InOrder-Nachfolger von p = 5:
leftMostDescendant(p.right) = 6
6 8 1
15
parentOfLeftMostAncestor
10
5 12
7 3
InOrder-Nachfolger von p = 8:
parentOfLeftMostAncestor(p) = 10
Implementierung von leftMostDescendant
private Node<K,V> leftMostDescendant(Node<K,V> p) { assert p != null;
while (p.left != null) p = p.left;
return p;
}
p
leftMostDescendant(p)
...
...
Implementierung von parentOfLeftMostAncestor
private Node<K,V> parentOfLeftMostAncestor(Node<K,V> p) { assert p != null;
while (p.parent != null && p.parent.right == p) p = p.parent;
return p.parent; // kann auch null sein }
p
parentOfLeftMostAncestor(p)
...
...
...
Traversierungsschleife für InOrder-Nachfolger
// Erster Knoten:
Node<K,V> p = null;
if (root != null)
p = leftMostDescendant(root);
while( p != null) {
System.out.print(p.key + ", ");
if (p.right != null)
p= leftMostDescendant(p.right);
else
p = ParentOfLeftMostAncestor(p);
}
6 8 1
15 10
5 12
7 3
§ Traversersierung-Schleife ergibt:
3. Suchbäume
§ Binäre Suchbäume
- Begriffe und Eigenschaften (Wiederholung PROG 2) - Traversierung (Wiederholung PROG 2)
- Binäre Suchbäume (Wiederholung PROG 2) - Traversierung von Bäumen mit Elternzeiger
§ AVL-Bäume
§ B-Bäume
§ 2-3-4-Bäume und Rot-Schwarzbäume
Definition von AVL-Bäumen
§ Ein binärer Suchbaum heißt AVL-Baum oder (höhen)balanzierter Baum, falls sich für jeden Knoten die Höhen der beiden Teilbäume um höchstens 1 unterscheiden.
§ Die Abkürzung AVL geht zurück auf Adelson-Velskij und Landis.
Beispiel für AVL-Baum: Beispiel für Nicht-AVL-Baum
10
6
5 12
3 7 11 15
10
8
5 12
7
Höhe des Teilbaums Höhenunterschied
= Höhe rechter Teilbaum
− Höhe linker Teilbaum (Beachte: Höhe eines leeren Teilbaums ist -1.)
3
1
0 0
2
1 0
0 0
0
1 2
3 -1
0
0 0 0
0 0
+1
+2 0
-2
-1 +1
Eigenschaft
Rotationen (1)
Zentrale Eigenschaft von AVL-Bäumen:
Ein unbalanzierter Baum, der einen Höhenunterschied von -2 (linkslastiger Baum) oder +2 (rechtslastiger Baum) hat und dessen Teilbäume ausbalanziert sind, lassen sich durch eine einfache Rotationsoperation ausbalanzieren.
Fall A: Baum ist linkslastig, d.h. Höhenunterschied = -2
p
q
Teilbäume A, B, und C sind ausbalanziert und können auch leer sein.
h+1 -2
Unterfall A1: linker Teilbaum hat Höhenunterschied -1 oder 0:
A
C h-1
B
h h od.
h-1 -1 oder 0
Rotate Right
q
p
h od.
h+1 A
C B
h
h od.
h-1 1 od. 0
h-1
Rotationen (2)
Fall A: Baum ist linkslastig, d.h. Höhenunterschied = -2
p
q h+1 -2
Unterfall A2: linker Teilbaum hat Höhenunterschied +1:
A
D h-1
B
h +1
Rotate Left
Right
rp
A C D
h
0
C h-1 r
h-1 od.
h-2
h-2 od.
h-1
1.
2.
B q h
Teilbäume B und C haben aber nicht beide die Höhe h-2.
Rotationen (3)
Fall B: Baum ist rechtslastig, d.h. Höhenunterschied = +2
p
q h+1 +2
Unterfall B1: rechter Teilbaum hat Höhenunterschied 0 oder +1:
B A
h-1
C h od.
h-1 h
0 oder +1
Rotate Left
q
p h od.
h+1
0 od. -1
A
C h
B h-1
0 oder +1
h od.
h-1
Rotationen (4)
Fall B: Baum ist rechtslastig, d.h. Höhenunterschied = +2 Unterfall B2: rechter Teilbaum hat Höhenunterschied -1:
Rotate
Right Left
rq
A C D
h
0 p
h+1 q +2
D A
h-1
C h
-1
B
r h-1
h-1 od.
h-2 h-2 od.
h-1
1.
2.
B p h
Teilbäume B und C haben aber nicht beide die Höhe h-2.
Idee für Algorithmus:
1) Füge ein bzw. lösche wie bei binären Suchbäumen.
2) Gehe dann von der Einfügestelle bzw. Löschstelle bis zur Wurzel
und balanziere falls notwendig mit einer der 4 Rotatationsoperationen lokal aus.
Einfügen und Löschen in AVL-Bäumen
Bemerkungen:
§ Da der Baum vor dem Einfügen bzw. Löschen ausbalanziert war, haben alle Teibäume einen Höhenunterschied von -1, 0 oder +1.
Damit können nach dem Schritt 1) nur die Fälle A1, A2, B1 oder B2 auftreten.
§ Schritt 2) lässt sich geschickt beim Aufstieg aus der Rekursion durchführen (siehe balance-Aufruf in der Methode insertR auf Seite 3-44).
§ Es lässt sich zeigen, dass beim Einfügen insgesamt maximal eine der 4 Rotationsoperationen notwendig ist.
§ Beim Löschen müssen i.a. mehrere Rotationsoperationen durchgeführt werden
(siehe Beispiel 2 auf Seite 3-49 und 3-50)
4
3 5 3
2
4
Beispiel zu Einfügen in AVL-Bäumen (1)
1
+1 +2
Rotate Left (Fall B1)
2
1 3 2
4
5 2
1 3
4
Füge 5 ein 1
Gehe vom Knoten 5 in Richtung Wurzel bis zum ersten
unbalanzierten Teilbaum.
4
7
5
14
Beispiel zu Einfügen in AVL-Bäumen (2)
Beispiel:
Füge 13 ein
Gehe bis zum ersten unbalanzierten
Teilbaum.
2
1 3
6
5 14
7 15 4
15 14 6
7 4
13 2
1 3 5
15 14 6
7 4
2
1 3 5
+2 1.
2. Rotate
Right Left (Fall B2)
15 13
2
1 3 6
Algorithmen (1)
§ Node wie bei binären Suchbäumen.
Zusätzlich wird für jeden Knoten im Baum noch die Höhe des entsprechenden Teilbaums abgespeichert.
private static class Node<K, V> { int height;
K key;
V value;
Node<K, V> left;
Node<K, V> right;
private Node(K k, V v) { height = 0;
key = k;
value = v;
left = null;
right = null;
} }
private int getHeight(Node<K,V> p) { if (p == null)
return -1;
else
return p.height;
}
private int getBalance(Node<K,V> p) { if (p == null)
return 0;
else
return getHeight(p.right) - getHeight(p.left);
}
Algorithmen (2)
§ Erweitere die rekursive insertR-Operation für binäre Suchbäume um eine Balanzieroperation, die beim rekursiven Aufstieg durchgeführt wird.
private Node<K,V> insertR(K key, V value, Node<K,V> p) { if (p == null) {
p = new Node(key, value);
oldValue = null;
}
else if (key.compareTo(p.key) < 0) p.left = insertR(key, value, p.left);
else if (key.compareTo(p.key) > 0)
p.right = insertR(key, value, p.right);
else { // Schlüssel bereits vorhanden:
oldValue = p.value;
p.value = value;
}
p = balance(p);
return p;
}
§ Die removeR- und getRemMinR-Operation werden analog erweitert.
Algorithmen (3)
private Node<K,V> balance(Node<K,V> p) { if (p == null)
return null;
p.height = Math.max(getHeight(p.left), getHeight(p.right)) + 1;
if (getBalance(p) == -2) {
if (getBalance(p.left) <= 0) p = rotateRight(p);
else
p = rotateLeftRight(p);
}
else if (getBalance(p) == +2) { if (getBalance(p.right) >= 0)
p = rotateLeft(p);
else
p = rotateRightLeft(p);
}
return p;
}
Fall A1 Fall A2
Fall B1 Fall B2
Höhe aktualisieren.
Algorithmen (3)
p
q
C
Rotate
Right
qp A
private Node<K,V> rotateRight(Node<K,V> p) { assert p.left != null;
Node<K, V> q = p.left;
p.left = q.right;
q.right = p;
p.height = Math.max(getHeight(p.left), getHeight(p.right)) + 1;
q.height = Math.max(getHeight(q.left), getHeight(q.right)) + 1;
return q;
}
Algorithmen (4)
private Node<K,V> rotateLeft(Node<K,V> p) { // analog zu rotateRight
}
private Node<K,V> rotateLeftRight(Node<K,V> p) { assert p.left != null;
p.left = rotateLeft(p.left);
return rotateRight(p);
}
private Node<K,V> rotateRightLeft(Node<K,V> p) { assert p.right != null;
p.right = rotateRight(p.right);
return rotateLeft(p);
}
Beispiel 1 zu Löschen in AVL-Bäumen
Lösche 10
Rotate Left (Fall B1) 5
3 9
4 18 26
20
25 13
15 10
17 19
5
3 9
4 18 26
20
25 13
15 13
17 19
5
3 9
4 18 26
20 15 25
13
17 19 +2
5
3 9
4 19 26
20 18 25
13
17 15
Beispiel 2 zu Löschen in AVL-Bäumen (1)
Lösche 5
Rotate Left Right
(Fall A2) 5
3 9
4 18 26
20
25 13
15 10
17 19
9
3 9
4 18 26
20
25 13
15 10
17 19
9-2 3
4 18 26
20
25 13
15 10
4
3 9
18 26 20
25 13
15 10
Beispiel 2 zu Löschen in AVL-Bäumen (2)
Rotate Right Left
(Fall B2) 4 +2
3 9
18 26 20
25 13
15 10
19 17
4
3 9 26
18
20 13 25
15 10
17 19
3. Suchbäume
§ Binäre Suchbäume
- Begriffe und Eigenschaften (Wiederholung PROG 2) - Traversierung (Wiederholung PROG 2)
- Binäre Suchbäume (Wiederholung PROG 2) - Traversierung von Bäumen mit Elternzeiger
§ AVL-Bäume
§ B-Bäume
§ 2-3-4-Bäume und Rot-Schwarzbäume
Definition von B-Bäumen der Ordnung m
1. Balanzierung: Jedes Blatt hat die gleiche Tiefe.
2. Anzahl Schlüssel:
- maximal m-1 (einschl.).
- minimal ém/2ù -1 (einschl.). Ausnahme Wurzel: minimal 1 Schlüssel.
3. Anzahl Kinder:
Jeder Knoten (außer den Blättern) mit i vielen Schlüsseln hat genau i+1 Kinder.
(Beachte, dass ein B-Baum aus genau einem Knoten – die Wurzel – bestehen darf) 4. Schlüsselordnung:
Für jeden Knoten K mit i-1 Schlüsseln k
0, k
1, …, k
i-2und i Kindern mit ihren Teilbäumen B
0, B
1, …, B
i-1gilt folgende Beziehung:
Schlüssel in B
0< k
0< Schlüssel in B
1< k
1< Schlüssel in B
2< … Schlüssel in B
i-2< k
i-2< Schlüssel in B
i-1.
k0 k1 … ki-2 Knoten K mit seinen i-1 Schlüsseln
B B B … B B
i Kinder mit ihren Teilbäumen
Beispiele für B-Bäume der Ordnung m = 4
10 30 35 50
20 60
70 90 20 60
10
20
60 90
B-Baum der Höhe 0 B-Baum der Höhe 1
B-Baum der Höhe 2
Implementierung von B-Bäumen der Ordnung m
§ Jeder Knoten besteht aus
- einem statischen Feld der Größe m-1 für die Schlüssel und - einem statischem Feld der Größe m für die Zeiger
auf die Kinder.
k0 k1 … ki-2
...
k0 k1 … ki-2 ...
Feld für m-1 Schlüssel
Feld für m Zeiger auf die Kinder
… ...
...
Suchen in B-Bäumen
Beispiel: Suchen nach k = 43
V search(K key, Node<K,V> p) { if (p == 0)
return null; // nicht gefunden;
suche key in Schlüsselmenge von Knoten p (beispielsweise mit binärer Suche);
if (key gefunden) return Daten;
p = Teilbaum, in dem key liegen könnte;
return search(key, p);
}
10 30 35 50
20 60
70 90 Rekursiver Aufruf
Einfügen in B-Bäumen
V insert(K key, V value, Node<K,V> p) { suche Schlüssel key im Baum p;
if (key gefunden) {
speichere Daten in oldValue;
ersetze Daten durch value;
return oldValue;
}
füge key, value an der Blattstelle ein, wo die Suche beendet wurde;
if (kein Schlüsselüberlauf) return null;
for (alle Knoten k vom aktuellen Knoten bis zur Wurzel) { if (Schlüsselüberlaufbei k)
führeSplit-Operation für k durch;
else
return null;
} }
§ Einfügen immer in einem Blatt.
§ Schlüsselüberlauf, falls Anzahl der Schlüssel gleich m.
§ insert kann wie bei AVL- Bäumen rekursiv realisiert werden.
Die Behandlung der Schlüssel- überläufe geschieht dann beim rekursiven Aufstieg.
§ Muss die Wurzel gesplittet
werden, dann entsteht eine
neue Wurzel (siehe Beispiel)
Split-Operation bei Schlüsselüberlauf
k0 k1 … kmit-1 kmit kmit+1 … km-1 Aktueller Knoten mit Schlüsselüberlauf:
Knoten enthält genau m Schlüssel
... ...
k0 k1 … kmit-1 kmit+1 … km-1
Split- Operation Elternknoten
Teile Schlüssel- menge an der Stelle mit = ëm/2û
... kmit ...
Beispiel 1: Einfügen von 82
10 30 35 50
20 60
70 90
11 12 15
5 7 21 24 33 38 43 45 52 55 65 67 69 80 85 96
10 30 35 50
20 60
70 90
1) Suchen von 82
2) 82 einfügen;
keine Split-Operation
notwendig, da genügend Platz
Beispiel 2: Einfügen von 39
10 30 35 50
20 60
70 90
11 12 15
5 7 21 24 33 38 44 45 52 55 65 67 69 96
10 30 35 50
20 60
70 90
1) Suchen und Einfügen von 39
2) Schlüsselüberlauf
80 82 85
Beispiel 2: Einfügen von 39 (Fortsetzung)
10 30 35 50
20 60
70 90
11 12 15
5 7 21 24 33 38 39 44 45 52 55 65 67 69 80 82 85 96
3) Split-Operation führt zu weiterem Überlauf
10 30 35 44 50
20 60
70 90
11 12 15
5 7 21 24 33 38 39 45 52 55 65 67 69 80 82 85 96
Beispiel 2: Einfügen von 39 (Fortsetzung)
10 30 35 44 50
20 60
70 90
11 12 15
5 7 21 24 33 38 39 52 55 65 67 69 80 82 85 96
10 30 35
20 44 60
70 90 50
5) Split-Operation und fertig!
45
Beispiel 3: Erzeugen eines neuen Knotens bei einer Einfüge-Operation
20 40 50
50 10 20
10 wird gesucht und eingefügt.
Überlauf!
10 20 40 50
Split-Operation anwenden, da nicht genügend Platz.
Dabei wird ein neuer Knoten erzeugt.
40
B-Baum mit genau einem Knoten
mit 3 Schlüsseln
Löschen in B-Bäumen
V remove(K key, Node<K,V> p) { suche Schlüssel key im Baum;
if (key nicht gefunden) return null;
if (key befindet sich im Blatt)
speichere Daten in oldValue und lösche key;
else
speichere Daten in oldValue und ersetze key (mit value)
durch nächst größeren Schlüssel key’ (mit value') und lösche key’;
// key’ befindet sich im rechten Teilbaum von key ganz links in einem Blatt for (alle Knoten k vom aktuellen Knoten bis zur Wurzel) {
if ( Schlüsselunterlaufbei k) {
if (Geschwisterknoten kann Schlüssel abgeben)
Übernahme von Schlüsseln vom Geschwisterknoten;
else
Verschmelze Knoten k mit einem Geschwisterknoten;
} else
§ Löschen immer in einem Blatt.
§ Schlüsselunterlauf, falls
Anzahl der Schlüssel kleiner als ém/2ù -1.
§ remove kann wie bei AVL- Bäumen rekursiv realisiert werden.
Die Behandlung der Schlüsselunterläufe geschieht dann beim rekursiven Aufstieg.
§ Hat die Wurzel genau 2
Kinder, die verschmolzen
werden müssen, dann wird
die Wurzel leer und muss
entfernt werden (siehe
Behandlung eines Schlüsselunterlaufs: Übernahme von Schlüssel
Aktueller Knoten;
Schlüsselmenge ist zu klein
k1 k2 Elternknoten
Rechter Geschwisterknoten
§ Prüfe zuerst, ob linker Geschwisterknoten Schlüssel abgeben kann. Falls nicht, dann prüfe rechten Knoten.
§ Abgabe von Schlüsseln, so dass Knoten in etwa gleich groß werden.
§ Verfahren kann dann abgebrochen werden,
da im Elternknoten kein Schlüsselunterlauf entstehen kann.
Linker
Geschwisterknoten
Übernahme von Schlüssel (hier vom linken Geschwisterknoten)
Aktueller Knoten
Elternknoten
... ...
Rechter Geschwisterknoten
Linker Geschwisterknoten gibt dem aktuellen Knoten gerade soviel Schlüssel (und Kindzeiger) ab, so dass sich die Knotenanzahl um maximal 1 unterscheidet.
... g1 g2 ...
Linker Geschwisterknoten
... g2 ... k1 ... ...
... k1 k2 ...
g1 k2
... ... ... ...
... ...
... ...
Behandlung eines Schlüsselunterlaufs: Verschmelzung
Aktueller Knoten;
Schlüsselmenge ist zu klein
k1 k2 Elternknoten
Rechter Geschwisterknoten Linker
Geschwisterknoten
§ Beide Geschwisterknoten haben zu wenig Schlüssel, um Schlüssel abgeben zu können.
§ Führe daher Verschmelzung mit einem Geschwisterknoten durch.
§ Fasse aktuellen Knoten mit linkem Geschwisterknoten (falls vorhanden) zusammen.
Sonst fasse mit rechtem Geschwisterknoten zusammen.
§ Bei Elternknoten wird ein Schlüssel nach unten verschoben.
Daher kann nun beim Elternknoten ein Schlüsselunterlauf vorkommen.
Führe daher Verfahren gegebenenfalls rekursiv beim Elternknoten fort.
Verschmelzung (hier mit linkem Geschwisterknoten)
Aktueller Knoten
Elternknoten
Rechter Geschwisterknoten
Fasse aktuellen Knoten mit
linkem Geschwisterknoten zusammen Linker Geschwisterknoten
... k1 k2 ...
..
. ..
.
... k2 ...
... ...
...
... ...
Beispiel 1: Löschen von 96
10 30 35
20 44 60
70 90
11 12 15
5 7 21 24 33 38 39 45 52 55 65 67 69 80 82 85 96 50
1) Suchen und
Löschen von 96
führt zu Unterlauf
Beispiel 1: Löschen von 96 (Fortsetzung)
10 30 35
20 44 60
70 90
11 12 15
5 7 21 24 33 38 39 45 52 55 65 67 69 80 82 85
50
2) Linker Geschwisterknoten kann Schlüssel abgeben.
Fertig!
10 30 35
20 44 60
70 85 50
Beispiel 2: Löschen von 80
1) Suchen und Löschen von 80 führt zu Unterlauf
10 30 35
20 44 60
70
11 12 15
5 7 21 24 33 38 39 51 52 53 65 80
50 55
58
Beispiel 2: Löschen von 80 (Fortsetzung)
2) Verschmelzung mit linkem Geschwisterknoten
führt zu weiterem Unterlauf.
10 30 35
20 44 60
10 30 35
20 44 60
70
11 12 15
5 7 21 24 33 38 39 51 52 53 65
50 55
58
50 55
Beispiel 2: Löschen von 80 (Fortsetzung)
3) Linker Geschwisterknoten kann Schlüssel abgeben.
Fertig!
10 30 35
20 44 60
11 12 15
5 7 21 24 33 38 39 51 52 53 65 70
50 55
58
10 30 35
20 44 55
50 60
Beispiel 3: Löschen von 60
1) Suchen und Löschen von 60
2) Schüssel 60 befindet sich nicht in Blatt: daher durch nächst größeren Schlüssel 65 ersetzen und Schlüssel 65 löschen.
3) Kein Schlüsselunterlauf, daher fertig!
10 30 35
20 44 60
70 85
11 12 15
5 7 21 24 33 38 39 45 52 55 65 67 69 80 82 90
50
10 30 35
20 44 65
70 85 50
50
Beispiel 4: Wurzel muss entfernt werden
40
20
1) Suchen und Löschen von 50
2) Schlüsselunterlauf;
Verschmelzung mit Geschwisterknoten
20 40
3) Schlüsselunterlauf in der Wurzel;
Wurzel entfernen
20 40
Analyse (1)
Höhe eines B-Baumes mit n Schlüsseln:
§ Im schlechtesten Fall hat die Wurzel 2 Kinder und jeder andere Nicht-Blatt- Knoten ém/2ù viele Kinder.
§ Damit ergibt sich eine maximale Höhe von (siehe [Ottmann u. Widmayer]):
h ≤ log
ém/2ù(n+1)/2
Beispiel:
§ Für m = 256 und n = 10
9ergibt sich eine maximale Höhe von h £ 4.1
Aufwand für Suchen, Einfügen und Löschen:
§ Da die Höhe eines B-Baums durch log
ém/2ù(n+1)/2 beschränkt ist, ergibt sich eine maximale Laufzeit von:
T(n) = O( log
ém/2ù(n) )
Analyse (2)
Speicherplatzausnutzung:
§ Eine naheliegende Implementierung eines B-Baums sieht für jeden Knoten ein statisches Feld der Größe m für die Schlüssel und Zeiger auf die Kinder vor.
§ Da jeder Knoten (außer Wurzel) zwischen ém/2ù -1 und m-1 viele Schlüssel enthält, stellt sich die Frage, wie groß die Speicherplatzausnutzung ist.
§ Es gilt folgende Eigenschaft (siehe [Ottmann u. Widmayer]):
Wenn eine zufällig gewählte Folge von n Schlüsseln in einen anfangs leeren B-Baum der Ordnung m eingefügt werden, dann ist eine
Speicherplatzausnutzung zu erwarten von:
ln(2) » 69 %.
§ Falls eine absteigend oder aufsteigend sortierte Folge in einen leeren B-Baum einfügt wird, ergibt sich eine besonders schlechte Speicherplatzausnutzung, die aber immer noch bei ca. 50 % liegt.
§ Etwas lindern lässt sich das Problem, indem nicht bei jedem Knotenüberlauf eine
Splittoperation durchgeführt wird, sondern zuvor geprüft wird, ob Schlüssel an
Geschwisterknoten abgegeben werden können.
Anwendungen
Externe Suchverfahren
§ Eine der wichtigsten Anwendungen von B-Bäume sind externe Suchverfahren (z.B.
im Datenbankbereich). Die Menge der Datensätze ist dabei so groß, dass sie nur auf Hintergrundspeicher wie beispielsweise Festplatte abgespeichert werden kann.
§ Die Knotengröße m wird dann so gewählt, dass mit einem Festplattenzugriff die gesamten Daten eines Knotens (d.h. Schlüssel und Zeiger; Indexseite) gelesen werden können.
§ Die Datensätze selbst sind auf der Platte gespeichert.
Beispiel:
§ Wählt man beispielsweise m = 256 und sind ca. n = 10
9Datensätze zu verwalten,
dann ergibt sich ein B-Baum etwa der Höhe 4. Speichert man die beiden obersten
Ebenen des B-Baums (d.h. Wurzel und seine Kinder) im Hauptspeicher (das sind
maximal 255 + 256*255 = 65535 Schlüssel), dann kommt man im schlechtesten Fall
bei einer Suchoperation mit 3 Plattenzugriffe aus.
Variante: B * -Baum
§ Zusätzliche Forderung:
jeder Knoten (außer der Wurzel) muss mindestens zu 2/3 gefüllt sein.
§ Vorteil:
bessere Speicherplatzausnutzung und geringere Höhe des Baumes.
§ Nachteil:
bei den Operationen Einfügen und Löschen kommt ein Überschreiten der Maximal-
bzw. Minimalzahl der Schlüssel je Knoten im Vergleich zum B-Baum häufiger vor.
Variante: B + -Baum
§ Die Datensätze werden nur in den Blättern abgespeichert.
§ Um die Bereichssuche zu unterstützen (finde alle Datensätze mit Schlüssel k Î [k1, k2]), werden die Blätter miteinander verkettet.
§ In den Nicht-Blatt-Knoten stehen nur Schlüssel und Zeiger zu Navigationszwecken.
Der i-te Schlüssel k
ibefindet sich noch zusätzlich als kleinster Schlüssel im Teilbaum B
i+1§ Für die Anzahl der Schlüssel in den Blättern kann eine andere Minimal- und Maximalzahl gelten.
8 48 51
21 66 87
95 97
8 12 15 21 24 51 53 72 75 87 88 89 95 96 97
72
2 4 6 48 66
3. Suchbäume
§ Binäre Suchbäume
- Begriffe und Eigenschaften (Wiederholung PROG 2) - Traversierung (Wiederholung PROG 2)
- Binäre Suchbäume (Wiederholung PROG 2) - Traversierung von Bäumen mit Elternzeiger
§ AVL-Bäume
§ B-Bäume
§ 2-3-4-Bäume und Rot-Schwarzbäume
2-3-4-Bäume
§ B-Bäume der Ordnung 4 nennt man auch 2-3-4-Bäume.
Ein Nicht-Blatt-Knoten hat 2, 3 oder 4 Kinder.
10 30 35 50
20 60
70 90
11 12 15
5 7 21 24 33 38 43 45 52 55 65 67 69 80 85 96
§ 2-3-4-Bäume sind eine sehr gut geeignete Alternative zu AVL-Bäumen.
§ 2-3-4-Bäume lassen sich auch als Top-Down-Variante realisieren:
beim Einfügen bzw. Löschen wird der Baum mit einer Schleife von der Wurzel in
Richtung der Blätter durchlaufen.
2-3-4-Bäume und Rot-Schwarz-Bäume (1)
k1 k2 k3
k1 k2 oder
§ Eine beliebte Implementierungsvariante von 2-3-4-Bäumen sind Rot-Schwarz-Bäume, die mit einer wesentlich einfacheren Datenstruktur auskommen (wie bei binären
Suchbäumen) und außerdem top-down realisiert werden können.
Beispiel: Java-Collections und STL-Bibliothek von C++.
§ Rot-Schwarz-Bäume sind binäre Suchbäume, dessen Knoten entweder rot oder schwarz sind und für die bestimmte Färbungsregeln gelten.
§ Ein 2-3-4-Baum wird in ein Rot-Schwarz-Baum transformiert, indem
3-Knoten (Knoten mit 3 Schlüsseln) und 2-Knoten mittels einer einfachen Regel ersetzt werden. 1-Knoten bleiben unverändert.
k3 k1
k1 k2
k2
k2 k1
2-3-4-Bäume und Rot-Schwarz-Bäume (2)
15 50 60 70
30
5 10 20 40 45 55 65 80 85 90
Beispiel
5
15
10 20
30
60
40 55 65 85
50 70
Färbungsregeln von Rot-Schwarz-Bäumen
Ein Rot-Schwarz-Baum ist ein binärer Suchbaum mit folgenden Färbungsregeln:
(1) Jeder Knoten ist entweder rot oder schwarz.
(2) Die Wurzel ist immer schwarz.
(3) Ein roter Knoten darf kein rotes Kind haben.
(4) Jeder Pfad von der Wurzel zu einem Blatt hat die die gleiche Anzahl an schwarzen Knoten.
Bemerkung:
§ Durch die 4 Färbungsregeln wird gewährleistet, dass ein Rot-Schwarz Baum mit den Transformationsregeln von Seite 3-82 auch wieder
einem 2-3-4 Baum entspricht.
§ Rot-Schwarz-Bäume werden mit derselben Datenstruktur wie binäre
Suchbäume realisiert. Jedoch wird zusätzlich ein Bit für die Farbe
(rot oder schwarz) verwendet.
Einfügen eines Schlüssels in einen Rot-Schwarz-Baum (1)
§ durchlaufe Rot-Schwarz-Baum rsb mit einer Schleife von seiner Wurzel bis zu
demjenigen Blatt, unter dem der neue Knoten mit Schlüssel key eingefügt werden soll;
§ füge neuen roten Knoten mit Schlüssel key ein;
q = Wurzel des Baums rsb;
while (q ist nicht das Blatt, unter dem key eingefügt werden soll) { evtl. Farbwechsel (FW);
evtl. eine der Rotationsoperationen R, LR, L, RL;
if (key < q.key) q = q.left;
else if(key > q.key) q = q.right;
else
// Schlüssel bereits vorhanden; mache nichts;
}
if (key < q.key)
q.left = newRedNode(key);
else
q.right = newRedNode(key);
evtl. eine der Rotationsoperationen R, LR, L, RL;
§ da der neu eingefügte Knoten rot ist, ist die Einhaltung der Regel (4) gewährleistet
Einfügen eines Schlüssels in einen Rot-Schwarz-Baum (2)
c2 c1
q
c2 c1
q
§ Wende auf Knoten q, der zwei rote Kinder hat, einen Farbwechsel FW durch.
Falls q die Wurzel ist, dann bleibt q schwarz.
FW
§ Falls durch den Farbwechsels (FW) oder durch Einfügen eines neuen roten Knotens zwei hintereinanderfolgende rote Knoten entstehen, dann wird
eine der Rotationsoperationen R, LR, L oder RL durchgeführt.
C
Rotationsoperation R
s p
g
B A
q
R
g p
B
C A s
q
§ Rotation wird um Knoten p nach rechts durchgeführt.
p (parent) und g (grandparent) werden dabei umgefärbt.
§ Der Knoten s (Geschwister von p vor Anwendung der Rotation) muss
schwarz sein. (Begründung in [Weiss 2011])
C
Rotationsoperation LR
s p
g
B1 B2
q A
p g
q
B1 B2 A
C s
§ Rotation wird um Knoten p nach links und dann um q nach rechts durchgeführt. q und g werden dabei umgefärbt.
§ Der Knoten s (Geschwister von p vor Anwendung der Rotation) muss schwarz sein. (Begründung in [Weiss 2011])
Daher keine weitere Verletzung der Farbregel.
(2) R
(1) L
C
Rotationsoperation L
s p
g
B
A q
L
q p
B s C
A g
§ Rotation wird um Knoten p nach links durchgeführt.
p (parent) und g (grandparent) werden dabei umgefärbt.
§ Der Knoten s (Geschwister von p vor Anwendung der Rotation) muss
schwarz sein. (Begründung in [Weiss 2011])
C
Rotationsoperation RL
s p
g
B1 B2
q A
g p
q
B1 B2 A
C s
§ Rotation wird um Knoten p nach rechts und dann um q nach links durchgeführt. q und g werden dabei umgefärbt.
§ Der Knoten s (Geschwister von p vor Anwendung der Rotation) muss schwarz sein. (Begründung in [Weiss 2011])
Daher keine weitere Verletzung der Farbregel.
(1) R (2) L
Beispiel (1)
5
15
10 20
30
70
55
50 65 90
60 85
40
80
§ Einfügen von 45.
§ Der Baum wird bei der Wurzel 30 beginnend top down durchlaufen:
30, 70, 60, 50.
§ Bei 50 wird ein Farbwechsel FW durchgeführt.
5
15
10 20
30
70
55
50 65 90
60 85
40
80
Beispiel (2)
§ Durch den Farbwechsel wird eine Rechtsrotation R notwendig
5
15
10 20
30
70
55
50 65 90
60 85
40
80 5
15
10 20
30
60
40 55
50 70
65
90 85 80
Beispiel (3)
§ Der Durchlauf kann bei Knoten 50 fortgesetzt werden. Dadurch wird als nächstes Knoten 40 besucht.
§ Der neue Knoten 45 kann rechts von 40 als roter Knoten eingefügt werden.
§ Fertig!
5
15
10 20
30
60
40 55
50 70
65
90 85 80
5
15
10 20
30
60
40 55
50 70
65
90 85 80 45