Binärbaum
Binärbäume sind in der Informatik die am häufigsten verwendete Unterart der Bäume. Im Gegensatz zu anderen Arten von Bäumen können die Knoten eines Binärbaumes nur höchstens zwei direkte Nachkommen haben.
Meist wird verlangt, dass sich die Kindknoten eindeutig in linkes und rechtes Kind einteilen lassen. Ein anschauliches Beispiel für einen solchen Binärbaum ist die Ahnentafel, bei der allerdings die Elternteile durch die Kindknoten zu modellieren sind.
Ein Binärbaum ist entweder leer, oder er besteht aus einer Wurzel mit einem linken und rechten Teilbaum, die wiederum Binärbäume sind. Ist ein Teilbaum leer, bezeichnet man den entsprechenden Kindknoten als fehlend.
Meistens wird die Wurzel in graphischen Darstellungen (wie in der nebenstehenden) oben und die Blätter unten platziert. Entsprechend ist ein Weg von der Wurzel in Richtung Blatt einer von oben nach unten.
Inhaltsverzeichnis
1 Terminologie
2 Weitere Begriffe
3 Abzählungen
4 Anwendungen
4.1 Binärer Suchbaum
4.2 Partiell geordneter Baum
4.3 Vollständiger Binärbaum und vollständig balancierter Binärbaum
4.4 Weitere Binärbäume
5 Repräsentation und Zugriff
5.1 In-Order-Index
5.2 Links/Rechts-Index
5.3 Repräsentation durch ein Array
6 Traversierung
6.1 Rekursive Implementierungen
6.2 Iterative Implementierung
6.3 Abstieg zum ersten oder letzten Element
7 Einfügen, Einfügepunkt
8 Löschen
9 Rotation
9.1 Komplexität
9.2 Doppelrotation
9.3 Rotationsmetrik
10 Umwandeln einer Form eines Binärbaums in eine andere
11 Siehe auch
12 Literatur
13 Weblinks
14 Einzelnachweise
Terminologie |
Die Begriffe Knoten und Kante werden von den Graphen übernommen. Die Kante ist definitionsgemäß gerichtet (auch: Bogen oder Pfeil). Wenn es aus dem Kontext klar genug hervorgeht, wird auch nur von Kante gesprochen.
Bei gerichteten Graphen kann man einem Knoten sowohl Ausgangsgrad wie Eingangsgrad zuordnen. Üblicherweise werden Binärbäume als Out-Trees aufgefasst. In einem solchen gewurzelten Baum gibt es genau einen Knoten, der den Eingangsgrad 0 hat. Er wird als die Wurzel bezeichnet. Alle anderen Knoten haben den Eingangsgrad 1. Der Ausgangsgrad ist die Anzahl der Kindknoten und ist beim Binärbaum auf maximal zwei beschränkt. Damit ist seine Ordnung als Out-Tree ≤ 2.
Knoten mit Ausgangsgrad ≥ 1 bezeichnet man als innere Knoten, solche mit Ausgangsgrad 0 als Blätter oder äußere Knoten. Bei Binärbäumen – und nur dort – findet sich gelegentlich die Bezeichnung Halbblatt für einen Knoten mit Ausgangsgrad 1 (englisch manchmal: non-branching node). Dann ist ein Blatt ein doppeltes Halbblatt.
Weitere Begriffe |
Ein Binärbaum heißt geordnet, wenn jeder innere Knoten ein linkes und eventuell zusätzlich ein rechtes Kind besitzt (und nicht etwa nur ein rechtes Kind), sowie der linke Knoten „kleiner“, der rechte Knoten „größer“ als der Betrachtungsknoten ist. Man bezeichnet ihn als voll, wenn jeder Knoten entweder Blatt ist (also kein Kind besitzt), oder aber zwei (also sowohl ein linkes wie ein rechtes) Kinder besitzt – es also kein Halbblatt gibt. Für die Eigenschaft voll werden gelegentlich auch die Begriffe saturiert oder strikt verwendet. Man bezeichnet volle Binärbäume als vollständig, wenn alle Blätter die gleiche Tiefe haben, wobei die Tiefe eines Knotens als die Anzahl der Bögen bis zur Wurzel definiert ist.
Die Höhe eines gewurzelten Baums ist die maximal auftretende Tiefe. Viele Autoren setzen sie aber um eins höher, da man so dem leeren Baum die Höhe 0 und dem nur aus der Wurzel bestehenden Baum die Höhe 1 geben kann, was gewisse rekursive Definitionen kürzer zu fassen gestattet. (Und da Tiefe ein Attribut eines Knotens, Höhe aber eines des ganzen Baums ist, muss es nicht unbedingt Verwirrungen geben.) In diesem Artikel sei diese letztere Definition durchgehalten.
Der Binärbaum wird entartet genannt, wenn jeder Knoten entweder Blatt ist (Anzahl Kinder ist 0) oder Halbblatt (Anzahl Kinder ist 1). In diesem Fall stellt der Baum eine Liste dar. Besondere Formen sind die geordneten Listen, bei denen ein Baum jeweils nur aus linken oder nur aus rechten Kindern besteht.
Abzählungen |
- So wie ein Baum mit n{displaystyle n} Knoten n−1{displaystyle n-1} Kanten hat, hat ein Binärbaum mit n{displaystyle n} Knoten n−1{displaystyle n-1} Kanten.
- Ein Binärbaum mit n{displaystyle n} Knoten hat n+1{displaystyle n+1} (unmittelbare) Einfügepunkte.
- Ein Binärbaum mit b{displaystyle b} Blättern und c{displaystyle c} Halbblättern hat an jedem Halbblatt einen Einfügepunkt und an jedem Blatt zwei, also 2b+c{displaystyle 2b+c} (unmittelbare) Einfügepunkte.
- Ist i{displaystyle i} die Anzahl der inneren Knoten (einschließlich Wurzel, ohne Halbblätter), so errechnet sich i=b−1{displaystyle i=b-1}.
Anwendungen |
Binärer Suchbaum |
Die in der Praxis wohl wichtigste Anwendung der Binärbäume sind die binären Suchbäume, worunter die AVL-Bäume, Rot-Schwarz-Bäume und Splay-Bäume zu rechnen sind. Bei Suchbäumen gibt es in jedem Knoten „Schlüssel“, nach denen die Knoten „linear“ im Baum geordnet sind. Auf dieser Ordnung basiert dann ein möglichst effizientes Suchen.
Partiell geordneter Baum |
Ein partiell geordneter Baum T ist ein spezieller Baum,
- dessen Knoten markiert sind
- dessen Markierungen aus einem geordneten Wertebereich stammen
- in dem für jeden Teilbaum U mit der Wurzel x gilt: Alle Knoten aus U sind größer markiert als x oder gleich x.
Intuitiv bedeutet dies: Die Wurzel jedes Teilbaumes stellt ein Minimum für diesen Teilbaum dar. Die Werte des Teilbaumes nehmen in Richtung der Blätter zu oder bleiben gleich.
Derartige Bäume werden häufig in Heaps verwendet.
Vollständiger Binärbaum und vollständig balancierter Binärbaum |
In einem vollständigen Binärbaum haben alle Blätter die gleiche Tiefe.
Induktiv lässt sich zeigen, dass ein vollständiger Binärbaum der Höhe h(≥1){displaystyle h;(geq 1)}, den man häufig als Bh{displaystyle B_{h}} bezeichnet, genau
2h−1{displaystyle 2^{h}-1} Knoten,
2h−1−1{displaystyle 2^{h-1}-1} innere Knoten (nicht Blatt, aber evtl. Wurzel),
2t{displaystyle 2^{t}} Knoten in Tiefe t{displaystyle t} für 0≤t≤h−1{displaystyle 0leq tleq h-1}, insbesondere also
2h−1{displaystyle 2^{h-1}} Blätter
besitzt.
Ein vollständig balancierter Binärbaum ist ein voller Binärbaum, bei dem die Abstände von der Wurzel zu zwei beliebigen Blättern um höchstens 1 voneinander abweichen. Ein vollständiger Binärbaum ist ein vollständig balancierter Binärbaum. (Vergleiche Balancierter Baum und AVL-Baum.)
Weitere Binärbäume |
Eine Darstellung eines Binärbaumes, in dem die Knoten mit rechtwinkligen Dreiecken und die Bögen mit Rechtecken dargestellt werden, nennt man pythagoreischen Binärbaum.
Auch Fibonacci-Bäume und binäre Heaps basieren auf Binärbäumen.
Repräsentation und Zugriff |
Die Abbildung zeigt eine naheliegende Art der Speicherung. Sie entspricht in etwa den C-Strukturen:
struct knoten { // 1 Objekt = 1 Knoten
char schlüssel;
struct knoten * links; // linkes Kind
struct knoten * rechts; // rechtes Kind
} object;
struct knoten * anker; // Zeiger auf die Wurzel
Zur besseren Unterscheidung der Objekte sind diese beziehentlich mit den Schlüsseln »F«, »G«, »J« und »P« versehen. Diese Schlüssel sind auch der Einfachheit halber als Ziel der Verweise genommen worden (anstelle von echten Speicheradressen). Wie üblich soll ein Zeigerwert 0 ausdrücken, dass auf kein Objekt verwiesen wird, es also kein Kind an dieser Stelle gibt.
Der große Vorteil des Baums gegenüber dem Array liegt in der flexibleren Speicherverwaltung: Mit dem Entstehen oder Vergehen eines Objektes kann auch der es darstellende Speicher entstehen oder vergehen, wogegen die einzelnen Einträge beim Array fest mit diesem verbunden sind.
In-Order-Index |
Wird in jedem Knoten die Anzahl der Elemente des zugehörigen Unterbaums gehalten, kann das Aufsuchen eines Elements vermöge seines in-order-Index in ganz ähnlicher Weise wie das Aufsuchen mit einem Schlüssel im binären Suchbaum bewerkstelligt werden. Dies hat allerdings die nachteilige Implikation, dass Einfüge- und Löschoperation immer Korrekturen bis hinauf zur Wurzel erfordern, womit sich dann auch die in-order-Indizes von Knoten ändern. Die Vorgehensweise dürfte also bei nicht statischen Binärbäumen von fraglichem Wert sein, und bei statischen ist der gewöhnliche Array-Index in Bezug auf Laufzeit überlegen.
Der Aufwand ist O(h){displaystyle {mathcal {O}}(h)}, wo h{displaystyle h} die Höhe des Baums ist.
Links/Rechts-Index |
Jeder Knoten kann durch eine variabel lange Kette von Binärziffern genau spezifiziert werden.
Die Maßgabe könnte folgendermaßen lauten:
- Eine „0“ am Anfang (gleichzeitig Ende) entspricht dem leeren Binärbaum.
- Eine „1“ am Anfang lässt auf die Wurzel zugreifen.
- Eine anschließende „0“ bzw. „1“ lässt auf das linke bzw. rechte Kind zugreifen.
Jedem Knoten in einem Binärbaum kann so eindeutig eine Binärkette zugeordnet werden.
Bei höhen-balancierten Bäumen ist die Binärkette in ihrer Länge durch O(logn){displaystyle {mathcal {O}}(log n)} beschränkt, so dass sie in ein vorzeichenloses Integer passen mag. Eine denkbare Codierung der variablen Länge in einem Wort fester Länge lässt die Binärkette nach der ersten „1“ beginnen.
Der maximale Aufwand für einen Zugriff ist O(h){displaystyle {mathcal {O}}(h)}.
Repräsentation durch ein Array |
Ein Binärbaum kann durch ein Array repräsentiert werden, dessen Länge im Wesentlichen der Anzahl der Knoten des Baumes entspricht, genauer: 2h−1{displaystyle 2^{h}-1} mit h{displaystyle h} als der Höhe des Baumes. Eine Anordnung findet sich bei der binären Suche im Array.
Eine andere Art der Repräsentation beginnt mit der Indizierung bei 1 mit Verweis auf die Wurzel. Dann setzt sie sich „zeilenweise“ fort: alle Knoten derselben Tiefe werden von links nach rechts fortlaufend nummeriert. Das heißt: das Auslesen des Arrays von links entspricht einem Level-Order-Traversal (oder Breadth-First-Traversal) des Baums. Falls der Binärbaum nicht voll besetzt ist, müssen ausgelassene Knoten durch Platzhalter besetzt werden, so dass also 2h−1−n{displaystyle 2^{h}-1-n} Speicherzellen verschwendet werden.
Diese Nummerierung hat die angenehme Eigenschaft, dass man leicht die Indizes der verbundenen Knoten berechnen kann. Im Array A sei Ai ein Knoten, dann ist A2i sein linkes und A2i+1 sein rechtes Kind; die abgerundete Hälfte j=⌊i2⌋{displaystyle j=lfloor {tfrac {i}{2}}rfloor ,} verweist auf den Elter Aj.
In der Genealogie ist dieses Indizierungsschema auch unter dem Begriff Kekule-Nummerierung bekannt.
Da bei der Abbildung von einem Binären Baum in ein Array keine expliziten Zeiger auf Kinder bzw. Elter-Knoten notwendig sind, wird diese Datenstruktur auch als implizite Datenstruktur bezeichnet.
Eine Anwendung dieser Darstellung ist der binäre Heap, der für die Sortierung von Elementen verwendet wird.
Traversierung |
Traversierung bezeichnet das systematische Untersuchen der Knoten des Baumes in einer bestimmten Reihenfolge.
Es gibt verschiedene Möglichkeiten, die Knoten von Binärbäumen zu durchlaufen. Man unterscheidet die folgenden Varianten:[1] (Dabei ist „Durchlaufen der Teilbäume“ l und r als rekursiver Aufruf zu verstehen.)
depth-first (Tiefensuche):
pre-order oder Hauptreihenfolge (N–l–r):
Zuerst wird die Wurzel N betrachtet und anschließend der linke l, schließlich der rechte Teilbaum r durchlaufen.
post-order oder Nebenreihenfolge (l–r–N):
Zuerst wird der linke l, dann der rechte Teilbaum r durchlaufen und schließlich die Wurzel N betrachtet.
in-order oder symmetrische Reihenfolge (l–N–r):
Zuerst wird der linke Teilbaum l durchlaufen, dann die Wurzel N betrachtet und schließlich der rechte Teilbaum r durchlaufen. Diese Reihenfolge entspricht bei binären Suchbäumen der Anordnung der Schlüssel und ist für die meisten Anwendungen die gegebene.
reverse in-order oder anti-symmetrische Reihenfolge (r–N–l):
Zuerst wird der rechte Teilbaum r durchlaufen, dann die Wurzel N betrachtet und schließlich der linke Teilbaum l durchlaufen.
breadth-first (Breitensuche) oder level-order:
Beginnend bei der Baumwurzel werden die Ebenen von links nach rechts durchlaufen.
Rekursive Implementierungen |
Die Aktion, die an einem Knoten auszuführen ist, geschieht im Unterprogramm callback
, das vom Benutzer zu liefern ist. Eine gewisse Kommunikation mit dem aufrufenden Programm kann bei Bedarf über die Variable param
vorgenommen werden.
preOrder( knoten, callback, param) {
// Führe die gewünschten Aktionen am Knoten aus:
callback( knoten, param );
if ( knoten.links ≠ null )
preOrder( knoten.links ); // rekursiver Aufruf
if ( knoten.rechts ≠ null )
preOrder( knoten.rechts ); // rekursiver Aufruf
}
postOrder( knoten, callback, param) {
if ( knoten.links ≠ null )
postOrder( knoten.links ); // rekursiver Aufruf
if ( knoten.rechts ≠ null )
postOrder( knoten.rechts ); // rekursiver Aufruf
// Führe die gewünschten Aktionen am Knoten aus:
callback( knoten, param );
}
inOrder( knoten, callback, param) {
if ( knoten.links ≠ null )
inOrder( knoten.links ); // rekursiver Aufruf
// Führe die gewünschten Aktionen am Knoten aus:
callback( knoten, param );
if ( knoten.rechts ≠ null )
inOrder( knoten.rechts ); // rekursiver Aufruf
}
revOrder( knoten, callback, param) {
if ( knoten.rechts ≠ null )
revOrder( knoten.rechts ); // rekursiver Aufruf
// Führe die gewünschten Aktionen am Knoten aus:
callback( knoten, param );
if ( knoten.links ≠ null )
revOrder( knoten.links ); // rekursiver Aufruf
}
Eine Traversierung über den ganzen Baum umfasst pro Knoten genau einen Aufruf einer der rekursiven Traversierungs-Funktionen. Der Aufwand (für die reine Traversierung) bei n{displaystyle n} Knoten ist also in Θ(n){displaystyle Theta (n)}.
Iterative Implementierung |
Eine iterative Implementierung erlaubt es, einen einzelnen Querungs-Schritt, eine „Iteration“, von einem Knoten zu seinem Nachbarknoten auszuführen. So kann man in gewohnter Manier eine Programmschleife für ein Intervall mit Anfang und Ende aufsetzen, die fraglichen Knoten nacheinander aufsuchen und für sie die gewünschten Aktionen ausprogrammieren.[2]
Als Beispiel sei hier nur die in-order-Traversierung gezeigt, die insbesondere bei binären Suchbäumen eine große Rolle spielt, da diese Reihenfolge der Sortier-Reihenfolge entspricht.
inOrderNext( knoten, richtung ) {
// richtung = 1 (Rechts = aufwärts = „in-order“)
// oder = 0 (Links = abwärts = „reverse in-order“)
knotenY = knoten.kind[richtung]; // 1 Schritt in die gegebene Richtung
if ( knotenY ≠ null ) {
richtung = 1 - richtung; // spiegele Links <-> Rechts
// Abstieg zu den Blättern über Kinder in der gespiegelten Richtung:
knotenZ = knotenY.kind[richtung];
while ( knotenZ ≠ null ) {
knotenY = knotenZ;
knotenZ = knotenY.kind[richtung];
}
return knotenY; // Blatt oder Halbblatt
}
// Aufstieg zur Wurzel über Vorfahren der gegebenen Richtung:
knotenY = knoten;
do {
knotenZ = knotenY;
knotenY = knotenZ.elter;
if ( knotenY = null )
return null; // knotenZ ist die Wurzel:
// d. h. es gibt kein Element mehr in dieser Richtung
} until ( knotenZ ≠ knotenY.kind[richtung] );
// knotenY ist der erste Vorfahr in der gespiegelten Richtung
return knotenY;
}
Eine Traversierung über den ganzen Baum beinhaltet pro Bogen einen Abstieg (in der Richtung des Bogens) und einen Aufstieg (in der Gegenrichtung). Ein Baum mit n{displaystyle n} Knoten hat n−1{displaystyle n-1} Bögen, so dass eine Gesamttraversierung über 2n−2∈Θ(n){displaystyle 2n-2in Theta (n)} Stufen geht. Der Aufwand für eine Einzel-Traversierung ist also im Mittel in O(1){displaystyle {mathcal {O}}(1)} und im schlechtesten Fall in O(h){displaystyle {mathcal {O}}(h)} mit h{displaystyle h} als der Höhe des Baums.
Abstieg zum ersten oder letzten Element |
Ganz ähnlich wie eine Einzel-Traversierung funktioniert die Suche nach dem ersten oder letzten Element.
firstLast( binärBaum, richtung ) {
// richtung = Links (erstes) oder Rechts (letztes)
knotenY = binärBaum.wurzel;
if ( knotenY = null )
return null; // der Baum ist leer
// Abstieg zu den (Halb-)Blättern
do {
knotenZ = knotenY;
knotenY = knotenZ.kind[richtung];
} while ( knotenY ≠ null );
return knotenZ; // Blatt oder Halbblatt
Der Aufwand ist O(h){displaystyle {mathcal {O}}(h)}, wo h{displaystyle h} die Höhe des Baums ist.
Einfügen, Einfügepunkt |
Es sei angenommen, dass die Navigation zu einem Einfügepunkt bereits erfolgt ist. Einfügepunkt bedeutet einen Knoten und eine Richtung (rechts bzw. links). Ein unmittelbarer Einfügepunkt in einem binären Baum ist immer ein rechtes (bzw. linkes) Halbblatt, ein mittelbarer ist der unmittelbare Nachbar in der angegebenen Richtung und spezifiziert zusammen mit der Gegenrichtung dieselbe Stelle im Binärbaum – zum echten Einfügen muss aber die Einfügefunktion noch bis zu dem Halbblatt hinabsteigen, welches den unmittelbaren Einfügepunkt darstellt.
Zum Einfügen lässt man das Kind auf der geforderten Richtung des Knotens auf das neue Element verweisen, damit ist dieses korrekt eingefügt. Die Komplexität der Einfügeoperation ist somit konstant.
Nach dem Einfügen ist das neue Element ein Blatt des Binärbaums.
Im folgenden Beispiel wird ein Knoten mit dem Schlüssel J in einen binären Baum am (unmittelbaren) Einfügepunkt (M, links) eingefügt – der mittelbare wäre (G, rechts).
S S
/ /
/ /
G W G W
/ /
/ ----> /
D M D M
/ / /
B F P B F J P
Durch wiederholtes Einfügen an immer derselben Stelle kann es dazu kommen, dass der Baum zu einer linearen Liste entartet.
Löschen |
Beim Löschen muss man deutlich mehr Fälle unterscheiden. Wichtig ist z. B., wie viele Kinder der Knoten hat.
Fall A: Zu löschender Knoten hat höchstens ein Kind.
Ist der Knoten ein Blatt (Knoten ohne Kinder), dann wird beim Löschen einfach der Knoten entfernt. Hat der zu löschende Knoten genau ein Kind, wird dieses an die Stelle des zu löschenden Knotens gesetzt.
Fall B: Zu löschender Knoten hat zwei Kinder.
In diesem Fall kann die Löschung sowohl über den linken wie über den rechten Teilbaum vollzogen werden. Um die in-order-Reihenfolge aufrechtzuerhalten, ist aber ein Abstieg bis zu einem Halbblatt unvermeidlich.
Eine Möglichkeit ist, den linken Teilbaum an die Position zu setzen, an der der zu löschende Knoten war, und den rechten Teilbaum an den linken an dessen rechtester Stelle anzuhängen, wie es das Beispiel zeigt (G soll gelöscht werden):
S S
/ /
/ /
G W D W
/ /
/ ----> /
D M B F
/ /
B F J P M
/
K J P
K
Die Veränderungen in den Höhen fallen jedoch kleiner aus, wenn der zu löschende Knoten durch einen (unmittelbaren) Nachbarn in der in-order-Reihenfolge ersetzt wird.[3] Im Beispiel ist F der linke Nachbar von G, steht also im linken Teilbaum ganz rechts; J ist der rechte Nachbar von G, steht also im rechten Teilbaum ganz links. Die in-order-Reihenfolge ist F – G – J. Der linke/rechte Nachbar kann einen linken/rechten Teilbaum haben, der an die Stelle gehängt werden muss, wo der Nachbar war.
Im folgenden Beispiel wird der zu löschende Knoten G durch seinen rechten Nachbarn J ersetzt:
S S
/ /
/ /
G W J W
/ /
/ /
D M ----> D M
/ / / /
B F J P B F K P
K
Um dem Baum möglichst wenig Gelegenheit zu geben, einseitig zu werden, kann man systematisch zwischen linkem und rechtem Abstieg abwechseln. Stehen Balance-Werte zur Verfügung, liegt es nahe, den Abstieg auf der evtl. höheren Seite zu bevorzugen.
Durch wiederholtes Löschen kann es dazu kommen, dass der Baum zu einer linearen Liste entartet.
Wegen der unvermeidlichen Abstiege bis zu den Halbblättern ist die Komplexität der Löschoperation im schlechtesten Fall O(h){displaystyle {mathcal {O}}(h)}, wobei h{displaystyle h} die Höhe des Baumes ist. Da der Abstieg einer Einzel-Traversierung entspricht und Abstiege in einer Gesamttraversierung gleich häufig sind wie Aufstiege, konvergiert der Mittelwert der abzusteigenden Ebenen für wachsende Anzahl der Knoten genau gegen 1.
Die Abbildungen und der Pseudocode zeigen das Entfernen eines Elements, das zwei Kinder und einen nahen Enkel besitzt, aus einem binären Baum.
Der Knoten 5 soll gelöscht werden
Der neue Baum
- Pseudocode
remove( binärBaum, knoten ) {
// Es sei angenommen, dass knoten.links ≠ null, knoten.rechts ≠ null
// und knoten.rechts.links ≠ null
knotenY = knoten.rechts;
while ( knotenY ≠ null ) {
knotenZ = knotenY;
knotenY = knotenZ.links;
}
// knotenZ ist der rechte Nachbar von knoten
if ( knotenZ.rechts ≠ null )
knotenZ.rechts.elter = knotenZ.elter;
knotenZ.elter.links = knotenZ.rechts;
knotenZ.rechts = knoten.rechts;
knoten.rechts.elter = knotenZ;
knotenZ.links = knoten.links;
knoten.links.elter = knotenZ; // knoten.links ≠ null
if ( knoten.elter.links = knoten )
knoten.elter.links = knotenZ;
else
knoten.elter.rechts = knotenZ;
knotenZ.elter = knoten.elter;
}
Rotation |
Mit einer Rotation (вращение (russ. für Drehung) bei Adelson-Velsky und Landis[4]) kann man einen Binärbaum in einen anderen überführen und damit Eigenschaften, insbesondere Höhen von Teilbäumen (beispielsweise in Rot-Schwarz-Bäumen und AVL-Bäumen) oder Suchtiefen von Knoten (beispielsweise in Splay-Bäumen) beeinflussen. Da bei einer Rotation alle beteiligten Knoten sich nur „vertikal“ bewegen, ändert sich die in-order-Reihenfolge nicht, so dass der Baum bezüglich dieser Reihenfolge (die bei Suchbäumen die Sortierfolge ist) äquivalent bleibt.
Eine Rotation lässt sich durch die Rotationsrichtung Links oder Rechts und durch die Wurzel des betroffenen Teilbaums spezifizieren.
Statt der beiden kann auch ein Kindknoten angegeben werden, der in dieser Verwendung als der Pivot (Drehpunkt) der Rotation bezeichnet wird. Dabei ist die Rotationsrichtung der Kindesrichtung entgegengesetzt.
Zum Beispiel bewirkt RotateLeft(L) die Absenkung des Knotens L und die Anhebung seines rechten Kindknotens (hier als Pivot: R).
Es handelt sich aber nicht um eine kontinuierliche Drehung, eher um eine bistabile Wippe, also das Kippen einer Kante (hier: L↘R) in ihre andere Orientierung (L↙R). Die Anforderungen
- Umkehrung der Orientierung einer gerichteten Kante
- Bewahrung der in-order-Reihenfolge
- kleinstmögliche Veränderung des Baums
ziehen gewisse Anpassungen bei den Nachbarknoten nach sich, nämlich: (unten) das Einhängen des zwischen den beiden Knoten befindlichen Kindes (hier: m) als inneres Kind am unteren Knoten und (oben) das Ersetzen des bisherigen Kindes beim (Groß-)Elter (hier: P) durch den oberen Knoten.[5]
P P
/ /
L RotateLeft(L) R
/ --------> /
l <-------- / r
R RotateRight(R) L
/ /
m r l m
- Erklärung zu RotateLeft(L)
L wird zum linken Kind von R. Der ursprünglich linke Kindbaum m von R (der Teilbaum in der Mitte) wird zum rechten Kindbaum von L.
- Erklärung zu RotateRight(R)
R wird zum rechten Kind von L. Der ursprünglich rechte Kindbaum m von L wird zum linken Kindbaum von R.
Komplexität |
In beiden Fällen ändert sich zusätzlich die Aufhängung des neuen Baums von oben her. Die Aufhängungen der äußeren Kindbäume l und r bleiben erhalten.
Somit sind 3 Verknüpfungen anzupassen, die in den Graphiken verstärkt gezeichnet sind. Im Ergebnis benötigt eine Rotation konstante Laufzeit O(1){displaystyle {mathcal {O}}(1)}.
Doppelrotation |
Eine Doppelrotation besteht aus zwei unmittelbar hintereinander ausgeführten gegenläufigen (Einzel)rotationen. Dabei wird ein Knoten um zwei Ebenen angehoben. Sie wird bspw. bei der Rebalancierung von AVL-Bäumen benötigt. Die Anzahl der anzupassenden Verknüpfungen ist 5.
Beim Spalten eines AVL-Baums können auch Dreifachrotationen vorkommen.
Rotationsmetrik |
Der Rotationsabstand zwischen 2 Binärbäumen mit derselben Anzahl von Knoten ist die Minimalzahl an Rotationen, die erforderlich sind, um den ersten Baum in den zweiten zu überführen. Mit dieser Metrik wird die Menge BTn{displaystyle BT_{n}} der Binärbäume mit n{displaystyle n} Knoten zu einem metrischen Raum, denn die Metrik erfüllt Definitheit, Symmetrie und Dreiecksungleichung. Der Raum BTn{displaystyle BT_{n}} ist ein zusammenhängender metrischer Raum mit einem Durchmesser ≤2n−6{displaystyle leq 2n-6}.[6] Das heißt: Zu 2 verschiedenen Binärbäumen A{displaystyle A} und B∈BTn{displaystyle Bin BT_{n}} gibt es eine natürliche Zahl m≤2n−6{displaystyle mleq 2n-6} und Binärbäume T1,…,Tm−1∈BTn{displaystyle T_{1},dots ,T_{m-1}in BT_{n}}, so dass mit T0:=A{displaystyle T_{0}:=A} und Tm:=B{displaystyle T_{m}:=B} für i=1,…,m{displaystyle i=1,dots ,m} jeweils Ti{displaystyle T_{i}} aus Ti−1{displaystyle T_{i-1}} durch eine Rotation hervorgeht.
Es ist ungeklärt, ob es einen polynomiellen Algorithmus zur Berechnung des Rotationsabstands gibt.
Umwandeln einer Form eines Binärbaums in eine andere |
Bei den folgenden Umwandlungen wird die in-order-Reihenfolge nicht geändert. Ferner sei n{displaystyle n} die Anzahl der Knoten im Baum.
- Ein Binärbaum lässt sich mit Aufwand O(n){displaystyle {mathcal {O}}(n)} für Platz und Zeit in eine geordnete Liste umwandeln.
- Da das Eintragen eines einzelnen Eintrags in eine geordnete Liste konstanten Aufwand bedeutet, ist angesichts der Komplexität der #Traversierung linearer Aufwand leicht zu schaffen. Schwieriger ist es, das Eintragen in-place zu bewerkstelligen, also den Platz für die Zeiger der Liste vom Platz für den Binärbaum zu nehmen.
- Eine geordnete Liste lässt sich mit Zeitaufwand O(n){displaystyle {mathcal {O}}(n)} in einen vollständig balancierten Binärbaum umwandeln.
- Die Form eines vollständig balancierten Binärbaums hängt nur von seiner Knotenzahl ab. Ist ein Teilbaum mit einer lückenlosen Folge von m{displaystyle m} Knoten aufzubauen, dann ordnet man dem linken Kindbaum eine lückenlose Folge von ⌈m−12⌉{displaystyle lceil {tfrac {m-1}{2}}rceil } und dem rechten eine von ⌊m−12⌋{displaystyle lfloor {tfrac {m-1}{2}}rfloor } Knoten zu. Damit weichen die Abstände zweier beliebiger Blätter von der Wurzel um höchstens 1 voneinander ab, wie es sein muss.
- In einem Binärbaum lässt sich mit dem Zeitaufwand O(n){displaystyle {mathcal {O}}(n)} jeder Knoten mit der Anzahl der Knoten in seinem Teilbaum versehen.
- Bei der Traversierung kann man die Knotenzahlen pro Teilbaum bilden und im Knoten abspeichern.
- Ein AVL-Baum lässt sich ohne Änderung seiner Form mit Zeitaufwand O(n){displaystyle {mathcal {O}}(n)} als Rot-Schwarz-Baum einfärben.
- Die Menge der AVL-Bäume ist eine echte Teilmenge in der Menge der Rot-Schwarz-Bäume. Der dortige Beweis zeigt nebenbei, dass man anhand der Höhen, die man beim in-order-Durchlauf exakt mitschreibt, die Rot-Schwarz-Einfärbung vornehmen kann.
Siehe auch |
- Binärer Suchbaum
- H-Baum
- Kartesischer Baum
Literatur |
Donald Knuth: The art of computer programming vol 1. Fundamental Algorithms. 3. Auflage. Addison-Wesley, 1997, ISBN 0-201-89683-4, Abschnitt 2.3, S. 318 ff.- Horst Wettstein: Systemprogrammierung. 2. Auflage. Carl Hanser Verlag, 1980, ISBN 3-446-13217-1.
- Lennart Krothaus: Der Binärbaum-Ein Informatikwunder. 3. Auflage. MethiVerlag, 1998.
Robert Sedgewick, Kevin Wayne: Algorithms Fourth Edition. (PDF) Pearson Education, 2011, ISBN 978-0-321-57351-3.
Weblinks |
Commons: Binärbäume – Sammlung von Bildern, Videos und Audiodateien
Ben Pfaff: An Introduction to Binary Search Trees and Balanced Trees. (PDF; 1675 kB) 2004 (englisch).
Einzelnachweise |
↑ Die Bezeichnungen finden sich bspw. bei Knuth.
↑ S. a. Pfaff 2004, p.58 „4.9.3.7 Advancing to the Next Node“ mit einem Stapelspeicher, genannt „Traverser“, für den Rückweg zur Wurzel. Die vorgestellte Lösung setzt einen Zeigerelter
zum Elterknoten voraus.
↑ Da zwischen den beiden Knoten kein weiterer Knoten liegen kann, wird die in-order-Reihenfolge nicht verändert. Diese Vorgehensweise wurde zum ersten Mal 1962 von T. Hibbard vorgeschlagen (zitiert nach #Sedgewick S. 410.)
↑ G. M. Adelson-Velsky, E. M. Landis: Один алгоритм организации информации. In: Doklady Akademii Nauk SSSR. 146, 1962, S. 263–266. (russisch)
↑ Die Eindeutigkeit dieser Anpassungen ist (nur) bei den binären Bäumen gegeben. Denn wenn eine Kante L→R gekippt wird, muss am vorher unteren Knoten R eine „Valenz“ für das neue Kind L frei gemacht werden. Die einzige in der korrekten Richtung ist die Valenz für m. Bei L wird gleichzeitig eine Valenz (die vorher auf R zeigte) frei, die die Richtung von m hat und m übernehmen muss. Ganz ähnlich verhält es sich am oberen Ende der Kante.
↑ Daniel D. Sleator, Robert E. Tarjan, William P. Thurston: Rotation distance, triangulations, and hyperbolic geometry. In: Journal of the American Mathematical Society, 1, (3), 1988, S. 647–681.