Zum Inhalt springen
Zurück zur Themenübersicht

Praktische Informatik

Bäume, Graphen und Wegsuche

Zwei Strukturen, mit denen sich Verzeichnisse, Netze und Navigationsprobleme gleichermaßen beschreiben lassen.

Benötigte Grundlagen

Dieses Vorwissen brauchst du für das Kapitel. Schau kurz nach, wenn dir etwas davon nicht mehr präsent ist, sonst leg direkt los.

Einführung

Ein Dateisystem, ein Stammbaum, ein Straßennetz, ein soziales Netzwerk, das Internet: Auf den ersten Blick haben diese fünf nichts gemeinsam.

Auf den zweiten sind sie dasselbe. In allen fünf gibt es Dinge und Verbindungen zwischen ihnen, und alles Weitere folgt daraus.

Genau das leisten Bäume und Graphen: Sie sind keine Datenstrukturen für einen bestimmten Zweck, sondern eine Sprache, in der sich sehr verschiedene Aufgaben formulieren lassen. Wer ein Problem als Graph beschrieben hat, kann fertige Verfahren darauf anwenden, statt jedes Mal neu anzufangen.

Das kannst du nach diesem Kapitel

  • die Begriffe , Kante, Wurzel, Blatt, Tiefe verwenden.

  • einen binären Suchbaum aufbauen und darin suchen.

  • Graphen als Adjazenzmatrix und Adjazenzliste darstellen und beide vergleichen.

  • Tiefensuche und Breitensuche durchführen und unterscheiden.

  • den kürzesten Weg in einem gewichteten Graphen bestimmen (Vertiefung).

Kurz aufgefrischt

Vorausgesetzt werden Feld, , und Warteschlange aus Datentypen und Datenstrukturen, aus Rekursion und Teile und herrsche sowie die .

Der Baum

Ein Baum besteht aus , die durch Kanten verbunden sind, und hat einen ausgezeichneten Knoten, die Wurzel. Jeder andere Knoten hat genau einen Vorgänger.

                (8)          ← Wurzel, Tiefe 0
               /   \
            (3)     (10)     ← Tiefe 1
           /   \       \
         (1)   (6)     (14)  ← Tiefe 2
              /   \
            (4)   (7)        ← Tiefe 3
BegriffBedeutung
Wurzelder oberste Knoten, ohne Vorgänger
Kinddirekter Nachfolger
Elternknotendirekter Vorgänger
BlattKnoten ohne Kinder
Tiefe eines KnotensAnzahl Kanten bis zur Wurzel
Höhe des Baumsgrößte vorkommende Tiefe

Ein Binärbaum ist ein Baum, in dem jeder Knoten höchstens zwei Kinder hat, ein linkes und ein rechtes.

🔴 Beachte, dass ein Baum von Natur aus aufgebaut ist: Jedes Kind ist selbst wieder die Wurzel eines Baums. Deshalb sind fast alle Verfahren auf Bäumen rekursiv, und deshalb sind sie kurz.

Der binäre Suchbaum

Ein Binärbaum wird zum binären Suchbaum, wenn für jeden gilt:

Alle Werte im linken Teilbaum sind kleiner, alle im rechten größer als der Knoten selbst.

Der Baum oben erfüllt das. Prüfe es am Knoten 3: links steht 1 (kleiner), rechts stehen 6, 4, 7 (alle größer). Und am Knoten 8: links 3, 1, 6, 4, 7 (alle kleiner), rechts 10, 14 (größer).

Suchen ist damit dasselbe wie die binäre Suche, nur auf einer Zeigerstruktur:

suche(knoten, x):
    wenn knoten leer:        gib "nicht gefunden"
    wenn knoten.wert = x:    gib gefunden
    wenn x < knoten.wert:    suche(knoten.links, x)
    sonst:                   suche(knoten.rechts, x)

Jeder Vergleich halbiert im günstigen Fall die Möglichkeiten, also O(log⁡n)O(\log n).

Aber nur im günstigen Fall. Fügt man die Werte 1,2,3,4,51, 2, 3, 4, 5 der Reihe nach ein, entsteht kein Baum, sondern eine Kette:

(1)
  \
  (2)
    \
    (3)
      \
      (4)

Die Höhe ist dann nn statt log⁡n\log n, und die Suche fällt auf O(n)O(n) zurück. Ein binärer Suchbaum ist also nur so gut wie seine Ausgewogenheit. In der Praxis benutzt man deshalb Bauformen, die sich beim Einfügen selbst wieder ausgleichen.

