Ein binärer Baum kann mit der Methode Preorder oder Inorder dargestellt werden. Die beiden Methoden beschreiben denselben Baum, nur die Reihenfolge der Elemente ist unterschiedlich. Show
Beispiel: Bei Inorder wird zuerst der
linke Baum dargestellt, dann der Knoten selbst und anschließend der rechte Baum. Die Rekonstruktion wird klar, wenn man sich die beiden Schemata Preorder: { K,L,R } vor Augen führt. "K" ist der Knoten und besteht immer nur aus einer einzigen Zahl. "L" und "R" sind die jeweiligen Äste, wobei die sortierten Mengen Inorder-L und Preorder-L übereinstimmen müssen, ebenso Inorder-R und Preorder-R. Um diese (unsortierten) Mengen besser zu unterscheiden, schreibt man Preorder: { K,Lp,Rp } Die Mengen Li,K,Ri sind über K eindeutig bestimmbar. Weil Lp und Rp wieder vom Typ { K,L,R } sind, muss das erste Element von Lp/Rp der linke/rechte Kind-Knoten von K sein. Im nächsten Schritt teilt sich das Schema in zwei unabhängige Äste, Preorder linker Ast: { K,L,R } = { Lp } Preorder rechter Ast: { K,L,R } = { Rp } usw. rekursiv. K=8 Die Mengen Li und Lp (ebenso Ri und Rp) stimmen nicht überein. Fehler ! _________________________________________________________________________ K=5 Der linke Kind-Knoten von 5 ist leer (Pivot-Element von Lp). _________________________________________________________________________ rechts K=4 Der linke Kind-Knoten von 4 ist die 7 (Pivot-Element von Lp). _________________________________________________________________________ Die neue Preorder (rechts) ergibt sich aus Rp = 9,2 links K=7 Der linke Kind-Knoten von 7 ist die 6 (Pivot-Element von Lp). rechts K=9 Der linke Kind-Knoten von 9 ist die 2
(Pivot-Element von Lp). _________________________________________________________________________ Zwei der wichtigsten Themen in der Informatik sind das Sortieren und Suchen von Datensätzen. Eine Datenstruktur, die für beides häufig zum Einsatz kommt, ist der Binärbaum im Allgemeinen und seine Spezialfälle binärer Suchbaum und binärer Heap. In diesem Artikel erfährst du:
Den Quellcode zum Artikel findest du in diesem GitHub-Repository. Binärbaum DefinitonEin Binärbaum ist eine Baum-Datenstruktur, in der jeder Knoten maximal zwei Kindknoten hat. Die Kindknoten werden als linkes und rechtes Kind bezeichnet. Binärbaum BeispielEin Binärbaum sieht beispielsweise wie folgt aus: Binärbaum-BeispielBinärbaum TerminologieDie folgenden Begriffe sollte man als Entwickler kennen:
Die folgende Grafik zeigt dieselbe Binärbaum-Datenstruktur wie zuvor, beschriftet mit Knotentypen, Knotentiefe und Höhe des Binärbaumes. Binärbaum-Datenstruktur mit KnotentypenEigenschaften von BinärbäumenBevor wir zur Implementierung von Binärbäumen und deren Operationen kommen, zunächst eine kurze Übersicht über einige spezielle Arten von Binärbäumen. Voller BinärbaumIn einem vollen Binärbaum (englisch: full binary tree) haben alle Knoten entweder keine oder zwei Kinder. Voller BinärbaumVollständiger BinärbaumLeider wird diese Bezeichnung in der Literatur nicht einheitlich verwendet. Manche Autoren bezeichnen einen kompletten Binärbaum als vollständig, andere bezeichnen einen perfekten Binärbaum als vollständig. Ich werde daher nur die Begriffe komplett und perfekt verwenden. Kompletter BinärbaumIn einem kompletten Binärbaum (englisch: complete binary tree) sind alle Ebenen, außer möglicherweise die letzte, vollständig gefüllt. Wenn die letzte Ebene nicht vollständig gefüllt ist, dann sind deren Knoten so weit wie möglich links angeordnet. Kompletter BinärbaumPerfekter BinärbaumEin perfekter Binärbaum (englisch: perfect binary tree) ist ein voller Binärbaum, in dem alle Blätter die gleiche Tiefe haben. Perfekter Binärbaum der Höhe 3Ein perfekter Binärbaum der Höhe h hat n = 2h+1-1 Knoten und l = 2h Blätter. Bei einer Höhe von 3 sind das 15 Knoten, davon 8 Blätter. Balancierter BinärbaumIn einem balancierten Binärbaum (englisch: balanced binary tree) unterscheiden sich die linken und rechten Teilbäume eines jeden Knoten in der Höhe um maximal eins. Balancierter BinärbaumSortierter BinärbaumIn einem sortierten Binärbaum (englisch: sorted binary tree) enthält der linke Teilbaum eines Knotens nur Werte die kleiner (oder gleich) als der Wert des Elternknotens sind, und der rechte Teilbaum nur Werte die größer (oder gleich) als der Wert des Elternknotens sind. Solch eine Datenstruktur wird auch binärer Suchbaum genannt. Binärbaum in JavaFür die Binärbaum-Implementierung in Java definieren wir zunächst die Datenstruktur für die Knoten
(Klasse Node im GitHub-Repository). Der Einfachheit halber verwenden wir
Die Der Binärbaum selbst besteht zunächst einmal nur aus dem Interface BinaryTree und dessen Minimal-Implementierung BaseBinaryTree, welche lediglich eine Referenz auf den Wurzelknoten enthält:
Warum wir uns hier die Mühe machen ein Interface zu definieren, wird sich im weiteren Verlauf des Tutorials zeigen. Die Binärbaum-Datenstruktur ist damit vollständig definiert. Binärbaum-TraversierungEine der wichtigsten Operationen auf Binärbäumen ist die Traversierung aller Knoten, also das Besuchen aller Knoten in einer bestimmten Reihenfolge. Die gängigsten Arten der Traversierung sind:
In den folgenden Abschnitten werden die verschiedenen Arten an folgendem Beispiel gezeigt: Beispiel für Binärbaum-TraversierungDas oben erwähnte Besuchen während der Traversierung realisieren wir mit dem Visitor-Pattern, d. h. wir erstellen ein Visitor-Objekt, das wir an die Traversierungsmethode übergeben. Tiefensuche im BinärbaumBei der Tiefensuche (englisch: depth-first search, DFS) wird in einer bestimmten Reihenfolge:
Die gängigen Reihenfolgen sind: Hauptreihenfolge (pre-order)In der Hauptreihenfolge (Kennzeichnung: N–L–R) erfolgt die Traversierung in folgender Reihenfolge:
Die Knoten des Beispielbaumes werden, wie in folgender Grafik zu sehen, in folgender Reihenfolge besucht: 3→1→13→10→11→16→15→2 Binärbaum-Traversierung in Hauptreihenfolge (pre-order)Der Code hierfür ist relativ simpel (Klasse DepthFirstTraversalRecursive ab Zeile 21):
Die Methode kann entweder direkt aufgerufen werden – dann muss ihr der Wurzelknoten übergeben werden – oder über die nicht-statische Methode
Dazu muss eine Instanz von
Eine iterative Implementierung ist mit einem Stack möglich (Klasse DepthFirstTraversalIterative ab Zeile 20). Die iterativen Implementierungen sind recht komplex, weshalb ich sie hier nicht mit abdrucke. Warum ich in den iterativen Tree-Traversals Nebenreihenfolge (post-order)In der Nebenreihenfolge (Kennzeichnung: L–R–N) erfolgt die Traversierung in folgender Reihenfolge:
Die Knoten des Beispielbaumes werden hierbei in folgender Reihenfolge besucht: 13→1→11→15→2→16→10→3 Binärbaum-Traversierung in Nebenreihenfolge (post-order)Den Code findest du in DepthFirstTraversalRecursive ab Zeile 42:
Die iterative Implementierung, die bei der Post-Order-Traversierung noch komplizierter ist als bei der Pre-Order-Traversierung, findest du in DepthFirstTraversalIterative ab Zeile 44. Symmetrische Reihenfolge (in-order)In der symmetrischen Reihenfolge (Kennzeichnung: L–N–R) erfolgt die Traversierung in folgender Reihenfolge:
Die Knoten des Beispielbaumes werden in folgender Reihenfolge besucht: 13→1→3→11→10→15→16→2 Binärbaum-Traversierung in symmetrischer Reihenfolge (in-order)Den rekursiven Code findest du in DepthFirstTraversalRecursive ab Zeile 62:
Die iterative Implementierung der In-Order-Traversierung findest du in DepthFirstTraversalIterative ab Zeile 69. Im binären Suchbaum werden bei der In-Order-Traversierung die Knoten in der Reihenfolge ihrer Sortierung besucht. Anti-symmetrische Reihenfolge (reverse in-order)In der anti-symmetrischen Reihenfolge (Kennzeichnung: R–N–L) erfolgt die Traversierung in folgender Reihenfolge:
Die Knoten des Beispielbaumes werden in entgegengesetzter Reihenfolge zur In-Order-Traversierung besucht: 2→16→15→10→11→3→1→13 Binärbaum-Traversierung in anti-symmetrischer Reihenfolge (reverse in-order)Den rekursiven Code findest du in DepthFirstTraversalRecursive ab Zeile 83:
Die iterative Implementierung der Reverse-In-Order-Traversierung findest du in DepthFirstTraversalIterative ab Zeile 89. Im binären Suchbaum werden bei der Reverse-In-Order-Traversierung die Knoten in absteigender Reihenfolge ihrer Sortierung besucht. Breitensuche im BinärbaumBei der Breitensuche (englisch: breadth-first, BFS) – auch Level-Order-Traversierung genannt – werden die Knoten von der Wurzel beginnend, Ebene für Ebene, von links nach rechts besucht. Es ergibt sich folgende Reihenfolge: 3→1→10→13→11→16→15→2 Binärbaum-Traversierung via Breitensuche (level-order)Um die Knoten in dieser Reihenfolge zu besuchen, benötigen wir eine Queue, in der wir zuerst den Root-Knoten einfügen, und dann wiederholt das erste Element entnehmen, dieses besuchen und dessen Kinder in die Queue eintragen – solange bis die Queue wieder leer ist. Den Code findest du in der Klasse BreadthFirstTraversal:
Den Aufruf aller
Traversierungs-Arten findest du beispielhaft in der Methode Operationen auf BinärbäumenNeben der Traversierung sind weitere grundlegenden Operationen auf Binärbäumen das Einfügen sowie das Löschen von Knoten. Operationen zum Suchen werden von speziellen Binärbäume wie z. B. dem binären Suchbaum bereitgestellt. Ohne spezielle Eigenschaften können wir im Binärbaum nur suchen, in dem wir über alle Knoten traversieren und diese mit dem gesuchten Element vergleichen. Element einfügenBeim Einfügen neuer Elemente müssen wir verschiedene Fälle unterscheiden: Fall A: Knoten unter Blatt oder Halbblatt einfügenEs ist leicht einen neuen Knoten an ein Blatt oder ein Halbblatt anzuhängen. Hierzu müssen wir lediglich die Fall B: Knoten zwischen internen Knoten und dessen Kind einfügenDoch wie geht man vor, wenn man einen Knoten zwischen einem internen Knoten und einem seiner Kinder einfügen will? Neuen Knoten unter internen Knoten einfügenDas ist nur mit einer Reorganisation des Baumes möglich. Wie genau der Baum reorganisiert wird, hängt von der konkreten Art des Binärbaumes ab. Wir implementieren in diesem Tutorial einen sehr einfachen Binärbaum und gehen für die Reorganisation wie folgt vor:
Die folgende Grafik zeigt den zweiten Fall: Wir fügen den neuen Knoten N zwischen P und R ein: Einfügen eines neuen Knotens zwischen internem Knoten und seinem KindDas ist – wie gesagt – eine sehr einfache Implementierung. Im Beispiel oben resultiert diese in einem stark unbalancierten Binärbaum. Spezielle Binärbäume gehen hier anders vor, um eine Baum-Struktur beizubehalten, die die speziellen Eigenschaften des jeweiligen Binärbaumes (Sortierung, Balancierung, etc.) erfüllt. Baumknoten einfügen – Java-QuellcodeHier siehst du den Code zum Einfügen eines neuen Knotens mit Wert (Der Switch-Ausdruck mit den geschweiften Klammern wurde in Java 12/13 eingeführt).
In der Methode
Element löschenAuch beim Löschen eines Knotens müssen wir verschiedene Fälle unterscheiden. Fall A: Knoten ohne Kinder (Blatt) löschenIst der zu löschende Knoten N ein Blatt, hat also selbst keine Kinder, dann wird der Knoten einfach entfernt. Dazu prüfen wir, ob der Knoten linkes oder rechtes Kind des Parents P ist und setzen entsprechend dessen Fall B: Knoten mit einem Kind (Halbblatt) löschenHat der zu löschende Knoten N selbst ein Kind C, dann rückt dieses an die gelöschte
Position auf. Wir müssen wieder prüfen, ob der zu löschende Knoten N linkes oder rechtes Kind des Parents P ist. Danach setzen wir entsprechend die Fall C: Knoten mit zwei Kindern löschenWie geht man vor, wenn man einen Knoten mit zwei Kindern löschen will? Wie entfernt man einen internen Knoten aus einem Binärbaum?Dies ist nur mit einer Umorganisation des Binärbaum möglich. Analog zum Einfügen gibt es auch für das Löschen – je nach konkreter Art des Binärbaumes – unterschiedliche Strategien. In einem Heap beispielsweise wird an die Position des gelöschten Knotens der letzte Knoten des Baums gesetzt und danach der Heap repariert. Wir verwenden für unser Tutorial folgende, einfach zu implementierende Variante:
Es ist gut zu erkennen, wie diese Strategie zu einem sehr unbalancierten Binärbaum führt. Binärbäume wir der binäre Suchbaum und der Binäre Heap haben daher – wie auch beim Einfügen – komplexere Strategien. Baumknoten löschen – Java-QuellcodeDie folgende Methode (Klasse SimpleBinaryTree ab Zeile 71) entfernt den übergebenen Knoten
Die Methode Für den Fall, dass der zu löschende Knoten die Wurzel ist, wird einfach die Wurzelreferenz neu gesetzt.
In Fall C (Knoten mit zwei Kindern löschen) wird der Baum, wie im vorangegangenen Abschnitt beschrieben, umorganisiert. Dies geschieht in der separaten Methode
In der
Methode Array-Darstellung des BinärbaumsZum Abschluss möchte ich dir eine alternative Repräsentation des Binärbaums zeigen: die Speicherung in einem Array. Dabei enthält das Array so viele Elemente wie ein perfekter Binärbaum der Höhe des zu speichernden Binärbaumes, also 2h+1-1 Elemente bei einer Höhe h (in der folgenden Grafik: 7 Elemente bei Höhe 2). Die Knoten des Baumes werden von der Wurzel abwärts, Ebene für Ebene, von links nach rechts durchnummeriert und auf das Array abgebildet, wie folgende Grafik zeigt: Array-Repräsentation eines BinärbaumsNicht vorhandene Knoten können durch einen festgelegten Wert dargestellt werden. Sollte der Baum beispielsweise nur nicht-negative ganze Zahlen enthalten, könnte ein fehlender Knoten durch den Wert -1 dargestellt werden. Bei einem kompletten Binärbaum kann alternativ das Array entsprechend verkürzt werden oder die Anzahl der Knoten als zusätzlicher Wert gespeichert werden. Vor- und Nachteile der Array-DarstellungDie Array-Darstellung hat folgende Vorteile:
Dagegen aufwiegen muss man folgende Nachteile:
ZusammenfassungIn diesem Artikel hast du erfahren, was ein Binärbaum ist, welche Arten von Binärbäumen es gibt, welche Operationen auf Binärbäume anwendbar sind und wie man Binärbäume in Java implementiert. Wenn dir der Artikel gefallen hat, teile ihn gerne über einen der Share-Buttons. Möchtest du informiert werden, wenn der nächste Artikel veröffentlicht wird? Dann klicke hier, um dich für den HappyCoders-Newsletter anzumelden. |