Fortgeschrittene Algorithmen und Programmierung
Prof. Dr. Alexandra Mikityuk
HTW Berlin
BFS erkundet einen Graphen Ebene für Ebene (wie eine Welle im Wasser) und findet so den kürzesten Weg — gemessen in Anzahl Schritten.
Eine To-do-Liste: vorne raus (besuchen), neue Nachbarn hinten dran. So entsteht die Wellen-Reihenfolge ganz von allein.
A → B,C (1 Schritt) → D,E (2 Schritte). Jeder Knoten wird beim ersten Erreichen auf kürzestem Weg gefunden.
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.
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.
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?
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.
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).
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.
Distanz zum Start ist 0, alle anderen erst mal ∞ (unendlich = „noch nicht erreicht").
Nimm den noch offenen Knoten mit der kleinsten Distanz und markiere ihn als endgültig „fertig".
Für jeden Nachbarn: Ist der Weg über mich billiger als sein bisheriger? Dann verbessern (= Relax).
Das ist der eine geniale Trick von Dijkstra — und der Punkt, an dem die meisten kurz stocken. Schauen wir ihn uns ganz langsam an.
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.
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. ✓
„Relax" klingt kompliziert, ist aber nur ein Vergleich: Komme ich über meinen Nachbarn billiger ans Ziel als bisher? Wenn ja — neuen, besseren Wert eintragen.
– – direkt A→B = 4 · Umweg A→C→B = 1+2 = 3
// 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
}
Fünf Orte A–E, die Zahlen an den Kanten sind die Kosten. Wir suchen die billigste Strecke von A zu jedem anderen Ort.
A→C→B) sind es nur 1 + 2 = 3. Genau diese Verbesserung wird Dijkstra gleich finden.
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".
A fertig, seine Kanten relaxed
| fertig | A | B | C | D | E |
|---|---|---|---|---|---|
| Start | 0 | ∞ | ∞ | ∞ | ∞ |
| fertig A | 0 | 4 | 1 | ∞ | ∞ |
C = 1 — nicht B = 4! Genau nach diesem Muster geht es weiter. Jetzt die ganze Tabelle:
Wir führen eine Distanz-Tabelle. Grün = gerade verbessert (relaxed), blau = fertig (endgültig). Immer den kleinsten offenen Wert als Nächstes.
Referenz: der gewichtete Graph
| Schritt: fertig | A | B | C | D | E |
|---|---|---|---|---|---|
| Start | 0 | ∞ | ∞ | ∞ | ∞ |
| fertig A (0) | 0 | 4 | 1 | ∞ | ∞ |
| fertig C (1) | 0 | 3 | 1 | 6 | ∞ |
| fertig B (3) | 0 | 3 | 1 | 4 | ∞ |
| fertig D (4) | 0 | 3 | 1 | 4 | 7 |
| fertig E (7) | 0 | 3 | 1 | 4 | 7 |
dist[B] von 4 auf 3 (Weg über C!). Beim Fertigstellen von B sinkt dist[D] von 6 auf 4. Genau das macht „Relax".
Dijkstra liefert die günstigste Distanz von A zu jedem Knoten — und die fett markierten Kanten bilden den Kürzeste-Wege-Baum.
| von A nach | Kosten | Weg |
|---|---|---|
| C | 1 | A→C |
| B | 3 | A→C→B |
| D | 4 | A→C→B→D |
| E | 7 | A→C→B→D→E |
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) | |
|---|---|---|
| misst | Anzahl Kanten (Schritte) | Summe der Gewichte |
| nächster Knoten | vorderster in der Queue | der mit kleinster Distanz |
| Kante prüfen | besucht? ja/nein | Relax: billiger? dann verbessern |
| passt für | ungewichtete Graphen | gewichtete Graphen (km, min, €) |
| Aufwand (Big-O) | O(n + Kanten) — linear | O(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², …)
Dijkstras Garantie („den Kleinsten abhaken ist endgültig") gilt nur, wenn Kanten nie negativ sind. Ein kleines Gegenbeispiel zeigt, warum.
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.
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.
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.
Schaut auf die Tabelle von vorhin: Dijkstra muss zu jedem Knoten mehrere Dinge gleichzeitig festhalten. Bisher wären das viele lose Variablen.
| fertig | A | B | C | D | E |
|---|---|---|---|---|---|
| Start | 0 | ∞ | ∞ | ∞ | ∞ |
| A (0) | 0 | 4 | 1 | ∞ | ∞ |
| C (1) | 0 | 3 | 1 | 6 | ∞ |
| B (3) | 0 | 3 | 1 | 4 | ∞ |
| D (4) | 0 | 3 | 1 | 4 | 7 |
| E (7) | 0 | 3 | 1 | 4 | 7 |
Jede Spalte ist ein Knoten. Pro Knoten führt Dijkstra seine Distanz und ob schon „fertig" — genau das muss C sich merken.
char name[5];
int dist[5];
int fertig[5];
// alle 3 beschreiben DIESELBEN Knoten,
// aber nichts im Code verbindet sie 😵
5 Slots = 5 Knoten (A–E), Index 0–4. Spalte [2] = Knoten C (name C, dist 1, fertig ✓) — dieselbe Sache, über 3 Arrays verstreut.
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.
A ──► B ──► ∅ auftauchten.
struct — Daten bündelnEin struct ist ein selbst gebauter Datentyp: eine beschriftete Schublade mit Fächern. Perfekt, um alles zu einem Knoten an einem Ort zu halten.
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 .
. 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.
->Sobald ihr (wie bei der Nachbarliste aus VL 10) einen Zeiger auf ein struct habt, schreibt man statt des Punkts den Pfeil ->.
Knoten k; k.dist = 1; // Variable → .
Knoten *p=&k; p->dist = 1; // Zeiger → ->
p->dist ist nur die Kurzform für (*p).dist: erst dem Zeiger folgen (das Dereferenzieren * aus VL 10), dann ins Fach.
. · habt ihr einen Zeiger darauf → Pfeil ->. Den Pfeil braucht gleich jeder dynamisch erzeugte Knoten.
malloc — Speicher zur LaufzeitEin 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.
Eure normalen Variablen & festen Arrays. Wird automatisch aufgeräumt. Größe muss vorher feststehen.
Ein großes Lager. Mit malloc holt ihr euch zur Laufzeit genau so viel, wie ihr braucht — und gebt es mit free zurück.
#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.
NULL liefern (kein Platz) → prüfen, bevor man folgt. Und dem NULL-Zeiger zu folgen = Absturz.
struct, Zeiger und malloc sind mächtig — und genau hier passieren die häufigsten Fehler. Diese drei erkennt ihr ab jetzt sofort.
// p ist ein Zeiger!
p.dist = 1; ❌
p->dist = 1; ✅
Zeiger → Pfeil. Den Punkt nur bei der Variablen selbst.
x = malloc(...);
// ... benutzen ...
free(x); ✅
Jedes malloc braucht genau ein free — sonst Memory Leak.
p = malloc(...);
if (p == NULL) ...
p->dist = 1; // erst danach
Erst prüfen, dann folgen. Dem NULL-Zeiger zu folgen = Absturz.
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.
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
Jetzt mit gewicht — genau das, was Dijkstra zum Relaxen braucht.
dist[B] = 3?dist[B] von 4 auf 3 verbessert. Mehr Kanten, aber billiger — genau der Punkt, an dem BFS scheitern würde.
p ist ein Zeiger auf ein struct (z. B. eine per malloc erzeugte Kante). Wie setzt man das Feld gewicht?p.gewicht = 1;p->gewicht = 1;gewicht.p = 1;*gewicht = 1;->. p->gewicht = (*p).gewicht: erst folgen, dann ins Fach. Den Punkt . nimmt man nur bei der Variablen selbst. (Und nicht vergessen: das per malloc Geholte am Ende freeen.)
if (dist[u] + gewicht < dist[v]) dist[v] = dist[u] + gewicht;. bzw. -> (bei Zeigern).(n*sizeof(Typ)) holt Speicher zur Laufzeit (auf NULL prüfen!), free gibt ihn zurück.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. 🚀