Der Vorteil gegenüber einem sortierten Feld: Einfügen und Löschen kosten O(log⁡n)O(\log n) statt O(n)O(n), weil nichts verschoben werden muss. Genau deshalb benutzen Datenbanken Baumstrukturen für ihre Indizes und keine sortierten Felder.

Die Suche nach der 6

8310161447

Markiert ist der Weg, den die Suche nimmt. An der Wurzel: 6<86 < 8, also links, und damit sind die 10 und die 14 in einem Schritt erledigt, ohne angesehen worden zu sein. An der 3: 6>36 > 3, also rechts. Gefunden. Drei Vergleiche für acht Werte, denn jeder Vergleich schließt einen ganzen Teilbaum aus; das ist dieselbe Halbierung wie bei der binären Suche, nur auf einer Zeigerstruktur. 🔴 Möglich ist das nur wegen der Suchbaumbedingung: Alles im linken Teilbaum ist kleiner, alles im rechten größer und zwar an jedem . Prüfe es an der 3: links steht 1, rechts stehen 6, 4 und 7. Und beachte die Kehrseite, die man diesem Bild nicht ansieht: Fügt man die Werte 1,2,3,4,51, 2, 3, 4, 5 der Reihe nach ein, entsteht kein Baum, sondern eine Kette. Dann ist die Höhe nn statt log⁡n\log n, und die Suche fällt auf O(n)O(n) zurück. Ein Suchbaum ist nur so gut wie seine Ausgewogenheit.

Durchlaufen

Um alle zu besuchen, gibt es drei Reihenfolgen. Sie unterscheiden sich nur darin, wann der Knoten selbst an die Reihe kommt:

ReihenfolgeAblaufErgebnis im Beispielbaum
HauptreihenfolgeKnoten, links, rechts8, 3, 1, 6, 4, 7, 10, 14
Symmetrischlinks, Knoten, rechts1, 3, 4, 6, 7, 8, 10, 14
Nebenreihenfolgelinks, rechts, Knoten1, 4, 7, 6, 3, 14, 10, 8

Die zweite Zeile ist bemerkenswert: Die symmetrische Reihenfolge liefert die Werte sortiert. Das ist kein Zufall, sondern folgt unmittelbar aus der Suchbaumbedingung. Erst kommt alles Kleinere, dann der Knoten, dann alles Größere, und das gilt auf jeder Ebene.

Der Graph

Ein Graph verallgemeinert den Baum: und Kanten, aber ohne Wurzel, ohne Richtungsvorgabe und mit erlaubten Kreisen.

      A ───── B
      │  \    │
      │   \   │
      C ─── D─┘
       \   /
         E
ArtBedeutung
ungerichtetKanten in beide Richtungen (Freundschaft)
gerichtetKanten nur in eine Richtung (Einbahnstraße, Verweis)
gewichtetKanten tragen einen Wert (Entfernung, Kosten, Dauer)

Ein Baum ist ein Sonderfall: ein zusammenhängender Graph ohne Kreise.

Zwei Darstellungen

Adjazenzmatrix: eine Tabelle mit einer Zeile und einer Spalte je ; 1 bedeutet „Kante vorhanden".

      A  B  C  D  E
   A  0  1  1  1  0
   B  1  0  0  1  0
   C  1  0  0  1  1
   D  1  1  1  0  1
   E  0  0  1  1  0

Adjazenzliste: für jeden Knoten eine Liste seiner Nachbarn.

A: B, C, D
B: A, D
C: A, D, E
D: A, B, C, E
E: C, D
MatrixListe
SpeicherO(n2)O(n^2)O(n+m)O(n + m)
„Gibt es Kante A–B?"O(1)O(1)O(Grad)O(\text{Grad})
„Alle Nachbarn von A"O(n)O(n)O(Grad)O(\text{Grad})
geeignet fürdichte Graphendünne Graphen

Dabei ist mm die Zahl der Kanten. Für ein Straßennetz mit einer Million Kreuzungen wäre die Matrix mit 101210^{12} Einträgen unbrauchbar, während die Liste nur die wenigen tatsächlich vorhandenen Straßen speichert. Deshalb ist die Liste in der Praxis der Normalfall.

Tiefensuche und Breitensuche

