← Startseite
🚗

Vorlesung 11

Gewichtete Graphen & Dijkstra
Der kürzeste Weg, wenn Wege unterschiedlich teuer sind — gebaut mit struct & malloc

Fortgeschrittene Algorithmen und Programmierung
Prof. Dr. Alexandra Mikityuk
HTW Berlin

Dijkstra struct malloc / free

Lernziele

  • BFS aus VL 10 wieder aufgreifen — und seine Grenze erkennen: es zählt nur Kanten, nicht ihr Gewicht
  • Dijkstra verstehen: den kürzesten Weg in gewichteten Graphen finden (km, Minuten, Kosten)
  • Das Herzstück „Relax" begreifen — eine Distanz verbessern, wenn ein Umweg billiger ist
  • Sehen, warum BFS nur ein Spezialfall von Dijkstra ist (alle Gewichte = 1)
  • Die zwei C-Werkzeuge lernen, um so etwas zu bauen: struct (Knoten-Daten bündeln) & malloc (zur Laufzeit anlegen)

Rückblick VL 10: BFS — die Welle

BFS erkundet einen Graphen Ebene für Ebene (wie eine Welle im Wasser) und findet so den kürzesten Weg — gemessen in Anzahl Schritten.

A B C D E

🟢 Werkzeug: Queue

Eine To-do-Liste: vorne raus (besuchen), neue Nachbarn hinten dran. So entsteht die Wellen-Reihenfolge ganz von allein.

🔵 Ergebnis

A → B,C (1 Schritt) → D,E (2 Schritte). Jeder Knoten wird beim ersten Erreichen auf kürzestem Weg gefunden.

Die stille Annahme von BFS: jede Kante ist gleich teuer (ein Schritt = ein Schritt). Solange das stimmt, ist BFS perfekt. Aber stimmt es in der echten Welt?

Das Problem: Kanten haben ein Gewicht

In echten Karten ist nicht jede Straße gleich lang. Eine Kante trägt ein Gewicht — km, Minuten, Kosten. Und damit wählt BFS plötzlich falsch.

4 1 2 A B C

💭 BFS verrechnet sich

BFS nimmt A→B: nur 1 Kante — aber Kosten 4.

Klüger: A→C→B über 2 Kanten, aber nur 1 + 2 = 3. Der Weg mit mehr Knoten ist hier der günstigere!

BFS zählt nur Kanten — nicht ihr Gewicht. Es kann gar nicht wissen, dass A→B teuer ist.

Wir brauchen einen Algorithmus, der Gewichte aufsummiert und immer die billigste Gesamtstrecke wählt. Sein Name: Dijkstra — der Algorithmus hinter eurem Navi.

Erst die Intuition: wie ein Navi denkt

Bevor wir formal werden — stellt euch vor, ihr sucht die billigste Bahnverbindung von A zu allen anderen Orten. Wie würdet ihr von Hand vorgehen?

🎫 Die Preistafel

