Anhang: AVL-Bäume & Heaps
Die Sweep-Algorithmen brauchen zwei Datenstrukturen: einen balancierten Suchbaum für den Status und eine Prioritätswarteschlange für die Ereignisse. Die Folien wiederholen dafür AVL-Bäume und Heaps (Folien 45–73).
AVL-Bäume
Definitionen (Folie 45)
- Ein Suchbaum ist balanciert, wenn seine Höhe ist. Dann laufen alle Wörterbuch-Operationen (Suchen, Einfügen, Löschen) in .
- Ein binärer Suchbaum ist ein AVL-Baum, wenn sich in jedem Knoten die Höhen der beiden Teilbäume um höchstens 1 unterscheiden.
- Ein AVL-Baum hat höchstens Höhe ca. .
- Name: Adelson-Velskij und Landis (1962).
Balance eines Knotens = Höhe(rechts) − Höhe(links). Ein leerer Teilbaum hat Höhe , ein Blatt Höhe . Im AVL-Baum ist die Balance überall .
Rebalancieren durch Rotationen (Folien 47–51)
Nach Einfügen oder Löschen kann irgendwo Balance entstehen. Vier Fälle:
| Fall | Situation | Rotation |
|---|---|---|
| A1 | linkslastig (), linkes Kind hat Balance oder | Rechtsrotation |
| A2 | linkslastig (), linkes Kind hat Balance | Links-Rechts (Doppelrotation) |
| B1 | rechtslastig (), rechtes Kind hat Balance oder | Linksrotation |
| B2 | rechtslastig (), rechtes Kind hat Balance | Rechts-Links (Doppelrotation) |
Merkregel: Zeigen Knoten und Kind in dieselbe Richtung (beide links- oder beide rechtslastig), reicht eine einfache Rotation. Zeigen sie in entgegengesetzte Richtungen („Knick“), braucht man eine Doppelrotation: erst das Kind gerade ziehen, dann den Knoten rotieren.
Prinzip (Folie 52)
- Einfügen / Löschen wie im normalen binären Suchbaum.
- Vom geänderten Knoten nach oben zur Wurzel laufen und unbalancierte Teilbäume per Rotation reparieren.
- Beim Einfügen reicht höchstens eine (ggf. doppelte) Rotation.
- Beim Löschen können mehrere Rotationen entlang des Pfads nötig sein.
Code (Folien 55–60)
struct Node { int height; KeyType key; ValueType val; Node *left, *right; };
int getHeight(const Node* p) { return p ? p->height : -1; }
int getBalance(const Node* p) { return p ? getHeight(p->right) - getHeight(p->left) : 0; }
void rotateRight(Node*& p) { // Vor.: p->left != 0
Node* q = p->left;
p->left = q->right;
q->right = p;
p->height = std::max(getHeight(p->left), getHeight(p->right)) + 1;
q->height = std::max(getHeight(q->left), getHeight(q->right)) + 1;
p = q;
}
// rotateLeft analog
void rotateLeftRight(Node*& p) { rotateLeft(p->left); rotateRight(p); }
void rotateRightLeft(Node*& p) { rotateRight(p->right); rotateLeft(p); }
void balance(Node*& p) {
if (!p) return;
p->height = std::max(getHeight(p->left), getHeight(p->right)) + 1;
if (getBalance(p) == -2) {
if (getBalance(p->left) <= 0) rotateRight(p); // A1
else rotateLeftRight(p); // A2
} else if (getBalance(p) == +2) {
if (getBalance(p->right) >= 0) rotateLeft(p); // B1
else rotateRightLeft(p); // B2
}
}
bool insert(KeyType key, ValueType& v, Node*& p) {
bool result;
if (!p) { p = new Node{0, key, v, nullptr, nullptr}; result = true; }
else if (key < p->key) result = insert(key, v, p->left);
else if (key > p->key) result = insert(key, v, p->right);
else result = false; // Schlüssel existiert schon
if (result) balance(p); // auf dem Rückweg rebalancieren
return result;
}
| Operation | Laufzeit |
|---|---|
Rotationen, balance | |
search, insert, remove |
Heaps
Definition (Folien 62–63)
(Max-)Heap-Eigenschaft: In jedem Knoten gilt . (Min-Heap analog mit .)
Ein Heap ist ein Binärbaum, der
- in jedem Knoten die Heap-Eigenschaft erfüllt,
- auf allen Ebenen außer der letzten vollständig gefüllt ist,
- dessen letzte Ebene linksbündig gefüllt ist.
Folgen (Folie 64): Das Maximum steht in der Wurzel, und die Höhe ist .
Unterschied zum Suchbaum: Der Suchbaum kann beliebige Schlüssel schnell finden, der Heap nur das Maximum (bzw. Minimum). Das reicht für die Ereignisstruktur, denn man braucht immer nur das nächste Ereignis.
Als Array (Folie 65)
Ein Heap lässt sich in-situ im Array speichern: Die Kinder von stehen in und , der Elternknoten in .
Aufbau: build_heap und heapify (Folien 66–71)
void heapify(Node a[], int n, int i) { // a[1..n]
int max = i, li = 2 * i, re = li + 1;
if (li <= n && a[li].key > a[max].key) max = li;
if (re <= n && a[re].key > a[max].key) max = re;
if (i != max) { std::swap(a[i], a[max]); heapify(a, n, max); }
}
void build_heap(Node a[], int n) {
for (int i = n / 2; i > 0; --i) heapify(a, n, i); // a[n/2+1..n] sind Blätter
}
heapify(i) setzt voraus, dass beide Teilbäume von schon Heaps sind. Ist kleiner als ein Kind, wird es mit dem größeren Kind getauscht und sinkt weiter nach unten. Deshalb läuft build_heap von unten nach oben.
Laufzeit (Folien 72–73)
heapify: Rekursion (der linke Teilbaum hat höchstens Knoten, wenn die letzte Ebene genau halb voll ist). Mit dem Master-Theorem: . Alternativ: für einen Knoten der Höhe .build_heap: Die naive Abschätzung ist zu grob. Es gibt höchstens Knoten der Höhe , also weil .
Einen Heap baut man also in linearer Zeit, Einfügen und Maximum-Entfernen kosten je .