Um alle erreichbaren zu besuchen, gibt es zwei Grundverfahren. Sie unterscheiden sich in einem einzigen Punkt, nämlich der verwendeten Datenstruktur:

Tiefensuche benutzt einen : Man geht so weit wie möglich in eine Richtung und kehrt erst um, wenn es nicht weitergeht. erledigt das von selbst, denn der Aufrufstapel ist der Stapel.

Breitensuche benutzt eine Warteschlange: Man besucht erst alle direkten Nachbarn, dann deren Nachbarn, also Ebene für Ebene.

besuche(start):
    lege start in die Struktur
    solange Struktur nicht leer:
        knoten = entnehmen
        wenn knoten schon markiert: weiter
        markiere knoten
        lege alle Nachbarn hinein

Derselbe Ablauf mit einem Stapel ergibt die Tiefensuche, mit einer Warteschlange die Breitensuche. Das ist ein bemerkenswerter Befund: Die Wahl der Datenstruktur bestimmt das Verhalten des Verfahrens.

🔴 Der wichtige Unterschied in der Anwendung: Die Breitensuche findet in einem ungewichteten Graphen den Weg mit den wenigsten Kanten. Sie erreicht jeden Knoten zuerst über den kürzesten Kantenweg, weil sie Ebene für Ebene vorgeht. Die Tiefensuche findet irgendeinen Weg, meist nicht den kürzesten; sie eignet sich dafür gut zum Erkennen von Kreisen und zum vollständigen Durchsuchen.

Die Markierung besuchter Knoten ist nicht optional. Ohne sie läuft das Verfahren in einem Graphen mit Kreisen endlos, denn anders als beim Baum kann man zu einem bereits besuchten Knoten zurückkehren.

Tiefensuche (Stapel)

S 1A 2B 5C 3E 6D 4

Die Ziffer neben jedem ist die Besuchsreihenfolge, gestartet wird bei S. Die Tiefensuche benutzt einen , und weil der zuletzt Abgelegtes zuerst herausgibt, geht sie so weit wie möglich in eine Richtung: S, A, C, D, bis es nicht mehr weitergeht. Erst dann kehrt sie um und nimmt den anderen Ast, B und E. Der tiefste Knoten D ist damit schon der vierte Besuch. 🔴 In der braucht man dafür gar keine Datenstruktur zu bauen: Der Aufrufstapel ist der Stapel, und deshalb sind Tiefensuchen meist drei Zeilen lang. Was die Tiefensuche allerdings nicht leistet: Sie findet irgendeinen Weg, nicht den kürzesten. Vergleiche dazu das nächste Bild.

Breitensuche (Warteschlange)

S 1A 2B 3C 4E 5D 6

Derselbe Graph, dasselbe Verfahren, eine einzige Änderung, statt des eine Warteschlange. Und die Reihenfolge ist eine andere: S, dann beide Nachbarn A und B, dann deren Nachbarn C und E, erst zuletzt das tiefe D. Die Breitensuche arbeitet Ebene für Ebene. 🔴 Halte diesen Befund fest, er ist bemerkenswert: Die Wahl der Datenstruktur bestimmt das Verhalten des Verfahrens. Der Ablauf ist Zeile für Zeile derselbe. Praktisch folgt daraus der wichtige Unterschied: Weil die Breitensuche jeden zuerst über den kürzesten Kantenweg erreicht, findet sie in einem ungewichteten Graphen den Weg mit den wenigsten Kanten, die Tiefensuche im Bild darüber tut das nicht. Und in beiden Fällen gilt: Ohne Markierung der besuchten Knoten läuft das Verfahren in einem Graphen mit Kreisen endlos.

Vertiefung: kürzeste Wege mit Gewichten

Dieser Abschnitt gehört zum erhöhten Anforderungsniveau.

Sobald Kanten Gewichte tragen, genügt die Breitensuche nicht mehr. Der Weg mit den wenigsten Kanten ist nicht der kürzeste, wenn eine einzelne Kante sehr lang ist.

Das Verfahren von Dijkstra löst das:

1. Alle bekommen die vorläufige Entfernung ∞\infty, der Startknoten die 0. 2. Wähle unter den noch nicht abgeschlossenen Knoten den mit der kleinsten vorläufigen Entfernung. 3. Prüfe für jeden seiner Nachbarn, ob der Weg über den gewählten Knoten kürzer ist als die bisher bekannte Entfernung. Wenn ja, trage die neue Entfernung ein. 4. Markiere den Knoten als abgeschlossen und wiederhole ab 2.