Neben jedem Ort steht der billigste bekannte Preis dorthin. Am Anfang: Start = 0, alle anderen („noch keine Verbindung gefunden"). Diese Tafel wird nach und nach besser.

🔄 Der Denk-Wechsel gegenüber BFS

BFS zählte nur Halte (ein Schritt = ein Schritt). Jetzt trägt jede Strecke einen Preis — und ihr addiert sie. Deshalb kann die billigste Verbindung sogar mehr Halte haben (wie A→C→B von vorhin).

Die eine goldene Regel: Plant immer von dem Ort weiter, der auf der Tafel gerade am billigsten steht. Warum man dem trauen darf — und die konkreten Schritte — kommen auf den nächsten beiden Slides.

Dijkstra — die Idee

Wie BFS arbeitet Dijkstra mit einer „Welle" — aber sie wächst nicht nach Schritten, sondern nach aufsummierten Kosten. Jeder Knoten merkt sich seine bisher billigste bekannte Distanz zum Start.

1️⃣ Start = 0

Distanz zum Start ist 0, alle anderen erst mal (unendlich = „noch nicht erreicht").

2️⃣ Billigsten wählen

Nimm den noch offenen Knoten mit der kleinsten Distanz und markiere ihn als endgültig „fertig".

3️⃣ Nachbarn prüfen

Für jeden Nachbarn: Ist der Weg über mich billiger als sein bisheriger? Dann verbessern (= Relax).

Der gierige Kern (greedy): Wer als nächstes mit der kleinsten Distanz dran ist, ist endgültig fertig — billiger geht es nicht mehr, denn jeder andere Weg dorthin müsste über einen teureren Knoten laufen. Wiederhole, bis alle fertig sind.

Warum ist der billigste Knoten sofort „fertig"?

Das ist der eine geniale Trick von Dijkstra — und der Punkt, an dem die meisten kurz stocken. Schauen wir ihn uns ganz langsam an.

🧠 Die Behauptung

Nimm den noch offenen Knoten u mit der kleinsten bekannten Distanz. Dann ist dieser Wert schon die endgültig billigste Distanz — wir müssen u nie wieder anfassen.

🔒 Warum das stimmt

Jeder andere Weg zu u müsste über einen anderen, noch offenen Knoten laufen — und der hat eine größere oder gleiche Distanz. Plus eine Kante, die nie negativ ist. Also kann kein anderer Weg billiger sein. ✓

Hier steckt eine Bedingung versteckt drin: Kanten dürfen nicht negativ sein. Sonst könnte ein späterer Umweg einen schon „fertigen" Knoten doch noch billiger machen — und der ganze Trick fällt zusammen. (Mehr dazu in ein paar Slides.)

Das Herzstück: „Relax" einer Kante

„Relax" klingt kompliziert, ist aber nur ein Vergleich: Komme ich über meinen Nachbarn billiger ans Ziel als bisher? Wenn ja — neuen, besseren Wert eintragen.

4 1 2 A B C Start · 0 dist 4 → 3 dist 1

– – direkt A→B = 4 · Umweg A→C→B = 1+2 = 3

dist[C] = 1 Kante C→B kostet 2 → Weg über C = 1 + 2 = 3
dist[X] = billigster Weg vom Start bis X (Wert auf der Preistafel) gewicht(X,Y) = Kosten der einen Kante X→Y (Zahl an der Kante)
// B hatte bisher dist = 4 (direkt von A).  Über C wären es nur 3:
if (dist[C] + gewicht(C, B) < dist[B]) {   // 1 + 2 < 4  ✓
    dist[B] = dist[C] + gewicht(C, B);     // dist[B] = 3
}
Mehr ist Dijkstra nicht: immer den billigsten Knoten nehmen und seine Kanten „relaxen". Jede Verbesserung ist ein „ich habe einen kürzeren Weg gefunden". Schauen wir es Schritt für Schritt an.

Unser Beispiel — ein gewichteter Graph

Fünf Orte A–E, die Zahlen an den Kanten sind die Kosten. Wir suchen die billigste Strecke von A zu jedem anderen Ort.

4 1 2 5 1 3 A B C D E Start
Achtet auf A→B: direkt kostet die Kante 4. Aber über C (A→C→B) sind es nur 1 + 2 = 3. Genau diese Verbesserung wird Dijkstra gleich finden.

Die Distanz-Tabelle — der erste Schritt ganz langsam

Gleich kommt die volle Tabelle. Damit sie nicht erschlägt: So entsteht sie. Jede Spalte ist ein Knoten, jede Zeile ein „jetzt fertig". Das Zeichen heißt „noch kein Weg bekannt".

4 1 A B C D E 0 ✓ 4 1

A fertig, seine Kanten relaxed

fertigABCDE
Start0
fertig A041

🔍 Was hier passiert

  • Start: nur A ist erreichbar (0), der Rest ∞.
  • Kleinste offene Distanz? → A (0). A wird fertig.
  • Relax der Kanten von A: B bekommt Distanz 0+4 = 4, C bekommt 0+1 = 1.
  • D und E sind keine Nachbarn von A → bleiben ∞.
Nächster Knoten? Der kleinste offene Wert ist jetzt C = 1nicht B = 4! Genau nach diesem Muster geht es weiter. Jetzt die ganze Tabelle:

Dijkstra Schritt für Schritt

Wir führen eine Distanz-Tabelle. Grün = gerade verbessert (relaxed), blau = fertig (endgültig). Immer den kleinsten offenen Wert als Nächstes.

4 1 2 5 1 3 A B C D E

Referenz: der gewichtete Graph

Schritt: fertigABCDE
Start0
fertig A (0)041
fertig C (1)0316
fertig B (3)0314
fertig D (4)03147
fertig E (7)03147
Die zwei spannenden Zeilen: Beim Fertigstellen von C sinkt dist[B] von 4 auf 3 (Weg über C!). Beim Fertigstellen von B sinkt dist[D] von 6 auf 4. Genau das macht „Relax".

Das Ergebnis: die billigsten Wege

Dijkstra liefert die günstigste Distanz von A zu jedem Knoten — und die fett markierten Kanten bilden den Kürzeste-Wege-Baum.

A B C D E
von A nachKostenWeg
C1A→C
B3A→C→B
D4A→C→B→D
E7A→C→B→D→E
Bemerkenswert: Der Weg zu B nimmt 3 Kanten in Kauf (A→C→B), obwohl es eine direkte Kante A→B gäbe — weil die Summe billiger ist. BFS hätte hier die falsche Antwort gegeben.

BFS ist nur ein Spezialfall von Dijkstra

Die beiden sind keine Gegner — sie sind verwandt. Setzt man bei Dijkstra jedes Gewicht auf 1, kommt exakt BFS heraus.

BFS (VL 10)Dijkstra (heute)
misstAnzahl Kanten (Schritte)Summe der Gewichte
nächster Knotenvorderster in der Queueder mit kleinster Distanz
Kante prüfenbesucht? ja/neinRelax: billiger? dann verbessern
passt fürungewichtete Graphengewichtete Graphen (km, min, €)
Aufwand (Big-O)O(n + Kanten) — linearO(n²)
Gewichte alle = 1→ beide liefern dasselbe Ergebnis

n = Anzahl Knoten. Mit der Adjazenzliste aus VL 10 ist BFS linear — jeder Knoten und jede Kante genau einmal. Dijkstra (einfache Version) ist O(n²): es sucht n-mal unter n Knoten das kleinste → n × n. (Big-O wie in VL 4/5: 1, n, n², …)

Ein Gedanke, viele Algorithmen: „Erkunde nach Nähe, finde das Billigste zuerst." BFS und Dijkstra sind zwei Stufen derselben Idee — Dijkstra ist die Version, die rechnen kann.

Wo Dijkstra aussteigt: negative Kanten

Dijkstras Garantie („den Kleinsten abhaken ist endgültig") gilt nur, wenn Kanten nie negativ sind. Ein kleines Gegenbeispiel zeigt, warum.

2 5 −4 A B C

💥 Der Fehler

Dijkstra macht B sofort mit Distanz 2 fertig (direkt A→B). Aber der Weg A→C→B kostet 5 + (−4) = 1 — billiger! Zu spät: B ist schon abgehakt und wird nicht mehr korrigiert.

🤔 Wann ist ein Gewicht negativ?

Bei km, Zeit, Kosten: nie. Negativ heißt „Gewinn statt Kosten" — z. B. eine Gutschrift/Cashback auf einer Strecke, Energiegewinn bergab, oder Währungstausch mit Vorteil (Arbitrage). Selten — aber es gibt sie.

🛠️ Warum Bellman-Ford Negatives verkraftet

Der Trick: Bellman-Ford hakt nie einen Knoten früh als „fertig" ab. Es relaxt immer wieder ALLE Kanten, Runde um Runde. Taucht erst spät eine negative Abkürzung auf, wird sie in einer späteren Runde einfach noch nachgetragen — nichts ist zu früh festgezurrt. Genau das verbietet sich Dijkstra mit seinem Greedy-Trick.

Merksatz: „Nicht-negativ" ist der Preis für Dijkstras Tempo — genau der Greedy-Trick braucht ihn. Für Karten, Netze, Reisezeiten (also fast immer) ist Dijkstra richtig; nur bei echten negativen Kanten greift man zu Bellman-Ford.

🛠️ Jetzt bauen wir das in C — was muss Dijkstra sich merken?

Schaut auf die Tabelle von vorhin: Dijkstra muss zu jedem Knoten mehrere Dinge gleichzeitig festhalten. Bisher wären das viele lose Variablen.

fertigABCDE
Start0
A (0)041
C (1)0316
B (3)0314
D (4)03147
E (7)03147

Jede Spalte ist ein Knoten. Pro Knoten führt Dijkstra seine Distanz und ob schon „fertig" — genau das muss C sich merken.

🟠 Lose Variablen — Chaos

char  name[5];
int   dist[5];
int   fertig[5];
// alle 3 beschreiben DIESELBEN Knoten,
// aber nichts im Code verbindet sie 😵
[0] [1] [2] [3] [4] name dist fertig A B C D E 0 3 1 4 7

5 Slots = 5 Knoten (A–E), Index 0–4. Spalte [2] = Knoten C (name C, dist 1, fertig ✓) — dieselbe Sache, über 3 Arrays verstreut.

Ein Knoten = ein Ding

Viel besser: jeder Knoten bündelt Name + Distanz + fertig? in einer Einheit. Und weil wir die Graph-Größe oft erst zur Laufzeit kennen, wollen wir Knoten dynamisch anlegen.

Dafür zwei Werkzeuge: struct bündelt zusammengehörige Daten · malloc legt sie zur Laufzeit an. Genau die zwei, die in VL 10 nur als Bild A ──► B ──► ∅ auftauchten.

Werkzeug 1: struct — Daten bündeln

Ein struct ist ein selbst gebauter Datentyp: eine beschriftete Schublade mit Fächern. Perfekt, um alles zu einem Knoten an einem Ort zu halten.

📦 Knoten name = 'C' dist = 1 fertig = 1
typedef struct {
    char name;
    int  dist;     // billigste Distanz
    int  fertig;   // 0/1 = endgültig?
} Knoten;

Knoten k;
k.name = 'C';   k.dist = 1;   // Zugriff mit Punkt .
Mit dem Punkt . kommt ihr an jedes Fach: k.dist, k.fertig. Ab jetzt reist ein ganzer Knoten als ein Ding durchs Programm — statt drei Arrays parallel zu pflegen.

struct + Zeiger: der Pfeil ->

Sobald ihr (wie bei der Nachbarliste aus VL 10) einen Zeiger auf ein struct habt, schreibt man statt des Punkts den Pfeil ->.

📍 Punkt vs. Pfeil

Knoten  k;     k.dist  = 1;  // Variable → .
Knoten *p=&k;   p->dist = 1;  // Zeiger   → ->

🟰 Pfeil = folgen + Punkt

p->dist ist nur die Kurzform für (*p).dist: erst dem Zeiger folgen (das Dereferenzieren * aus VL 10), dann ins Fach.

Eine Regel, die alles entscheidet: habt ihr die Variable → Punkt .  ·  habt ihr einen Zeiger darauf → Pfeil ->. Den Pfeil braucht gleich jeder dynamisch erzeugte Knoten.

Werkzeug 2: malloc — Speicher zur Laufzeit

Ein Array braucht seine Größe im Voraus: Knoten netz[100];. Aber wie viele Orte hat die Karte? Oft weiß man das erst beim Laufen (z. B. aus einer Datei). Lösung: Speicher auf Bestellung.

🍽️ Stack

Eure normalen Variablen & festen Arrays. Wird automatisch aufgeräumt. Größe muss vorher feststehen.

🏬 Heap

Ein großes Lager. Mit malloc holt ihr euch zur Laufzeit genau so viel, wie ihr braucht — und gebt es mit free zurück.

„brauche Platz für n Knoten" → malloc → Zeiger auf den Block 🔑
Kernunterschied: Der Stack räumt für euch auf. Heap-Speicher bleibt liegen, bis ihr ihn selbst zurückgebt. Mehr Macht — mehr Verantwortung.

malloc benutzen — Schritt für Schritt

#include <stdlib.h>   // hier wohnen malloc & free

// Platz für n Knoten holen (n erst zur Laufzeit bekannt):
Knoten *netz = malloc(n * sizeof(Knoten));

if (netz == NULL) { return 1; }  // kein Platz? → abbrechen (VL10: NULL!)

netz[2].dist = 1;       // benutzen wie ein normales Array

free(netz);              // am Ende IMMER zurückgeben

📏 sizeof liefert die Bytes pro Knoten — Muster immer anzahl * sizeof(Typ).

💧 free nicht vergessen! Jedes malloc braucht genau ein free — sonst Memory Leak.

Zwei Sicherheits-Reflexe aus VL 10: malloc kann NULL liefern (kein Platz) → prüfen, bevor man folgt. Und dem NULL-Zeiger zu folgen = Absturz.

Stolpersteine in C — die drei Klassiker

struct, Zeiger und malloc sind mächtig — und genau hier passieren die häufigsten Fehler. Diese drei erkennt ihr ab jetzt sofort.

1️⃣ . statt ->

// p ist ein Zeiger!
p.dist = 1;p->dist = 1;

Zeiger → Pfeil. Den Punkt nur bei der Variablen selbst.

2️⃣ free vergessen

x = malloc(...);
// ... benutzen ...
free(x);

Jedes malloc braucht genau ein free — sonst Memory Leak.

3️⃣ NULL folgen

p = malloc(...);
if (p == NULL) ...
p->dist = 1; // erst danach

Erst prüfen, dann folgen. Dem NULL-Zeiger zu folgen = Absturz.

Faustregel: Wer sich bei jedem malloc sofort das passende free und die NULL-Prüfung dazudenkt — und Zeigern den Pfeil gönnt — umgeht die allermeisten C-Abstürze in diesem Kurs.

Alles zusammen: die Nachbarliste aus VL 10

Mit beiden Werkzeugen verwandeln wir das gemalte C ──► A ──► B ──► ∅ in echten Code: ein struct bündelt Nachbar + Gewicht + Zeiger, malloc legt jeden Knoten an.

typedef struct Kante {
    int ziel;            // Nachbar-Index
    int gewicht;         // Kosten der Kante
    struct Kante *next;  // nächster Nachbar
} Kante;

Kante *e = malloc(sizeof(Kante)); // Platz fuer EINE Kante holen
e->ziel    = 2;    // diese Kante fuehrt zu Knoten 2 (= C)
e->gewicht = 1;    // sie kostet 1
e->next    = NULL; // dahinter kommt (noch) kein Nachbar → Listenende

🧩 Jedes Werkzeug steckt drin

  • struct bündelt ziel + gewicht + next
  • malloc erzeugt jede Kante zur Laufzeit
  • -> füllt die Felder über den Zeiger
  • NULL markiert das Listenende (∅)

Jetzt mit gewicht — genau das, was Dijkstra zum Relaxen braucht.

Der Bogen schließt sich: dieselbe verkettete Liste wie bei BFS — nur trägt jede Kante jetzt ihr Gewicht. Mit dieser Struktur lässt sich Dijkstra (und BFS) sauber implementieren. Genau das übt ihr im Lab.

🤔 Mini-Quiz

Im Beispiel kostet die direkte Kante A→B 4. Warum sagt Dijkstra trotzdem dist[B] = 3?

A) Weil Dijkstra die kürzeste Kante zählt
B) Weil der Umweg A→C→B mit 1+2 = 3 billiger ist als die direkte 4
C) Weil A→B nicht existiert
D) Weil BFS immer 3 Schritte braucht

