Graphen, Baeume..

Werbung
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
Herunterladen