Der entscheidende Gedanke steckt in Schritt 2. Warum darf man den Knoten mit der kleinsten vorläufigen Entfernung als endgültig ansehen? Weil jeder andere Weg dorthin über einen Knoten führen müsste, der bereits weiter entfernt ist, und da alle Gewichte nicht negativ sind, kann ein solcher Weg nur länger werden.

An dieser Begründung hängt auch die Voraussetzung: Das Verfahren setzt nichtnegative Gewichte voraus. Gäbe es negative Kanten, könnte ein Umweg die Gesamtstrecke verkürzen, und die Annahme fiele in sich zusammen.

Jedes Navigationsgerät arbeitet im Kern so, mit Fahrzeit oder Entfernung als Gewicht.

Dijkstra: kürzeste Wege von A

421586A 0B 3C 2D 8E 14

An den Kanten stehen die Gewichte, an den die endgültige Entfernung von A. Rechne die interessanteste Stelle nach: Direkt kostet A→B 4. Über C sind es 2+1=32 + 1 = 3, also ist der Umweg über zwei Kanten kürzer als die eine direkte. Genau daran scheitert die Breitensuche aus dem Bild davor: Der Weg mit den wenigsten Kanten ist bei Gewichten nicht der kürzeste. Ebenso bei D: über C wären es 2+8=102 + 8 = 10, über B nur 3+5=83 + 5 = 8. 🔴 Und nun der Gedanke, auf dem das ganze Verfahren ruht. Warum darf man den Knoten mit der kleinsten vorläufigen Entfernung sofort als endgültig ansehen, hier C mit 2? Weil jeder andere Weg dorthin über einen Knoten führen müsste, der bereits weiter entfernt ist, und da alle Gewichte nicht negativ sind, kann ein solcher Weg nur länger werden. An dieser Begründung hängt zugleich die Voraussetzung: Gäbe es negative Kanten, könnte ein Umweg die Strecke verkürzen, und die Annahme fiele in sich zusammen.

Einen Suchbaum aufbauen und durchlaufen

Füge nacheinander 50,30,70,20,40,60,8050, 30, 70, 20, 40, 60, 80 in einen leeren Suchbaum ein. Gib danach die symmetrische Reihenfolge an.

  1. 1

    Regel beim Einfügen: Von der Wurzel aus vergleichen, bei kleiner nach links, bei größer nach rechts, bis ein freier Platz erreicht ist.

  2. 2

    50 wird Wurzel. 30 ist kleiner → links von 50. 70 ist größer → rechts von 50.

  3. 3

    20: kleiner als 50 → links zu 30; kleiner als 30 → links von 30.

  4. 4

    40: kleiner als 50 → zu 30; größer als 30 → rechts von 30.

  5. 5

    60 und 80 entsprechend im rechten Teilbaum:

                  (50)
                 /    \
             (30)      (70)
            /    \    /    \
         (20)   (40)(60)   (80)
    
  6. 6

    Der Baum ist ausgewogen: Höhe 2 bei 7 . Das entspricht log⁡28=3\log_2 8 = 3 Ebenen, also dem günstigen Fall.

  7. 7

    Symmetrische Reihenfolge (links, Knoten, rechts): 20,30,40,50,60,70,8020, 30, 40, 50, 60, 70, 80. Die Werte kommen sortiert heraus, und das folgt unmittelbar aus der Suchbaumbedingung.

  8. 8

    Zum Vergleich: Hätte man dieselben Werte aufsteigend eingefügt (20,30,40,…20, 30, 40, \ldots), wäre eine Kette der Höhe 6 entstanden, und die Suche hätte O(n)O(n) gekostet. Die Einfügereihenfolge entscheidet also über die Leistung.

Ein ausgewogener Baum der Höhe 2; die symmetrische Reihenfolge liefert 20,30,40,50,60,70,8020,30,40,50,60,70,80.

Tiefensuche gegen Breitensuche

Durchlaufe diesen Graphen ab A, einmal mit Tiefensuche, einmal mit Breitensuche. Nachbarn jeweils in alphabetischer Reihenfolge.