🤔 Mini-Quiz — Runde 2

p ist ein Zeiger auf ein struct (z. B. eine per malloc erzeugte Kante). Wie setzt man das Feld gewicht?

A) p.gewicht = 1;
B) p->gewicht = 1;
C) gewicht.p = 1;
D) *gewicht = 1;

Zusammenfassung — in einer Slide

  • BFS findet kürzeste Wege in Schritten — aber nur, wenn alle Kanten gleich teuer sind.
  • Dijkstra summiert Gewichte: immer den billigsten offenen Knoten fertig machen, dann seine Kanten relaxen.
  • Relax: if (dist[u] + gewicht < dist[v]) dist[v] = dist[u] + gewicht;
  • BFS = Dijkstra mit allen Gewichten 1 — zwei Stufen derselben Idee.
  • struct bündelt die Daten eines Knotens/einer Kante · Zugriff . bzw. -> (bei Zeigern).
  • malloc(n*sizeof(Typ)) holt Speicher zur Laufzeit (auf NULL prüfen!), free gibt ihn zurück.

📚 Quellen & zum Ausprobieren

🎥 Anschauen

  • Computerphile — „Dijkstra's Algorithm"
  • VisuAlgo.net — Dijkstra Schritt für Schritt animiert
  • cs50 (Harvard) — Pointers & Memory

📖 Nachlesen

  • Sedgewick, Algorithms — Shortest Paths
  • Kernighan & Ritchie — Kap. 6 (struct) & 7.8 (malloc)
Selbst-Test: Führt Dijkstra von Hand auf unserem Graphen aus, aber mit Start E. Welche Distanzen bekommt jeder Knoten? Wer die Tabelle korrekt füllt, hat heute alles verstanden.

Vielen Dank!

Nicht die wenigsten Kanten — die billigste Summe. 🧭

Prof. Dr. Alexandra Mikityuk

HTW Berlin · Büro Raum 308

Im Lab: BFS & Dijkstra mit eigener Queue, struct & malloc selbst bauen.

Nächste VL: mit Queues bauen wir Dijkstra im Code — und machen die O(n²)-Minimumsuche schnell. 🚀

1 / …