h ¡¢£¡¤¥¢£¦ §¨aume, Heaps

Werbung
D äume, Heaps
Alle bisher betrachteten Strukturen waren linear in dem Sinn, dass jedes Element höchstens einen Nachfolger hat. In einem Baum kann jedes Element keinen, einen oder beliebig viele Nachfolger haben. Bäume
sind wichtig als Strukturen in der Informatik, da sie auch oft im Alltag auftauchen: zum Darstellen von
Abhängigkeiten oder Strukturen, als Organigramme von Firmen, als Familienstammbaum, aber auch
zum Beschleunigen der Suche.
Definition: Ein Graph ist definiert als ein Paar B = (E, K) bestehend aus je einer endlichen Menge E
von Ecken (Knoten, Punkten) und einer Menge von Kanten. Eine Kante wird dargestellt als Zweiermenge
von Ecken {x, y}, den Endpunkten der Kante.
Ein Baum ist ein Graph mit der zusätzliche Einschränkung, dass es zwischen zwei Ecken nur eine (direkte oder indirekte) Verbindung gibt1 .
Wir befassen uns hier zuerst vor allem mit einer besonderen Art von Bäumen: den Binärbaumen. Ein
Baum heisst binär, falls jeder Knoten höchstens zwei Nachfolger hat.
1 Ein Baum ist ein zusammenhängender Graph ohne Zyklen.
4
4 Datentypen: Bäume, Heaps
Ein binärer Baum besteht aus einer Wurzel (Root) und (endlich vielen) weiteren Knoten und
verbindenden Kanten dazwischen. Jeder Knoten hat entweder keine, ein oder zwei Nachfolgerknoten.
Ein Weg in einem Baum ist eine Liste von disjunkten, direkt verbunden Kanten. Ein binärer Baum ist
vollständig (von der Höhe n), falls alle inneren Knoten zwei Nachfolger haben und die Blätter maximal
Weglänge n bis zur Wurzel haben.
Jedem Knoten ist eine Ebene (level) im Baum zugeordnet. Die Ebene eines Knotens ist die Länge des
Pfades von diesem Knoten bis zur Wurzel. Die Höhe (height) eines Baums ist die maximale Ebene, auf
der sich Knoten befinden.
Ein binärer Baum besteht also aus Knoten mit einem (Zeiger auf ein) Datenelement data , einem linken
Nachfolgerknoten left und einem rechten Nachfolgerknoten right .
left
p !"# $
protected T data;
protected BinaryTreeNode<T> leftChild;
protected BinaryTreeNode<T> rightChild;
right
4%
public BinaryTreeNode(T item){ data=item; }
// tree traversals
public BinaryTreeNode<T> inOrderFind(T item) { . . . }
public BinaryTreeNode<T> postOrderFind(T item) { . . . }
public BinaryTreeNode<T> preOrderFind(T item) { . . .}
// getter and setter methods
. . .
public class BinaryTree<T> {
protected BinaryTreeNode<T> rootTreeNode;
public BinaryTree(BinaryTreeNode<T> root) {
this.rootTreeNode = root;
}
// tree traversals
public BinaryTreeNode<T> inOrderFind(T item) {
return rootTreeNode.inOrderFind(item);
}
public BinaryTreeNode<T> preOrderFind(T item) { ... }
public BinaryTreeNode<T> postOrderFind(T item) { ... }
public BinaryTreeNode<T> postOrderFindStack(T item) { ... }
//getter and setter methods
. . .
44
4 Datentypen: Bäume, Heaps
4.1 Baumdurchläufe
Bäume können auf verschiedene Arten durchlaufen werden. Die bekanntesten Verfahren sind Tiefensuche (depth-first-search, DFS) und Breitensuche (breadth-first-search, BFS). Tiefensuche kann unterschieden werden in die drei Typen präorder, postorder und inorder, abhängig von der Reihenfolge der
rekursiven Aufrufe.
4.1.1 Tiefensuche
Präorder
• Betrachte zuerst den Knoten (die Wurzel des Teilbaums),
• durchsuche dann den linken Teilbaum,
• durchsuche zuletzt den rechten Teilbaum.
Inorder
• Durchsuche zuerst den linken Teilbaum,
• betrachte dann den Knoten,
• durchsuche zuletzt den rechten Teilbaum.
Postorder
• Durchsuche zuerst den linken Teilbaum,
• durchsuche dann den rechten Teilbaum,
• betrachte zuletzt den Knoten.
4&'
Baumdurchläufe
4-5
Präorder
Inorder
Postorder
W() betrachten als Beispiel für die Tiefensuche den Präorder-Durchlauf.
public BinaryTreeNode<T> preOrderFind(T item) {
if (data.equals(item))
return this;
if (leftChild != null) {
BinaryTreeNode<T> result = leftChild.preOrderFind(item);
if (result != null)
return result;
}
if (rightChild != null) {
BinaryTreeNode<T> result = rightChild.preOrderFind(item);
if (result != null)
return result;
}
4*
4 Datentypen: Bäume, Heaps
return null;
}
4.1.2 Tiefensuche mit Hilfe eines Stacks
Mit Hilfe eines Stacks können wir die rekursiven Aufrufe in der präorder Tiefensuche vermeiden. Auf dem
Stack werden die später zu behandelnden Baumknoten zwischengespeichert.
public BinaryTreeNode<T> preOrderFindStack(T item) {
Stack<BinaryTreeNode<T>> stack = new Stack<BinaryTreeNode<T>>();
stack.push(this.rootTreeNode);
while (!stack.isEmpty()) {
BinaryTreeNode<T> tmp = stack.pop();
if (tmp.getData().equals(item))
return tmp;
if (tmp.getRightChild() != null)
stack.push(tmp.getRightChild());
if (tmp.getLeftChild() != null)
stack.push(tmp.getLeftChild());
}
return null;
}
4&'
Baumdurchläufe
4-7
4.1.3 Breitensuche mit Hilfe einer Queue
Bei der Breitensuche besucht man jeweils nacheinander die Knoten der gleichen Ebene:
• Starte bei der Wurzel (Ebene 0).
• Bis die Höhe des Baumes erreicht ist, setze den Level um eines höher und gehe von links nach
rechts durch alle Knoten dieser Ebene.
Levelorder
B+( diesem Verfahren geht man nicht zuerst in die Tiefe, sondern betrachtet von der Wurzel aus zuerst
alle Elemente in der näheren Umgebung. Um mittels Breitensuche (levelorder) durch einen Baum zu
wandern, müssen wir uns alle Baumknoten einer Ebene merken. Diese Knoten speichern wir in einer
Queue ab, so dass wir später darauf zurückgreifen können.
public BinaryTreeNode<T> levelOrderFind(T item) {
QueueImpl<BinaryTreeNode<T>> queue = new QueueImpl<BinaryTreeNode<T>>();
queue.add(rootTreeNode);
while (!queue.isEmpty()) {
BinaryTreeNode<T> tmp = queue.poll();
if (tmp.getData().equals(item))
return tmp;
if (tmp.getLeftChild() != null)
queue.add(tmp.getLeftChild());
if (tmp.getRightChild() != null)
queue.add(tmp.getRightChild());
}
4,
4 Datentypen: Bäume, Heaps
return null;
}
4.2 Binäre Suchbäume
Ein binärer Suchbaum ist ein Baum, welcher folgende zusätzliche Eigenschaft hat:
Alle Werte des linken Nachfolger-Baumes eines Knotens K sind kleiner, alle Werte des rechten
Nachfolger-Baumes von K sind grösser als der Wert von K selber.
Der grosse Vorteil von binären Suchbäumen ist, dass wir sowohl beim Einfügen als auch beim Suchen
von Elementen immer bloss einen der zwei Nachfolger untersuchen müssen. Falls der gesuchte Wert
kleiner ist als der Wert des Knotens, suchen wir im linken Teilbaum, anderenfalls im rechten Teilbaum
weiter.
Beispiel: Die folgenden zwei Bäume entstehen durch Einfügen der Zahlen 37, 43, 53, 11, 23, 5, 17, 67,
47 und 41 in einen leeren Baum. Einmal werden die Zahlen von vorne nach hinten eingefügt, das zweite
Mal von hinten nach vorne.
41
37
11
5
23
17
17
43
41
5
53
47
67
47
23
11
67
43
37
53
4&
Binäre Suchbäume
public class BinarySearchTreeNode <T extends Comparable<T>> {
public void add(T item) {
int compare = data.compareTo(item);
if (compare > 0) {
// (data > item)?
if (leftChild == null)
leftChild = new BinarySearchTreeNode<T>(item);
else
leftChild.add(item);
// left recursion
} else {
// (item >= data)
if (rightChild == null)
rightChild = new BinarySearchTreeNode<T>(item);
else
rightChild.add(item); // right recursion
}
}
public BinarySearchTreeNode<T> find(T item) {
int compare = data.compareTo(item);
if (compare == 0)
return this;
if (compare > 0 && leftChild != null) // data > item
return leftChild.find(item);
if (compare < 0 && rightChild != null) // data < item
return rightChild.find(item);
return null;
}
. . .
}
4-9
4'-
4 Datentypen: Bäume, Heaps
4.3 B-Bäume
Ein B-Baum ist ein stets vollständig balancierter und sortierter Baum. Ein Baum ist vollständig balanciert,
wenn alle Äste gleich lang sind. In einem B-Baum darf die Anzahl Kindknoten variieren. Ein 3-4-5 BBaum ist zum Beispiel ein Baum, in welchem jeder Knoten maximal 4 Datenelemente speichern und
jeder Knoten (ausser der Wurzel und den Blättern) minimal 3 und maximal 5 Nachfolger haben darf (der
Wurzelknoten hat 0-4 Nachfolger, Blätter haben keine Nachfolger).
Durch die flexiblere Anzahl Kindknoten ist das Rebalancing weniger häufig nötig.
Ein Knoten eines B-Baumes speichert:
•
•
•
•
eine variable Anzahl s von aufsteigend sortierten Daten-Elementen k1 , . . . , ks
eine Markierung isLeaf, die angibt, ob es sich bei dem Knoten um ein Blatt handelt.
s + 1 Referenzen auf Kindknoten, falls der Knoten kein Blatt ist.
Jeder Kindknoten ist immer mindestens zur Hälfte gefüllt.
Die letzte Bedingung lautet formal: es gibt eine Schranke m, so dass m <= s <= 2m gilt. Das heisst,
jeder Kindknoten hat mindestens m, aber höchstens 2m Daten-Elemente.
Die Werte von k1 , . . . , ks dienen dabei als Splitter. Die Daten-Elemente der Kindknoten ganz links müssen
kleiner sein als k1 , diejenigen ganz rechts grösser als ks . Dazwischen müssen die Daten-Elemente des
i-ten Kindes grösser als ki und kleiner als ki+1 sein.
Das folgende Bild zeigt einen B-Baum mit m gleich 2. Jeder innere Knoten hat also mindestens 2 und
maximal 5 Nachfolger.
4&%
B-Bäume
4-11
Operationen in B-Bäumen
Suchen
Die Suche nach einem Datenelement e läuft in folgenden Schritten ab: Beginne bei der Wurzel als aktuellen Suchknoten k.
• Suche in k von links her die Position p des ersten Daten-Elementes x, welches grösser oder gleich e
ist.
•
•
•
•
Falls alle Daten-Elemente von k kleiner sind als e, führe die Suche im Kindknoten ganz rechts weiter.
Falls x gleich e ist, ist die Suche zu Ende.
Anderfalls wird die Suche beim p-ten Kindelement von k weitergeführt.
Falls k ein Blatt ist, kann die Suche abgebrochen werden (fail).
Einfügen
Beim Einfügen muss jeweils beachtet werden, dass nicht mehr als 2m Daten-Elemente in einem Knoten
untergebracht werden können.
Zunächst wird das Blatt gesucht, in welches das neue Element eingefügt werden müsste. Dabei kann
gleich wie beim Suchen vorgegegangen werden, ausser dass wir immer bis zur Blatt-Tiefe weitersuchen
(sogar, wenn wir den Wert unterwegs gefunden haben). Falls es in dem gesuchten Blatt einen freien
Platz hat, wird der Wert dort eingefügt.
Einfügen des Werts 31 in den folgenden Baum:
4'
4 Datentypen: Bäume, Heaps
Der Wert 31 sollte in das Blatt (30,34,40,44) eingefügt werden. Dieses ist aber bereits voll, muss also
aufgeteilt werden. Dies führt dazu, dass der Wert in der Mitte (34) in den Vorgänger- Knoten verschoben
wird. Da das alte Blatt ganz rechts vom Knoten (20,28) liegt, wird der Wert 34 rechts angefügt (neuer,
grösster Wert dieses Knotens). Damit erhält dieser Knoten neu 3 Werte und 4 Nachfolger.
.(+/+) Prozess muss eventuell mehrmals (in Richtung Wurzel) wiederholt werden, falls durch das Hochschieben des Elements jeweils der Vorgänger-Knoten ebenfalls überläuft.
4&%
B-Bäume
4-13
Löschen von Elementen
Beim Löschen eines Elementes muss umgekehrt beachtet werden, dass jeder Knoten nicht weniger als
m Datenelemente enthalten muss.
Falls das gelöschte Element in einem Blatt liegt, welches mehr als m Datenelemente hat, kann das
Element einfach gelöscht werden. Andernfalls können entweder Elemente vom benachbarte Blatt verschoben oder (falls zu wenig Elemente vorhanden sind) zwei Blätter verschmolzen werden.
Verschiebung Aus dem linken B-Baum soll das Element 18 gelöscht werden. Dies würde dazu führen,
dass das linke Blatt zu wenig Datenelemente hat. Darum wird aus dem rechten Nachbarn das kleinste
Element nach oben, und das Splitter-Element des Vorgängers in das linke Blatt verschoben.
Analog könnte (falls vorhanden) aus einem linken Nachbarn das grösste Element verschoben werden.
F011/ +(2 31+5+26 +(2+/ (22+)+2 7286+2/ 9:;B; <0/ 31+5+26 =>? @+1öscht wird, muss entweder von den
linken Nachfolgern das grösste, oder von den rechten Nachfolgern das kleinste Element nach oben verschoben werden, damit weiterhin genügend Elemente (als Splitter) vorhanden sind, und die Ordnung
bewahrt wird.
4'4
4 Datentypen: Bäume, Heaps
Verschmelzung Aus dem linken B-Baum soll das Element 60 gelöscht werden. Dies würde dazu führen,
dass das mittlere Blatt zu wenig Datenelemente hat. Weder der rechte noch der linke Nachbar hat
genügend Elemente, um eine Verschiebung durch zu führen - es müssen zwei Blätter verschmolzen
werden.
.0/ linke Blatt erhält vom mittleren Blatt das Element 55, sowie von der Wurzel das Element 50. Die
Wurzel muss ebenfalls ein Element abgeben, da nach der Verschmelzung bloss noch 2 Nachfolge-Knoten
existieren. Das rechte Blatt bleibt unverändert.
Mit Hilfe der Verschiebung- und Verschmelzungs-Operation können wir nun beliebige Elemente aus einem B-Baum löschen.
Beispiel
Aus dem folgenden Baum löschen wir zuerst das Element 75, danach das Element 85:
4&4
Priority Queues
4-15
4.4 Priority Queues
In vielen Applikationen will man die verschiedenen Elemente in einer bestimmten Reihenfolge (Priorität)
abarbeiten. Allerdings will man das (aufwändige!) Sortieren dieser Elemente nach möglichkeit vermeiden.
Eine der bekanntesten Anwendungen in diesem Umfeld sind Scheduling-Algorithmen mit Prioritäten. Alle
Prozesse werden gemäss ihrer Priorität in einer Priority Queue gesammelt, so dass immer das Element
mit höchster Priorität verfügbar ist. Priority Queues haben aber noch weit mehr Anwendungen, zum
Beispiel bei Filekomprimierungs- oder bei Graph-Algorithmen.
Eine elegante Möglichkeit der Implementierung einer Priority Queue ist mit Hilfe eines Heaps.
4.4.1 Heaps
Ein Heap ist ein (fast) vollständiger Baum, in welchem nur in der untersten Ebene ganz rechts Blätter
fehlen dürfen.
65
56
52
37
25
48
31
18
45
6
3
15
4'*
4 Datentypen: Bäume, Heaps
Definition: [Heap] Ein Heap ist ein vollständiger binärer Baum, dem nur in der untersten Ebene ganz
rechts Blätter fehlen dürfen mit folgenden Zusatzeigenschaften.
1. Jeder Knoten im Baum besitzt eine Priorität und eventuell noch weitere Daten.
2. Die Priorität eines Knotens ist immer grösser als (oder gleich wie) die Priorität der Nachkommen.
Diese Bedingung heisst Heapbedingung.
Aus der Definition kann sofort abgelesen werden, dass die Wurzel des Baumes die höchste Priorität
besitzt. Weil der Heap im wesentlichen ein vollständiger binärer Baum ist, lässt er sich einfach als Array2
implementieren. Wir numerieren die Knoten des Baumes von oben nach unten und von links nach rechts.
Die so erhaltene Nummerierung ergibt für jeden Knoten seinen Index im Array.
Die dargestellten Werte im Baum sind natürlich bloss die Prioritäten der Knoten. Die eigentlichen Daten
lassen wir der Einfachheit halber weg.
public class Heap<T extends Comparable<T>> {
private List<T> heap;
public Heap() { heap = new ArrayList<T>(); }
public T removeMax() { . . . }
public void insert(T data) { . . . }
private
private
private
private
boolean isLeaf(int position) { . . . }
int parent(int position) { . . . }
int leftChild(int position) { . . . }
int rightChild(int position){ . . . }
2 Dies hat den Nachteil, dass die maximale Anzahl Elemente (size ) beim Erzeugen des Heaps bekannt sein muss.
4&4
Priority Queues
4-17
0 65
2
1 56
3
7
25
4
37
8
31
A
9
18
52
5 45
48
10
6
6
15
11
3
1 2 3 4 5 6 7 8 9 10 11
65 56 52 37 48 45 15 25 31 18 6 3
...
W+)<+2 <(+ 7286+2 0CE <(+/+ W+(/+ (2 <+2 G))0H 0I@+1+@6J /8 @+16+2 Eür alle i, 0 ≤ i < length
folgenden Regeln:
• Der linke Nachfolger des Knotens i befindet sich im Array-Element 2*i+1
≥ heap[ 2*i+1]
• Der rechte Nachfolger des Knotens i befindet sich im Array Element 2*i + 2
Ferner gilt: heap[i] ≥ heap[ 2*i+2]
• Der direkte Vorfahre eines Knotens i befindet sich im Array-Element (i-1)/2
Ferner gilt: heap[i] ≤ heap[ (i-1)/2]
Ferner gilt: heap[i]
Wir sind jetzt in der Lage, die beiden wichtigen Operationen insert und removeMax zu formulieren.
die
4',
4 Datentypen: Bäume, Heaps
insert Da ein Element hinzugefügt werden muss, erhöhen wir zuerst length um eins. Das neue Element
wird dann an der Stelle length-1 eingefügt. Der Array repräsentiert immer noch einen vollständigen
binären Baum mit nur rechts unten fehlenden Blättern. Das neue Element verletzt aber eventuell die
Heapbedingung. Um wieder einen Heap zu erhalten, vertauschen wir das neue Element solange mit
seinen direkten Vorgängern, bis die Heapbedingung wieder erfüllt ist.
Diese Methode verfolgt einen direkten Weg von einem Blatt zur Wurzel. Da der binäre Baum vollständig
ist, hat ein solcher Weg höchstens die Länge der Höhe des Baumes. Mit anderen Worten, wir brauchen
höchstens log2 (n) Vertauschoperationen, um ein Element im Heap einzufügen.
65
55
52
56
37
25
48
31
18
55 45
52
6
3
15
55
55
45
p K ! LM !LN $
heap.add(data);
int crt = heap.size() - 1;
while ((crt != 0)
// heap[crt] > heap[parent(crt)]
&& (heap.get(crt).compareTo(heap.get(parent(crt))) > 0)) {
Collections.swap(heap, crt, parent(crt));
crt = parent(crt);
}
}
}
4&4
Priority Queues
4-19
removeMax Das Element mit der höchsten Priorität befindet sich im Element heap[0] und wird vom
Heap entfernt. heap[0] wird nun mit heap[length-1] überschrieben und length um eins verringert.
Damit erhalten wir wieder einen fast vollständigen binären Baum. Das neue Element heap[0] verletzt
nun vermutlich die Heapbedingung.
Wir vertauschen also heap[0] mit dem grösseren seiner beiden Nachfolger und fahren so fort, bis die
Heapbedingung wieder erfüllt ist.
45
56
65
48 45
56
55
45
37
25
48
31
18
52
6
3
45
65
p O KPQMN $
if (heap.isEmpty()) return null;
Collections.swap(heap, 0, heap.size() - 1);
T element = heap.remove(heap.size() - 1);
if (heap.size() > 1)
siftDown(0);
return element;
}
15
65
4-
4 Datentypen: Bäume, Heaps
private void siftDown(int position) {
while (!isLeaf(position)) {
int j = leftChild(position);
if ((j < heap.size() - 1)
// heap[j] < heap[j+1]
&& (heap.get(j).compareTo(heap.get(j + 1)) < 0)) {
j++;
}
// heap[position] >= heap[j]
if (heap.get(position).compareTo(heap.get(j)) >= 0) {
return;
}
Collections.swap(heap, position, j);
position = j;
}
}
Herunterladen