Binäre Bäume Definition: Ein binärer Baum T besteht aus einer Menge von Knoten, die durch eine Vater-Kind-Beziehung wie folgt strukturiert ist: 1. Es gibt genau einen hervorgehobenen Knoten r ∈ T , die Wurzel des Baums 2. Jeder Knoten außer r hat genau einen Vaterknoten 3. Die Wurzel r ist Vorfahre jedes Knotens. 4. Jeder Knoten hat höchstens zwei Kinder, ein linkes und ein rechtes. In einem binären Baum unterscheidet man zwei Arten von Knoten: Blätter - das sind Knoten, die kein Kind haben - und innere Knoten - das sind Knoten, die mindestens ein Kind haben, wobei auch im Fall mit einem Kind festgelegt sein muss, ob dieses Kind ein linkes und ein rechtes ist. Wir sprechen von einem echten binären Baum, wenn jeder innere Knoten zwei Kinder hat. Das folgende Beispiel zeigt links einen echten binären Baum, in der Mitte einen Baum, der nicht binär ist, weil ein Knoten drei Kinder hat und rechts einen unechten binären Baum. Wurzel 3 Kinder nur ein Kind Binäre Bäume können in Haskell als rekursiv definierter, algebraischer Typ implementiert werden. Wir beginnen mit einem Datentyp BinTree zur Darstellung echter Binärbäume, für die zwei Konstruktoren ausreichen: Ein Konstruktor Leaf für Binärbäume, die nur aus einem Blatt bestehen und ein Konstruktor Node, der einen neuen Wurzelknoten als Vater über zwei vorhandene Binärbäume stellt. data BinTree = Leaf | Node BinTree BinTree Zur Übertragung dieses Ansatzes auf unechte Binärbäume müsste man zwei zusätzliche Konstruktoren für Knoten mit nur einem rechten oder nur einem linken Kind einführen. Um das zu vermeiden, behilft man sich mit einem Trick. Jeder Knoten des darzustellenden Binärbaums T erhält soviele künstliche Blätter, dass er danch zwei Kinder hat (zweite Abbildung). Auf diese Weise entsteht ein echter Binärbaum, dessen innere Knoten die ursprünglichen Knoten von T sind. Da die neuen Blätter in T nicht vorhanden waren, benennt man sie mit Nil, einem Ausdruck für das Nichts. data NTree = Nil | Node NTree NTree echter Binärbaum mit Nil−Knoten nicht−echter Binärbaum Nil−Knoten Obwohl beide Definitionen bis auf die verschiedene Namesgebung gleich sind, ist die verbundene Interpretation anders. Für BinTree werden Blätter nur mit dem Konstruktor Leaf erzeugt, in NTree stellt man ein Blatt als Node Nil Nil dar. Häufig will man die Knoten eines Binärbaums mit bestimmten Werten (z.B. Zahlen oder Zeichen) belegen. Das kann durch eine polymorphe Variation der Definition erreicht werden, wobei der Typ a der Belegungsobjekte mit angegeben werden muss: data NTree a = Nil | Node a NTree NTree Tiefe, Höhe und Größe Definition: Sei T ein Baum und v ∈ T ein Knoten. Der Abstand von v zur Wurzel nennen wir die Tiefe von v und den Abstand von v zu seinem weitesten Nachfahren die Höhe von v. Die Höhe der Wurzel definiert die Höhe (und gleichzeitig die Tiefe) des Baums. Im folgenden Satz werden die wichtigsten Fakten über die Anzahl von Blättern und inneren Knoten in einem echten binären Baum zusammengestellt. Wir werden im Weiteren mit i(T ) und b(T ) die Anzahlen der inneren Knoten und der Blätter des Baums T bezeichnen. Die Größe eines Baums ist die Gesamtzahl der Knoten. Sie ergibt sich durch n(T ) = i(T ) + b(T ). Für die Höhe eines Knotens v bzw. des Baums T werden wir die Bezeichnungen h(v) bzw. h(T ) verwenden. Satz: Sei T ein echter binärer Baum der Höhe h. Dann gelten die folgenden fünf Bedingungen: (1) b(T ) = i(T ) + 1 (2) h≤ i(T ) ≤ 2h − 1 (3) h+1≤ b(T ) ≤ 2h (4) 2h + 1 ≤ n(T ) ≤ 2h+1 − 1 (5) log2 (n(T ) + 1) − 1 ≤ h ≤ n(T )−1 2 Beweis: Zuerst beobachten wir, dass die Bedingung (3) unmittelbar aus (1) und (2) folgt und (4) durch additive Verknüpfung von (2) und (3) entsteht. Die Ungleichungen in (5) folgen aus (4) durch einafche Umformungen und Anwendung der Logarithmusfunktion: n(T ) ≤ 2h+1 − 1 =⇒ n(T ) + 1 ≤ 2h+1 =⇒ log2 (n(T ) + 1) ≤ h + 1 =⇒ log2 (n(T ) + 1) − 1 ≤ h und n(T ) − 1 2 Bleibt nur noch der Beweis von (1) und (2), den man durch vollständige Induktion nach h = h(T ) führt: Der Induktionsanfang für h = 0 ist trivial, denn in diesem Fall besteht T nur aus der Wurzel, die gleichzeitig das einzige Blatt ist. Somit ist b(T ) = 1, i(T ) = 0 und h = 0 wodurch beide Bedingungen erfüllt sind. Beim Induktionsschritt geht man davon aus, dass (1) und (2) erfüllt sind für alle Bäume, deren Höhe kleiner als h ist (Induktionsvoraussetzung). Zum Nachweis der Induktionsbehauptung, die die Gültigkeit von (1) und (2) für alle Bäume T der Höhe h beinhaltet, betrachtet man zu einem solchen Baum T die Kinderknoten v1 und v2 der Wurzel r von T und die zwei Teilbäume T1 und T2 die aus v1 mit allen seinen Nachfahren bzw. aus v2 mit allen seinen Nachfahren entstehen. Offensichtlich ist sowohl h1 = h(T1 ) < h als auch h2 = h(T2 ) < h und damit kann man die Induktionsvoraussetzung auf beide anwenden. Während die Blätter von T durch disjunkte Vereinigung der Blättermengen von T1 und T2 entstehen, muss bei den inneren Knoten einen zusätzlichen, nämlich die Wurzel von T berücksichtigt werden: 2h + 1 ≤ n(T ) =⇒ h ≤ b(T ) = b(T1 ) + b(T2 ) und i(T ) = i(T1 ) + i(T2 ) + 1 Damit kann man unter Verwendung der Induktionsvoraussetzung Bedingung (1) wie folgt ableiten: b(T ) = b(T1 ) + b(T2 ) = (i(T1 ) + 1) + (i(T2 ) + 1) = i(T ) + 1 Auch die obere Schranke von i(T ) ergibt sich auf ähnliche Weise: i(T ) = i(T1 ) + i(T2 ) + 1 ≤ (2h1 − 1) + (2h2 − 1) + 1 ≤ 2 · 2h−1 − 1 = 2h − 1 Die untere Schranke von i(T ) kann man auch ohne Induktionsvoraussetzung begründen: Nach Definition der Höhe gibt es in T einen Weg der Länge h von Wurzel zu einem Blatt. Bis auf den letzten Knoten auf diesem Weg sind alle Knoten innere Knoten und somit ist i(T ) ≥ h. Traversierungen von Bäumen Häufig werden Bäume als Datenstrukturen verwendet, indem entweder alle Knoten des Baums oder nur die inneren Knoten oder nur die Blätter mit Zahlen oder anderen Objekten belegt werden. Zur systematischen Auflistung dieser Objekte verwendet man sogenannte Baum– Traversierungen, das sind Methoden mit denen alle Knoten des Baums besucht werden. Die drei wichtigsten Standardmethoden sind: • Preorder–Traversierung • Postorder–Traversierung • Inorder–Traversierung Alle drei Verfahren kann man rekursiv beschreiben und als gemeinsame Regel gilt die Verankerung, dass im trivialen Fall, in dem der Baum nur aus einem Knoten besteht (die gleichzeitig Wurzel und das einzige Blatt ist), nur dieser Knoten zu besuchen ist. Anderenfalls haben wir die Wurzel mit einem linken und einen rechten Teilbaum und verfahren nach folgenden Regeln: • Preorder: Besuche zuerst die Wurzel, dann traversiere den linken Teilbaum und danach den rechten Teilbaum. • Postorder: Traversiere zuerst den linken Teilbaum, danach den rechten Teilbaum und besuche zuletzt die Wurzel. • Inorder: Traversiere zuerst den linken Teilbaum, besuche dann die Wurzel und traversiere danach den rechten Teilbaum. 1 3 2 4 5 8 6 7 9 Für den abgebildeten Baum ergeben sich bei den drei Travesierungen die folgenden Besuchssequenzen: • Preorder: 1, 2, 4, 5, 8, 9, 3, 6, 7 • Postorder: 4, 8, 9, 5, 2, 6, 7, 3, 1 • Inorder: 4, 2, 8.5, 9, 1, 6, 3, 7 Eine der wichtigsten Anwendungen von Inorder-Traversierungen ist mit dem Begriff des binären Suchbaums verbunden: Definition: Ein binärer Suchbaum speichert in seinen Knoten Zahlen und hat die Eigenschaft, dass für jeden inneren Knoten v alle Zahlen aus dem linken Unterbaum von v kleiner oder gleich der Zahl in v sind und allen Zahlen im rechten Unterbaum von v größer oder gleich der Zahl in v sind. 6 3 2 11 9 5 8 13 9 Für einen binären Suchbaum gibt der Inorder-Durchlauf die geordnete Folge der gespeicherten Zahlen wieder. Das folgende Beispiel zeigt einen binären Suchbaum und mit den gestrichelten Pfeilen die Inorder-Traversierung. In einem binären Suchbäumen kann man sehr leicht überprüfen, ob eine bestimmte Zahl x gespeichert ist. Man beginnt den Suchprozess in der Wurzel und vergleicht x mit Zahl a in der Wurzel r. Es gibt 4 Fälle: 1. Ist x = a, dann hat man x bereits gefunden und ist fertig; 2. Ist x 6= a und r ist ein Blatt, dann kommt x im Baum nicht vor; 3. Ist r ein innerer Knoten und x < a, dann kann x nur im linken Teilbaum sein (dessen Wurzel das linke Kind von r ist) und man wiederholt dort die Suche rekursiv. 4. Ist r ein innerer Knoten und x > a, dann kann x nur im rechten Teilbaum sein (dessen Wurzel das rechte Kind von r ist) und man wiederholt dort die Suche rekursiv. Die Suchzeit ist proportional zur Höhe des Baums, also O(h).