A: B, C
B: A, D, E
C: A, F
D: B
E: B, F
F: C, E
  1. 1

    Tiefensuche (): so weit wie möglich in eine Richtung, dann zurück.

  2. 2

    A → B (erster Nachbar) → D (erster unbesuchter Nachbar von B). D hat nur A und B, beide besucht → zurück zu B.

  3. 3

    Von B weiter zu E → von E zu F → von F zu C. Alle besucht.

  4. 4

    Reihenfolge: A, B, D, E, F, C.

  5. 5

    Breitensuche (Warteschlange): erst alle direkten Nachbarn, dann deren Nachbarn.

  6. 6

    Ebene 0: A. Ebene 1: B, C. Ebene 2: D, E (von B) und F (von C).

    Reihenfolge: A, B, C, D, E, F.

  7. 7

    Der entscheidende Vergleich am C: Die Breitensuche erreicht ihn im zweiten Schritt über den Weg A–C, also über eine Kante. Die Tiefensuche erreicht ihn zuletzt über A–B–E–F–C, also über vier Kanten.

  8. 8

    Daraus folgt die praktische Regel: Wer den Weg mit den wenigsten Kanten sucht, muss die Breitensuche nehmen. Die Tiefensuche findet irgendeinen Weg und eignet sich besser zum vollständigen Durchsuchen und zum Erkennen von Kreisen.

Tiefensuche: A, B, D, E, F, C. Breitensuche: A, B, C, D, E, F. Nur die Breitensuche erreicht C über den kürzesten Kantenweg.

Typischer Fehler

„Ein binärer Suchbaum sucht immer in O(log⁡n)O(\log n)."

Nur wenn er ausgewogen ist, und das ist keine Eigenschaft der Struktur, sondern eine Folge der Einfügereihenfolge.

Füge 1,2,3,4,51, 2, 3, 4, 5 der Reihe nach ein. Jeder Wert ist größer als alle vorherigen, landet also immer rechts:

(1) → (2) → (3) → (4) → (5)

Das ist eine in Baumform. Die Höhe ist n−1n-1 statt log⁡2n\log_2 n, und die Suche kostet O(n)O(n).

Besonders unangenehm daran ist, dass gerade sortierte Eingabedaten den schlechtesten Fall erzeugen, und sortierte sind in der Praxis alles andere als selten.

Zwei übliche Auswege:

Selbstausgleichende Bauformen. Sie stellen nach jedem Einfügen die Ausgewogenheit durch Umhängen wieder her und garantieren damit O(log⁡n)O(\log n) in jedem Fall.

Zufällige Einfügereihenfolge. Sie führt mit hoher Wahrscheinlichkeit zu einem brauchbar ausgewogenen Baum, garantiert aber nichts.

Die allgemeine Lehre gilt über dieses Kapitel hinaus: Eine Aufwandsangabe ohne die zugehörige Voraussetzung ist wertlos. Beim Suchbaum lautet sie „ausgewogen", bei der binären Suche „sortiert", bei der Streuspeicherung „gut verteilende Streuwertfunktion".

Übung 1

leicht

a) Erkläre die Begriffe Wurzel, Blatt und Höhe. b) Was zeichnet einen binären Suchbaum aus? c) Welche Durchlaufreihenfolge liefert die Werte sortiert? d) Welche Datenstruktur benutzt die Breitensuche?

Tipp anzeigen

Zu b): Was gilt für jeden ?

Lösung anzeigen

a) Die Wurzel ist der oberste Knoten, der keinen Vorgänger hat. Ein Blatt ist ein Knoten ohne Kinder. Die Höhe des Baums ist die größte vorkommende Tiefe, also die Anzahl der Kanten auf dem längsten Weg von der Wurzel zu einem Blatt.

b) Für jeden Knoten gilt: Alle Werte im linken Teilbaum sind kleiner, alle im rechten größer als der Knoten selbst. Wichtig ist „jeden Knoten" und „ganzer Teilbaum", nicht nur die direkten Kinder.

c) Die symmetrische Reihenfolge: erst der linke Teilbaum, dann der Knoten, dann der rechte.

