Vorlesung 8 Graphen und Bäume Inhalt 1 Graphen 1.1 Kategorisierung von Graphen 1.2 Graphen als Datenstruktur 1.2.1 Die Adjazenzmatrix 1.2.2 Adjazenzlisten 1.2.3 Inzidenzmatrix 2 Bäume und Wälder 2.1 Binärbäume 2.2 Bäume und Binärbäume 2.3 Suchbäume 3 Heaps 4 Eine einfache Implementierung einer Klasse „Baum“ 1 3 6 7 8 9 9 11 12 12 13 15 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E Vorlesung 8 Lernziele: Das elementare Modellierungswerkzeug für Datenstrukturen „Graph“ soll kennengelernt und seine unterschiedlichen Beschreibungsformen beherrscht werden. Wichtig ist auch, die konkreten Ausprägungsformen wie Bäume und Halden zu verstehen. 1 Graphen Ein Graph ist anschaulich ein Gebilde aus Knoten (auch Ecken oder Punkte), die durch Kanten (Verbindungslinien) verbunden sein können. Obwohl Graphen vielfach durch eine Zeichnung dargestellt werden, lassen sich Graphen als „rein“ mathematische Strukturen auffassen, die zum Beispiel durch Zeichnungen nur repräsentiert oder visualisiert werden. Dies bedeutet vor allem, dass verschiedene Bilder denselben Graphen darstellen können. Knoten 1 1 2 2 Kante 3 4 3 4 Abbildung 1: Alternative Visualisierungen desselben Graphen Aus diesem Grund definieren wir uns zuerst einen Graph G rein formal. 1 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E Definition Ein Graph G ist ein geordnetes Paar zweier Mengen: G = (V, E). Dabei bezeichnet V die Menge der im Graph enthaltenen Knoten und E die Menge der Kanten des Graphen. Die Bezeichnungen der Mengen entstammen dem Englischen: V für vertex (engl. für Knoten) und E für edge (engl. für Kante). Graphen sind informatische bzw. mathematische Modelle insbesondere für Netzstrukturen, beispielsweise für das Netz der deutschen Autobahnen (Knoten: Auf- und Abfahrten, Dreiecke und Kreuze, Kanten: Fahrstrecken) oder das Streckennetz einer Fluggesellschaft. 1 5 5 661 2 661 3 1 2 4 3 17 4 17 5 5 5 7 8 661 7 8 66 20 19 18 9 66 19 18 66 18 20 66 66 13 648 14 19 32 9 648 13 648 21 66 18 32 21 19 14 648 20 15 5 15 21 16 20 21 16 661 17 52 22 50 49 3 5 3 49 51 22 50 50 17 3 52 3 661 18 661 18 5 Abbildung 2: Das Autobahnnetz um Frankfurt am Main als Beispiel für einen Graphen (Quelle: Die Grundlage der linken Karte findet sich in der Wikipedia http://de.wikipedia.org/wiki/Bild:Mk_Frankfurt_Nachbargemeinden.png , Autor: Michael König, September 5, 2005, License: GNU-FDL. Die dort vorgefundene Graphik wurde um das Autobahnnetz ergänzt und in den „amerikanischen“ Stil übertragen: Die Graphstruktur wird unmittelbar deutlich. Bitte beachten Sie an diesem Beispiel: Quasi alles verfügbare Kartenmaterial und andere Bilder, auch aus dem Internet, sind urheberrechtlich geschützt und dürfen nicht frei genutzt werden, bis auf ganz wenige Ausnahmen (z.B. oben). Wir können in diesem Beispiel die Elemente „Auffahrt“ dadurch eindeutig machen, das wir sie mit einer Autobahnnummer-Auffahrtnummer kennzeichnen, also z.B. „A3-49“ für die Auffahrt Kelsterbach (unten links). Die Dreiecke und Kreuze würden dann allerdings zwei Bezeichnungen tragen, für das Frankfurter Kreuz A3-50 und A5-22; was man durch eine geeignet gewählte Elementbezeichnung, z.B. „A3-50;A5-22“ behandeln kann. Offensichtlich hat unser Beispiel einige besondere Eigenschaften, z.B. führt nie eine Kante vom Knoten v nach v, auch gibt es immer höchstens eine Kante von v nach w. Dies führt uns direkt dazu, verschiedene Unterarten von Graphen zu unterschieden und kurz zu betrachten. 2 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E 1.1 Kategorisierung von Graphen Man unterscheidet in der Graphentheorie vor allem zwischen ungerichteten und gerichteten Graphen, Graphen mit Mehrfachkanten (Multigraphen) und ohne Mehrfachkanten, sowie Hypergraphen. Knoten und Kanten können auch mit Namen versehen sein, dann spricht man von einem benannten (labeled) Graphen. In Multigraphen können zwei Knoten durch mehrere Kanten verbunden sein, was in einfachen Graphen nicht erlaubt ist. Statt mehrere Linien zwischen zwei Punkten zu zeichnen, kennzeichnet man Mehrfachkanten auch häufig durch ihre Vielfachheit (Gewicht). In gerichteten Graphen oder auch orientierten Graphen werden Kanten statt durch Linien durch Pfeile gekennzeichnet, wobei der Pfeil vom ersten zum zweiten Knoten zeigt. Bei Hypergraphen verbindet eine Kante (auch Hyperkante genannt) nicht nur zwei, sondern mehrere Knoten gleichzeitig; sie „splittet sich auf“. Bei Hypergraphen mit vielen Kanten wird diese Darstellung sehr schnell unübersichtlich. Dabei ist die Menge E der Kanten • • in ungerichteten Graphen ohne Mehrfachkanten (auch schlichter oder einfacher „Graph“ genannt) eine Teilmenge aller möglichen 2-elementigen Teilmengen von V: E ⊆ { {i, j} i, j ∈V }. (Unser obiges Autobahn-Beispiel, aber dieses ist noch spezieller, siehe unten) in gerichteten Graphen ohne Mehrfachkanten eine Teilmenge des kartesischen Produktes V x V: E ⊆ { (i, j ) i, j ∈V } Beachten Sie den Unterschied zu ungerichteten Graphen: Hier ist ein Element von E ein geordnetes Paar (Tupel, nicht Menge), gibt also die Richtung an, meist (von, nach) oder (Startknoten i und Endknoten j ). Dies wird dann als Pfeil gezeichnet. Eine andere Bezeichnung für gerichtete Graphen ist Digraph (Directed Graph). 3 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E • in ungerichteten Graphen mit Mehrfachkanten eine Multimenge1 über der Menge aller 2-elementigen Teilmengen von V, • in gerichteten Graphen mit Mehrfachkanten eine Multimenge über dem kartesischen Produkt V x V, • in Hypergraphen eine Teilmenge der Potenzmenge von V. ungerichteter Graph gerichteter Graph ungerichteter Graph gerichteter Graph ohne Mehrfachkanten ohne Mehrfachkanten mit Mehrfachkanten mit Mehrfachkanten (Multigraph) (Multigraph) Abbildung 3: Visualisierung der Graphenarten Man sagt, falls der Graph G • • ein ungerichteter Graph ohne Mehrfachkanten ist und die Kante e zu E(G) gehört, ist e eine ungerichtete Kante von G, gerichteter Graph ohne Mehrfachkanten ist und die Kante e zu E(G) gehört, ist e eine gerichtete Kante von G. Von einer gerichteten Kante e = (v,w) bezeichnet man v als Startknoten und w als Endknoten der Kante e. Ein wichtiges Beispiel für einen gerichteten Graphen ist das World-Wide-Web. Hierbei modellieren wir die Webseiten als Knoten und setzen eine Kante von Webseite v nach Webseite w, wenn die Webseite v einen Link auf die Webseite w hat. Hat eine Kante e eines Graphen die Form (v, v) oder {v, v} , so spricht man von einer Schleife (oder Schlinge). Gerichtete Graphen ohne Schleifen nennt man schleifenlos oder schleifenfrei. Das ist in unserem Autobahnbeispiel offensichtlich der Fall. gegenüber dem gewöhnlichen Mengenbegriff können in einer Multimenge Elemente mehrfach vorkommen. Dementsprechend haben die für Multimengen verwendeten Mengenoperationen eine modifizierte Bedeutung. 1 4 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E Gelegentlich schließt man diesen Fall schon in der Definition eines Graphen aus. Man ergänzt die Definition also um i≠j ungerichteter Graph: E ⊆ { {i, j} i, j ∈V , i ≠ j} gerichteter Graph: E ⊆ { (i, j ) i, j ∈V , i ≠ j} Als Knotenzahl n(G)=|V(G)| eines Graphen G bezeichnet man die Anzahl seiner Knoten, als Kantenzahl m(G)=|E(G)| die Anzahl seiner Kanten. In Multigraphen summiert man über die Vielfachheit der Kanten. Einen Graph, dessen Knotenmenge endlich ist, nennt man endlicher Graph. Zwangsläufig ist in diesen auch die Kantenmenge endlich. Im Gegensatz dazu nennt man einen Graph, dessen Knotenmenge unendlich ist, unendlicher Graph. In der Praxis betrachtet man meist nur endliche Graphen und lässt daher das Attribut "endlich" weg, während man "unendliche Graphen" explizit kennzeichnet. Oft erweitert man Graphen G = (V,E) zu knotengefärbten Graphen, indem man das Tupel (V,E) zu einem Tripel (V,E,f) ergänzt, wobei f eine Abbildung von V in die Menge der natürlichen Zahlen ist. Anschaulich gibt man jedem Knoten damit eine Farbe, gekennzeichnet durch eine Zahl. Man könnte dies in unserem Beispiel nutzen, um einfache Auffahrten von Dreiecken und Kreuzen zu unterscheiden. Statt der Knoten kann man in Graphen ohne Mehrfachkanten und in Hypergraphen auch die Kanten färben und spricht dann von einem kantengefärbten Graph. Dazu erweitert man ebenfalls das Tupel (V,E) zu einem Tripel (V,E,f), wobei f aber eine Abbildung von E (statt von V) in die Menge der natürlichen Zahlen ist. Anschaulich gibt man jeder Kante damit eine Farbe. In Graphen mit Mehrfachkanten ist dies zwar prinzipiell auch möglich, aber schwieriger zu definieren, insbesondere, wenn Mehrfachkanten entsprechend ihrer Vielfachheit mehrere verschiedene Farben zugeordnet werden sollen. Statt von knoten- bzw. kantengefärbten Graphen spricht man von knoten- bzw. kantengewichteten Graphen, falls f statt in die natürlichen Zahlen in die reellen Zahlen abbildet. Knoten- bzw. kantengefärbte Graphen sind also Spezialfälle von knoten- bzw. kantengewichteten Graphen. Man bezeichnet f(v) bzw. f(e) auch als Gewicht des Knotens v bzw. der Kante e. Zur Unterscheidung spricht man auch von Knotengewicht bzw. Kantengewicht. Analog gibt es auch benannte Graphen (V, E, f, g), bei denen Knoten und/oder Kanten einen Namen (engl. label) tragen, und die Abbildungen f bzw. g den Knoten bzw. Kanten einen Namen zuordnen. Die zuvor abgebildeten Beispiele sind benannte Graphen, bei denen die Knoten mit Buchstaben und Zahlen benannt wurden. Dies wird bei Visualisierungen fast immer gemacht, um besser über den Graphen diskutieren zu können. Es gibt einige spezielle Typen von Graphen, die eine besondere Bedeutung haben. Hierzu folgende Definitionen: Sei G = (V,E) ein gerichteter oder ungerichteter Graph. Dann gilt: (a) Zwei Knoten v; w ∈V heißen benachbart oder adjazent, falls sie durch eine Kante verbunden sind. (b) Eine Kante ist mit einem Knoten inzident, wenn der Knoten ein Endpunkt der Kante ist. 5 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E (c) Eine Folge (v0, v1, …, vm) heißt ein Weg in G, falls für jedes i mit 0 ≤ i < m gilt (vi; vi+1) ∈ E (für gerichtete Graphen) oder {vi; vi+1} ∈ E (für ungerichtete Graphen). Die Weglänge ist m ist die Anzahl der Kanten des Wegs. Ein Weg, bei dem kein Knoten zweimal auftritt, ist ein einfacher Weg oder Pfad. Ein Weg heißt Zyklus, wenn v0 = vm bei der Folge (v0, … vm-1) gilt, der Weg also geschlossen ist. Ein geschlossener Pfad wird auch Kreis genannt. (d) Ein gerichteter Graph ist azyklisch, falls er keine Zyklen besitzt. (e) Ein ungerichteter Graph ist zusammenhängend, wenn es für je zwei Knoten v; w ∈ V einen Weg von v nach w gibt. Mit diesen Grundbegriffen können wir eine sehr wichtige Art von Graphen definieren, nämlich: Ein Baum ist ein zusammenhängender, ungerichteter oder gerichteter, azyklischer Graph ohne Mehrfachkanten. Ein in disjunkte Bäume zerlegbarer, ungerichteter Graph heißt Wald. Vermutlich fühlen Sie sich von der Menge all dieser Definitionen überfordert. Das ist verständlich. Merken Sie sich auf jeden Fall folgende Begriffe, Definitionen und Unterscheidungen: • gerichtete Graphen vs. ungerichtete Graphen • Schleifen in Graphen und schleifenlose Graphen • Weg in einem Graphen • azyklischer Graph und zusammenhängender Graph Deutlich mehr zu diesem Thema erfahren Sie dann im 2. Fachsemester in der Veranstaltung „Algorithmen und Datenstrukturen“. 1.2 Graphen als Datenstruktur Ein Graph als Datentyp sollte mindestens die folgenden Operationen sind haben • Einfügen (Kante, Knoten) • Löschen (Kante, Knoten) • Finden eines Objekts (Kante, Knoten). Die bekanntesten Repräsentationen von Graphen als Datenstruktur sind • die Adjazenzmatrix (Nachbarschaftsmatrix) • die Adjazenzliste (Nachbarschaftsliste) • die Inzidenzmatrix (Knoten-Kanten-Matrix, seltener genutzt) Beginnen wir mit der Adjazenzmatrix. 6 V O R L E S U N G E N 1.2.1 8 : G R A P H E N U N D B Ä U M E Die Adjazenzmatrix2 Ein Graph mit n Knoten kann durch eine n×n-Matrix repräsentiert werden. Dazu nummeriert man die Knoten von 1 bis n durch und trägt in die Matrix aus n Zeilen und n Spalten die Beziehungen der Knoten zueinander ein. In ungerichteten Graphen ohne Mehrfachkanten wird dazu in die i-te Zeile und j-te Spalte eine 1 eingetragen, wenn der i-te und j-te Knoten benachbart oder adjazent, d.h. durch eine Kante verbunden sind. Andernfalls wird eine 0 eingetragen. Da die Relation „benachbart“ symmetrisch ist (wenn Knoten i zu Knoten j benachbart ist, dann ist auch Knoten j zu Knoten i benachbart), ist auch die Adjazenzmatrix symmetrisch. Für die Koeffizienten der Matrix gilt also aij = aji . Eine dazu gleichwertige Aussage ist, dass die Matrix gleich ihrer Transponierten ist: A = AT.. Bei symmetrischen Matrizen reicht es prinzipiell aus, alle Elemente oberhalb oder unterhalb der Hauptdiagonalen zu anzugeben. Häufig betrachtet man ungerichtete Graphen ohne Schleifen, dann steht in der Hauptdiagonalen der Matrix überall die Null. In gerichteten Graphen ohne Mehrfachkanten wird in die i-te Zeile und j-te Spalte eine 1 eingetragen, wenn der i-te Knoten Vorgänger des j-ten Knoten ist, also (i,j) ∈ E. Andernfalls wird eine 0 eingetragen. 1 1 2 3 1 1 1 3 1 2 1 2 4 4 1 3 1 4 Hypergraphen lassen sich nicht durch eine Adjazenzmatrix darstellen. Abbildung 4: Repräsentation eines gerichteten Graphen (mit einer Schleife) in einer Adjazenzmatrix A In Graphen mit Mehrfachkanten trägt man in die i-te Zeile und j-te Spalte die Vielfachheit von {i,j} in ungerichteten Graphen bzw. (i,j) in gerichteten Graphen ein. Auch an dieser Stelle sieht man leicht, warum Graphen ohne Mehrfachkanten als Spezialfälle von Graphen mit Mehrfachkanten betrachtet werden können. In kantengewichteten Graphen trägt man häufig auch das Kantengewicht der jeweiligen Kante ein. Hypergraphen lassen sich nicht durch Adjazenzmatrizen darstellen (Warum nicht?). In vielen Programmiersprachen sind Arrays als Datentyp-Primitive für Matrizen vorhanden (siehe Vorlesungskapitel V06). Nicht so in Python. Hier implementieren wir ein veränderliches Feld durch geschachtelte Listen. Beispielsweise ist die Adjazenzmatrix aus Abbildung 4 A = [ [1,1,1,0] , [0,0,1,0] , [0,0,0,1] , [1,0,0,0] ] Die Zeilen sind jeweils eigene Elemente in der Liste und enthalten als Liste wiederum die Spaltenwerte als Elemente. Zugreifen kann man dann auf Element (i,j) der Adjazenzmatrix durch 2 Adjazent bedeutet benachbart, abgeleitet vom lateinischen adiacēns, aus ad = bei und iacēre = liegen. 7 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E print A[i][j] 1.2.2 Adjazenzlisten Eine Adjazenzliste macht von der Tatsache gebrauch, dass Graphen meist endlich sind und deshalb die Elemente linear abgezählt werden können. Die Liste wird in ihrer einfachsten Form durch eine einfach verkettete Liste aller Knoten des Graphen dargestellt. Jeder Knoten notiert dabei eine Liste aller seiner Nachbarn (in ungerichteten Graphen) bzw. Nachfolger in gerichteten Graphen. Vielfachheiten der Elemente (Knotengewichte und Kantengewichte) werden meist in Attributen der einzelnen Elemente gespeichert. Je nach Problemstellung kann es notwendig sein, statt einer einfach verketteten Liste eine doppelt verkettete Liste zu verwenden und damit in gerichteten Graphen zusätzlich zur Liste der Nachfolger auch eine Liste der Vorgänger zu verwalten. In der Praxis verwendet man daher meist diese Form der Repräsentation. Unser obiges Beispiel aus Abbildung 4 stellt sich dann wie folgt dar: 1 [1,2,3] 2 [3] 3 [4] 4 [1] 1 2 3 4 Abbildung 5 Repräsentation durch eine Adjazenzliste Zur direkten Umsetzung in Python könnte man ebenso verschachtelte Listen verwenden, etwa (Knoten-) Listen von (Kanten-) Listen: Adjazenzliste = [[1,2,3],[3],[4],[1]] Die elementaren Operationen lassen sich dann etwa wie folgt implementieren: • Einfügen-Kante: prüfen, ob schon vorhanden, sonst Kantenliste.append • Einfügen-Knoten: (ggf. prüfen, ob schon vorhanden), sonst Knotenliste.append • Lösche-Kante: prüfen, ob vorhanden, dann Kantenliste.remove • Lösche-Knoten: Knoten selbst löschen: Knotenliste.remove, dann aber auch alle Verweise auf diesen Knoten löschen … etwas aufwendiger, geht aber. Adjazenzlisten sind zwar aufwändiger zu implementieren und zu verwalten als reine Matrizen, bieten aber eine Reihe von Vorteilen gegenüber Adjazenzmatrizen. Zum einen verbrauchen sie stets nur linear viel Speicherplatz, was insbesondere bei dünnen Graphen (also Graphen mit wenig Kanten) von Vorteil ist, während die Adjazenzmatrix quadratischen Platzbedarf bezüglich der Anzahl Knoten besitzt (dafür aber kompakter bei dichten Graphen, also Graphen mit vielen Kanten ist). Zum anderen lassen sich viele graphentheoretische Probleme nur mit Adjazenzlisten in linearer Zeit lösen. Derartige Betrachtungen überschreiten aber unseren aktuellen Horizont: Auch dieses wird in der Veranstaltung „Algorithmen und Datenstrukturen“ vertieft werden. Wir wollen stattdessen noch kurz eine Implementierung mit einem Dictionary betrachten. Wenn wir hierbei die Knotenbezeichnungen als Schlüssel nehmen, dann garantiert uns dies Eindeutigkeit (was wir ja wollen) und einen einfachen Zugriff über die Knotenbezeichnung. 8 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E >>> # Initialisierung >>> Adjazenzliste = {"A":["A","B","C"],"B":["C"],"C":["D"],"D":["A"]} >>> Adjazenzliste {'A': ['A', 'B', 'C'], 'C': ['D'], 'B': ['C'], 'D': ['A']} >>> Wir haben damit die folgende Struktur aufgesetzt:, A →A, →B, →C, C →D, B →C, D →A. die den Graphen charakterisiert. 1.2.3 Inzidenzmatrix Ein Graph mit n Knoten und m Kanten kann auch durch eine n×m-Matrix repräsentiert werden. Dazu nummeriert man die Knoten von 1 bis n und die Kanten von 1 bis m durch und trägt in die Matrix die Beziehungen der Knoten zu den Kanten ein. Jede Spalte der Inzidenzmatrix enthält dazu genau zwei von Null verschiedene Einträge: In ungerichteten Graphen zweimal die 1 und in gerichteten Graphen einmal die 1 (Endknoten) und einmal die –1 (Startknoten). Wie bereits angedeutet ist diese Art der Datenstruktur ähnlich der Adjazenzmatrix nicht besonders effizient für ausgedünnte Graphen. 2 Bäume und Wälder Im vorigen Abschnitt definierten wir einen Baum als einen zusammenhängenden Graph, der zyklenfrei ist. Lassen wir die Eigenschaft „zusammenhängend“ für den ungerichteten Baum weg, so zerfällt er in unzusammenhängende Teilstücke, die wir in ihrer Gesamtheit als Wald bezeichnen, da jedes zusammenhängende Teilstück wieder ein Baum ist. Als Wald bezeichnet man also in der Graphentheorie einen ungerichteten Graphen ohne Kreisverbindung (Zyklus), der nicht zusammenhängend sein muss, aber kann. Jeder ungerichtete Baum bildet also auch einen Wald für sich. Betrachtungen über Wälder lassen sich damit auch auf ungerichtete Bäume übertragen. Umgekehrt sind aber auch Betrachtungen über ungerichtete Bäume häufig leicht auf Wälder übertragbar. Neben ungerichteten Bäumen betrachtet man auch gerichtete Bäume, die häufig auch als gewurzelte Bäume bezeichnet werden und sich weiter in In-Trees und Out-Trees unterscheiden lassen. Die Struktur entspricht im Wesentlichen der von ungerichteten Bäumen, jedoch gibt es einen einzigen, ausgezeichneten Knoten, den man Wurzel nennt und für den die Eigenschaft gilt, dass alle Kanten von diesem wegzeigen (Out-Tree) oder zu diesem hinzeigen (In-Tree). 9 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E Abbildung 6: Out-Tree und In-Tree Als Datenstruktur werden meist nur Out-Trees verwendet. Dabei können, ausgehend von der Wurzel, mehrere gleichartige Objekte so miteinander verkettet werden, dass die lineare Struktur der Liste aufgebrochen wird und eine Verzweigung stattfindet. Der maximale Ausgangsgrad wird als Ordnung eines Out-Trees bezeichnet und alle Knoten mit Ausgangsgrad 0 bezeichnet man als Blätter. Wie bei ungerichteten Bäumen bezeichnet man auch in gewurzelten Bäumen alle Knoten, die kein Blatt sind, als innere Knoten. Manchmal schließt man die Wurzel dabei aber aus. Als Tiefe einen Knotens bezeichnet man die Länge des Pfades von der Wurzel zu ihm und als Höhe des Out-Trees die Länge eines längsten Pfades (in obiger Abbildung: vier). Die Terminologie von Bäumen stammt vielfach aus der Welt der menschlichen Stammbäume. So bezeichnet man für einen von der Wurzel verschiedenen Knoten v den Knoten, durch den er mit einer eingehenden Kante verbunden ist als Vater, Vaterknoten, Elternknoten oder Vorgänger von v. Als Vorfahren von v bezeichnet man alle Knoten, die entweder Vater von v oder Vorgänger des Vaters sind. Umgekehrt bezeichnet man alle Knoten, die von einem beliebigen Knoten v aus durch eine ausgehende Kante verbunden sind als Kinder, Kinderknoten, Sohn oder Nachfolger von v. Als Nachfahren von v bezeichnet man Kinder von v oder deren Nachfahren. Als Geschwister oder Geschwisterknoten werden in einem Out-Tree Knoten bezeichnet, die den gleichen Vater besitzen. Out-Trees lassen sich auch rekursiv definieren: Sie bestehen aus einem Knoten w, der die Wurzel des Baumes darstellt, welcher ausschließlich mit den Wurzeln „knotendisjunkter“ (die Mengen der Knoten sind disjunkt) Out-Trees T1, T2, ..., Tn verbunden ist, und zwar in Richtung der Wurzeln von T1, T2, ..., Tn Bei Suchbäumen sind die Elemente in der Baumstruktur geordnet abgelegt, so dass man schnell Elemente im Baum finden kann. Dazu definieren wir uns eine Ordnung innerhalb der Knoten: • Ein partiell geordneter Baum T ist ein Baum, dessen Knoten markiert sind dessen Markierungen aus einem geordneten Wertebereich stammen in dem für jeden Teilbaum T' mit der Wurzel x gilt: Alle Knoten aus T' sind größer markiert als x oder gleich x. Also: 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. 10 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E 2.1 Binärbäume Da Bäume zu den meist verwendeten Datenstrukturen in der Informatik gehören, gibt es viele Spezialisierungen. So ist bei Binärbäumen die Anzahl der Kinder eines Knotens höchstens zwei. Sie werden als „linkes Kind“ und „rechtes Kind“ unterschieden und bezeichnet. • In balancierten Bäumen gilt zusätzlich, dass sich die Höhen des linken und rechten Teilbaums an jedem Knoten höchstens um eins unterscheiden. • Ein Binärbaum heißt geordnet, wenn jeder innere Knoten genau ein linkes und eventuell zusätzlich ein rechtes Kind besitzt (und nicht etwa nur ein rechtes Kind). • Er ist voll oder strikt, wenn jeder Knoten entweder Blatt ist (also kein Kind besitzt), oder aber zwei Kinder (also sowohl ein linkes wie ein rechtes) besitzt. • Er ist vollständig, wenn alle Blätter die gleiche Tiefe besitzen. Ein vollständiger Binärbaum Bn der Höhe n hat genau ⇒ 2i Knoten in Tiefe i, insbesondere also o 2n+1-1 Knoten, o 2n-1 innere Knoten, ⇒ 2n Blätter wenn mit „Höhe n“ die Länge des Pfades zu einem tiefsten Knoten bezeichnet wird. • Man unterscheidet hier weiter binäre Suchbäume, also Bäume, für die sich die Elemente (Knoten) x und y so anordnen lassen, dass die Bedingungen der Inhalte (Schlüssel, key) gelten o Die Elemente y im linken Teilbaum von x erfüllen key(y) < key(x) o Die Elemente y im rechten Teilbaum von x erfüllen key(y) > key(x) Binärbäume kann man zum Abarbeiten aller Knoten in verschiedener Art und Weise durchlaufen (traversieren). Man unterscheidet dabei die Reihenfolge, in der jeweils die Wurzel, das linke und das rechte Kind eines Knotens abgearbeitet werden. pre-order (W–L–R): wobei zuerst die Wurzel (W) betrachtet wird und anschließend zuerst der linke (L), dann der rechte (R) Teilbaum durchlaufen wird, in-order (L–W–R): wobei zuerst der linke (L) Teilbaum durchlaufen wird, dann die Wurzel (W) betrachtet wird und anschließend der rechte (R) Teilbaum durchlaufen wird und post-order (L–R–W): wobei zuerst der linke (L), dann der rechte (R) Teilbaum durchlaufen wird und anschließend die Wurzel (W) betrachtet wird. level-order Beginnend bei der Wurzel, werden die Ebenen von links nach rechts durchlaufen. Solche Transversierungen kann man am elegantesten rekursiv formulieren. Hier ist eine solche Formulierung für eine pre-order Transversierung: Funktion Preorder (Baum) W ← Baum.Wurzel If Baum.Links <> NULL L ← Preorder(Baum.Links) If Baum.Rechts <> NULL // Ordnung: W-L-R // W:= Wurzel des übergebenen Baumes // Existiert ein linker Unterbaum? // ja: L:= Preorder vom linken Unterbaum // Existiert ein rechter Unterbaum? 11 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E R ← Preorder(Baum.Rechts) // ja: R:= Preorder vom rechten Unterbaum Return W°L°R // Rückgabe: Verkettung aus W, L und R Die entsprechenden Formulierungen für die anderen Transversionsrichtungen sind analog. 2.2 Bäume und Binärbäume In Programmiersprachen ohne dynamische Listen hat sich auch ein Verfahren bewährt, bei dem ein allgemeiner Baum durch einen Binärbaum implementiert wird. Betrachten wir dazu einen allgemeinen Baum in Abbildung 7(a). Abbildung 7: (a) Allgemeiner Baum und (b) die Binärversion davon Dieser Baum hat mehrere Ebenen mit mehreren Geschwistern pro Ebene. Um ihn in einen Binärbaum umzuwandeln, legen wir für alle Kinder eine Reihenfolge fest. • Das erste Kind eines Knoten wird als „linkes Kind“ festgelegt. • Der Geschwisterknoten eines „linken Kindes“ wird jeweils zum „rechten Kind“ dieses linken Kindes. Diese beiden Regeln auf jede Ebene angewendet ergibt aus dem allgemeinen Baum den Binärbaum in Abbildung 7(b). 2.3 Suchbäume Die binären Suchbäume trennt man noch auf in die balancierten binären Suchbäume (AVLBäume3) und die allgemeinen (nicht-binären) balancierten B-Bäumen, sowie diversen Varianten, z.B. den B*-Bäumen (Bäumen sehr geringer Höhe). Achtung: das „B“ steht hier nicht für Binär, sondern für balanciert(!) und geordnet. B-Bäume müssen also keine Binärbäume sein, wie wir in PRG-1 Teil 3 kennen lernen werden! Durch die Balancierung wird neben der effizienten Suche einzelner Datenelemente auch das schnelle sequenzielle Durchlaufen aller Datenelemente unterstützt 3 entwickelt von Adelson-Velsky und Landis, 1962 12 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E 4 2 1 6 3 7 5 Abbildung 8: Balancierter binärer Suchbaum Spezialisierungen von B-Bäumen sind wiederum 2-3-4-Bäume oder so genannte Rot-SchwarzBäume. Bäume sind in ihrem Aufbau zwar mehrdimensional, jedoch in der Verkettung der Objekte oft unidirektional. Die Verkettung der gespeicherten Objekte beginnt bei der Wurzel des Baums und von dort in Richtung der Blätter des Baums. Aufgrund der einfachen Struktur der Bäume kann die Komplexität von auf Bäumen arbeitenden Algorithmen meist gut abgeschätzt werden. Oft arbeiten die Algorithmen mit einem Baum als Datenstruktur schneller als andere Algorithmen für dasselbe Problem. Um alle Knoten eines Graphen effizient betrachten zu können, werden gerne Bäume bzw. Wälder aus dem Graphen konstruiert. Bäume und Graphen werden Sie in der Theoretischen Informatik („Grundlagen der Informatik“) noch intensiv studieren. 3 Heaps In einem Heap (dtsch: Haufen, Halde) können Objekte oder Elemente strukturiert abgelegt und aus diesem wieder entnommen werden. Sie dienen damit der Speicherung von Mengen. Den Elementen ist dabei ein Schlüssel zugeordnet, der die Priorität der Elemente festlegt. Häufig werden auch die Elemente selbst als Schlüssel verwendet. Die Elemente gehorchen folgenden Forderungen: • • • Alle Prioritäten sind verschieden (z.B. Zufallszahlen) Die Wurzel hat die kleinste Priorität Wenn y ein Kind ist von x , so gilt: prio(y) > prio(x) In Abbildung 9 ist ein solcher Heap gezeigt. 2 3 4 6 5 7 9 Abbildung 9: Heap einer Prioritätsordnung Die Datenstruktur eines Heap vereint in der Implementierung die Datenstruktur eines Baums mit den Operationen einer Warteschlange, deren Elemente nach Prioritäten geordnet sind. 13 V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E Häufig hat der Heap neben den minimal nötigen Operationen einer Warteschlange wie insert, remove und extractMin (Schlüssel höchster Prio) auch noch weitere Operationen wie merge oder changeKey. Je nach Reihenfolge der Priorität in der Vorrangwarteschlange wird für eine Abfrage ein Min-Heap (min. Schlüssel in der Wurzel) oder ein Max-Heap (max. Schlüssel in der Wurzel) verwendet. Über die Menge der Schlüssel muss eine totale Ordnung festgelegt sein, über welche die Reihenfolge der eingefügten Elemente festgelegt wird. Beispielsweise könnte die Menge der ganzen Zahlen zusammen mit der Kleiner-Relation (<) als Schlüssel-Menge fungieren. Der Begriff Heap wird häufig so als bedeutungsgleich zu einem partiell geordneten Baum verstanden. Gelegentlich versteht man einschränkend darunter nur eine besondere Implementierungsform eines partiell geordneten Baums, nämlich die Einbettung in ein Array. Verwendung finden Heaps vor allem dort, wo es darauf ankommt, schnell ein Element mit höchster Priorität aus dem Heap zu entnehmen, beispielsweise bei Vorrangwarteschlangen. Spezialisierungen des Heap sind der Binäre Heap, der Binomial-Heap und der Fibonacci-Heap. Eine weitere Spezialisierung sind Treaps: Der Treap vereinigt Eigenschaften von Bäumen und Heaps in sich: Binary search Tree + Heap. Eine grobe Übersicht über Strukturen und Spezialisierungen von Graphen ist in folgender Abbildung wiedergegeben: Graph gerichtet azyklisch ··· Baum tree M-Way binary tree quadtree Suchbaum Halde search tree heap octtree ··· B-Tree ·· 14 · V O R L E S U N G E N 8 : G R A P H E N U N D B Ä U M E 4 Eine einfache Implementierung einer Klasse „Binärbaum“ Als Beispiel für die Definition und Benutzung von Klassen betrachten wir eine unvollständige Implementierung eines binären Baums, der einen einfachen Suchbaum über eine linear angeordnete Ordnung eines Attributs (data) ermöglicht. Er besteht aus Knoten, die jeweils nur zwei Verzweigungen haben: eine rechte und eine linke Verzweigung. class Node: def __init__(self, data=None): self.data = data self.left = None self.right = None def __str__(self): return "[%s,%i,%i]"%(str(self.data), id(self.left), id(self.right)) class BinaryTree: def __init__(self): self.root = None # Attribut “Baumwurzel” def add(self, data): # Hinzufügen eines Knotens if self.root == None: self.root = Node(data) else: curnode = self.root lastnode = self.root while curnode != None: lastnode = curnode if data < curnode.data: curnode = curnode.left # links laufen direction = -1 else: curnode = curnode.right # rechts laufen direction = +1 if direction == -1: lastnode.left = Node(data) # links einfügen else: lastnode.right = Node(data) # rechts einfügen def _prchilds(self, node): # Ausdruck aller Knoten des Baums if node != None: return "(%s; %s; %s)" % (self._prchilds(node.left), node, self._prchilds(node.right)) else: return "nil" Sie besteht aus der Klassendefinition eines einzelnen Knotens, der selbst über seinen Datenbehälter irgendeines Typs und zwei Referenzen für zwei Verbindungen zu anderen Knoten verfügt. Die Referenzen sind noch leer und werden erst bei ihrer Benutzung gesetzt. Wird beim Baum ein Knoten hinzugefügt, so wird beim ersten Mal die Wurzel erzeugt, danach der Baum durchsucht, bis das Ende erreicht ist und dann das Datum als neuer Knoten angehängt. Für die Anordnung der Daten ist eine feste Ordnung definiert. Natürlich fehlen hier noch weitere Methoden, wie das Löschen von Elementen oder das systematische Durchsuchen des Baumes nach einem Schlüssel. Dies sei dem interessierten Leser als Übung ans Herz gelegt. 15