d) Eine (FIFO). Die Tiefensuche benutzt einen Stapel.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    a) Die drei Grundbegriffe am Baum

    Die Wurzel ist der oberste Knoten, der keinen Vorgänger hat. Ein Blatt ist ein Knoten ohne Kinder. Die Höhe des Baums ist die größte vorkommende Tiefe, also die Anzahl der Kanten auf dem längsten Weg von der Wurzel zu einem Blatt.

  2. 2

    b) Die Suchbaum-Eigenschaft gilt für jeden Knoten und ganze Teilbäume

    Für jeden Knoten gilt: Alle Werte im linken Teilbaum sind kleiner, alle im rechten größer als der Knoten selbst. Wichtig sind beide Betonungen: „jeden Knoten“ und „ganzer Teilbaum“, nicht nur die direkten Kinder.

  3. 3

    c) Die symmetrische Reihenfolge liefert die Sortierung

    Die symmetrische Reihenfolge (auch Inorder): erst der linke Teilbaum, dann der Knoten, dann der rechte. Weil links alles Kleinere und rechts alles Größere liegt, entsteht dabei zwangsläufig die aufsteigende Reihenfolge.

  4. 4

    d) Breitensuche braucht eine Warteschlange

    Eine Warteschlange (FIFO). Die Tiefensuche benutzt dagegen einen Stapel (oder die Rekursion, die selbst einen Stapel benutzt).

Übung 2

mittel

a) Füge 40,20,60,10,30,5040, 20, 60, 10, 30, 50 in einen leeren Suchbaum ein und zeichne ihn. b) Gib die symmetrische Reihenfolge an. c) Wie viele Vergleiche braucht die Suche nach 30, wie viele nach 55? d) Was entstünde, wenn man dieselben Werte aufsteigend sortiert einfügte? Welchen Aufwand hätte die Suche dann?

Tipp anzeigen

Zu c): Zähle die besuchten .

Lösung anzeigen

a) Baum:

            (40)
           /    \
       (20)      (60)
      /    \     /
   (10)   (30) (50)

40 wird Wurzel; 20 kleiner → links; 60 größer → rechts; 10 kleiner als 40 und 20 → links von 20; 30 kleiner als 40, größer als 20 → rechts von 20; 50 größer als 40, kleiner als 60 → links von 60.

b) 10,20,30,40,50,6010, 20, 30, 40, 50, 60, sortiert.

c) Suche nach 30: 40 (größer, nach links), 20 (kleiner, nach rechts), 30 gefunden → 3 Vergleiche.

Suche nach 55: 40 (größer, nach rechts), 60 (kleiner, nach links), 50 (größer, nach rechts), dort ist kein Kind → 3 Vergleiche, Ergebnis „nicht gefunden".

d) Es entstünde eine Kette, denn jeder neue Wert wäre größer als alle vorherigen und liefe immer nach rechts:

(10) → (20) → (30) → (40) → (50) → (60)

Die Höhe wäre n−1=5n-1 = 5 statt 2, und die Suche kostete O(n)O(n) statt O(log⁡n)O(\log n). Bemerkenswert ist, dass ausgerechnet sortierte Eingabedaten den schlechtesten Fall erzeugen; in der Praxis behilft man sich mit selbstausgleichenden Bauformen.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    Teil a): Jeden Wert von der Wurzel aus einsortieren

    Man beginnt bei jedem neuen Wert wieder an der Wurzel und vergleicht sich nach unten durch, bis ein freier Platz erreicht ist. Nachträgliches Umhängen ist nicht erlaubt.

    Zwischenergebnis

    40 als Wurzel, danach 20 links und 60 rechts.

    Der Platz eines Werts hängt allein vom Weg der Vergleiche ab. Deshalb erzeugt eine andere Einfügereihenfolge einen anderen Baum, auch bei denselben Werten.

  2. 2

    Teil b): Symmetrisch heißt links, Knoten, rechts

    Man arbeitet : zuerst den ganzen linken Teilbaum ausgeben, dann den Knoten selbst, dann den ganzen rechten Teilbaum.

    Zwischenergebnis

    10,20,30,40,50,6010, 20, 30, 40, 50, 60.

  3. 3

    Teil c): Den Suchweg zählen

    Man zählt die besuchten Knoten. Auch die erfolglose Suche endet nach höchstens so vielen Vergleichen, wie der Baum hoch ist.

    40 \to 60 \to 50 \to \text{leer}

    Zwischenergebnis

    Beide Male drei Vergleiche.

    Dass die erfolglose Suche nicht teurer ist, liegt daran, dass sie ebenfalls einem Weg von der Wurzel nach unten folgt. Sie endet an einer leeren Stelle statt an einem Treffer.

  4. 4

    Teil d): Den schlechtesten Fall konstruieren

    Man fügt die Werte aufsteigend ein und stellt fest, dass jeder nach rechts läuft. Es entsteht eine Kette statt eines Baums.

    \text{Höhe } n-1 ;\Rightarrow; O(n)

    Zwischenergebnis

    Aufwand O(n)O(n) statt O(log⁡n)O(\log n).

Übung 3

schwer

a) Vergleiche Adjazenzmatrix und Adjazenzliste nach Speicherbedarf und Zugriffsgeschwindigkeit. Wann wählt man welche? b) Erkläre den Unterschied zwischen Tiefen- und Breitensuche und nenne für jede einen passenden Einsatzfall. c) Warum muss man besuchte markieren? d) (Vertiefung) Beschreibe das Verfahren von Dijkstra und begründe, warum es nichtnegative Gewichte voraussetzt.

Tipp anzeigen

Zu d): Warum darf der Knoten mit der kleinsten vorläufigen Entfernung als endgültig gelten?

Lösung anzeigen

a) Speicher: Die Matrix braucht immer O(n2)O(n^2), auch wenn kaum Kanten vorhanden sind. Die Liste braucht O(n+m)O(n + m) mit mm als Kantenzahl.

Zugriff: Die Frage „gibt es eine Kante von A nach B?" beantwortet die Matrix in O(1)O(1) durch einfaches Nachschlagen; die Liste muss die Nachbarliste durchsuchen. Umgekehrt liefert die Liste alle Nachbarn eines Knotens direkt, während die Matrix eine ganze Zeile mit nn Einträgen durchgehen muss.

Wahl: Bei dichten Graphen, in denen fast alle Knotenpaare verbunden sind, ist die Matrix angemessen. Bei dünnen Graphen ist die Liste deutlich sparsamer. In der Praxis überwiegen dünne Graphen: Ein Straßennetz mit einer Million Kreuzungen hätte als Matrix 101210^{12} Einträge, von denen praktisch alle null wären; jede Kreuzung hat nur wenige Nachbarn.

b) Tiefensuche ( oder ) geht so weit wie möglich in eine Richtung und kehrt erst um, wenn es nicht weitergeht. Breitensuche (Warteschlange) besucht erst alle direkten Nachbarn, dann deren Nachbarn, also Ebene für Ebene.

Tiefensuche eignet sich für: vollständiges Durchsuchen, Erkennen von Kreisen, Rücksetzverfahren wie beim Labyrinth. Sie braucht wenig Speicher, wenn der Graph tief und schmal ist.

Breitensuche eignet sich für: kürzeste Wege in ungewichteten Graphen, etwa „über wie viele Ecken kennen sich zwei Personen?". Sie erreicht jeden Knoten zuerst über den kürzesten Kantenweg, weil sie Ebene für Ebene vorgeht.

c) Weil ein Graph im Gegensatz zu einem Baum Kreise enthalten darf. Ohne Markierung würde das Verfahren einen bereits besuchten Knoten erneut betreten, von dort wieder zu seinen Nachbarn gehen und im Kreis laufen, also nie enden.

Zusätzlich verhindert die Markierung, dass Knoten mehrfach bearbeitet werden, was den Aufwand unnötig erhöhen würde. Bei einem Baum ist sie nicht nötig, weil es dort zu jedem Knoten genau einen Weg von der Wurzel gibt.

d) Verfahren: Alle Knoten erhalten die vorläufige Entfernung ∞\infty, der Startknoten die 0. Dann wiederholt: Wähle unter den noch nicht abgeschlossenen Knoten den mit der kleinsten vorläufigen Entfernung. Prüfe für jeden seiner Nachbarn, ob der Weg über diesen Knoten kürzer ist als die bisher bekannte Entfernung, und trage gegebenenfalls die kleinere ein. Markiere den Knoten als abgeschlossen und wiederhole.

Warum das funktioniert: Der Knoten mit der kleinsten vorläufigen Entfernung darf als endgültig gelten. Jeder andere Weg zu ihm müsste über einen noch nicht abgeschlossenen Knoten führen, und jeder solche Knoten hat bereits eine größere vorläufige Entfernung. Da alle Kantengewichte nicht negativ sind, kann ein Weg über einen weiter entfernten Knoten nur noch länger werden.

Deshalb die Voraussetzung: Gäbe es negative Gewichte, wäre genau dieser Schluss falsch. Ein Umweg über einen entfernteren Knoten könnte durch eine negative Kante insgesamt kürzer sein, und ein bereits abgeschlossener Knoten müsste nachträglich verbessert werden. Für solche Graphen braucht man andere Verfahren.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    a) Matrix gegen Liste: Speicher und Zugriff getrennt betrachten

    Speicher: Die Matrix braucht immer O(n2)O(n^2), auch wenn kaum Kanten vorhanden sind; die Liste braucht O(n+m)O(n + m) mit mm als Kantenzahl. Zugriff: „Gibt es eine Kante von A nach B?“ beantwortet die Matrix in O(1)O(1) durch Nachschlagen, die Liste muss suchen. Umgekehrt liefert die Liste alle Nachbarn eines Knotens direkt, während die Matrix eine ganze Zeile mit nn Einträgen durchgehen muss.

  2. 2

    a) Die Wahl: dicht oder dünn

    Bei dichten Graphen, in denen fast alle Knotenpaare verbunden sind, ist die Matrix angemessen. Bei dünnen Graphen ist die Liste deutlich sparsamer. In der Praxis überwiegen dünne Graphen: Ein Straßennetz mit einer Million Kreuzungen hätte als Matrix 101210^{12} Einträge, von denen praktisch alle null wären.

  3. 3

    b) Tiefen- und Breitensuche mit ihren Einsatzfällen

    Tiefensuche (Stapel oder Rekursion) geht so weit wie möglich in eine Richtung und kehrt erst um, wenn es nicht weitergeht, gut für vollständiges Durchsuchen, Erkennen von Kreisen und Rücksetzverfahren. Breitensuche (Warteschlange) besucht erst alle direkten Nachbarn, dann deren Nachbarn, gut für kürzeste Wege in ungewichteten Graphen.

  4. 4

    c) Warum besuchte Knoten markiert werden müssen

    Weil ein Graph im Gegensatz zu einem Baum Kreise enthalten darf. Ohne Markierung würde das Verfahren einen bereits besuchten Knoten erneut betreten, von dort wieder zu seinen Nachbarn gehen und im Kreis laufen, also nie enden.

  5. 5

    d) (Vertiefung) Dijkstra und warum er nichtnegative Gewichte braucht

    Verfahren: Alle Knoten erhalten die vorläufige Entfernung ∞\infty, der Startknoten die 0. Dann wiederholt: Wähle unter den noch nicht abgeschlossenen den Knoten mit der kleinsten vorläufigen Entfernung, prüfe für jeden Nachbarn, ob der Weg über diesen Knoten kürzer ist, trage gegebenenfalls den kleineren Wert ein, und markiere den Knoten als abgeschlossen.

  6. 6

    d) (Vertiefung) Der Kern des Beweises und wo er bricht

    Warum es funktioniert: Jeder andere Weg zu diesem Knoten müsste über einen noch nicht abgeschlossenen Knoten führen, und jeder solche hat bereits eine größere vorläufige Entfernung. Da alle Kantengewichte nicht negativ sind, kann ein Weg über einen weiter entfernten Knoten nur noch länger werden. Deshalb die Voraussetzung: Gäbe es negative Gewichte, wäre genau dieser Schluss falsch.

Zusammenfassung

Bäume und Graphen beschreiben Dinge und ihre Verbindungen und passen damit auf sehr verschiedene Aufgaben, von Dateisystemen über Stammbäume bis zu Straßennetzen. Ein Baum hat eine Wurzel und ist aufgebaut, weshalb Verfahren auf Bäumen fast immer rekursiv und dadurch kurz sind. Ein binärer Suchbaum hält für jeden alle kleineren Werte links und alle größeren rechts; das erlaubt Suchen, Einfügen und Löschen in O(log⁡n)O(\log n), allerdings nur bei ausgewogenem Baum, denn sortiert eingefügte Werte erzeugen eine Kette mit O(n)O(n). Die symmetrische Durchlaufreihenfolge gibt die Werte sortiert aus, was unmittelbar aus der Suchbaumbedingung folgt. Graphen verzichten auf Wurzel und Kreisfreiheit und werden als Adjazenzmatrix mit O(n2)O(n^2) oder als Adjazenzliste mit O(n+m)O(n+m) gespeichert, wobei in der Praxis fast immer die Liste passt. Tiefen- und Breitensuche unterscheiden sich allein durch die verwendete Datenstruktur, gegen Warteschlange, und nur die Breitensuche findet den Weg mit den wenigsten Kanten; besuchte Knoten muss man dabei markieren, sonst läuft das Verfahren in Kreisen endlos. Für gewichtete Graphen liefert das Verfahren von Dijkstra kürzeste Wege, setzt dafür aber nichtnegative Gewichte voraus.