Fortgeschrittene Algorithmen und Programmierung
Prof. Dr. Alexandra Mikityuk
HTW Berlin
O(1), O(log n), O(n), O(n²) praktisch bedeuten — und wie stark sie sich unterscheidenO(n²)) und mit der richtigen Datenstruktur (Min-Heap) auf O(E log V) drückenGraphen, Zeiger, BFS — kürzeste Wege in Schritten.
Dijkstra-Idee (Relax, greedy) + die Werkzeuge struct & malloc.
Drei Teile: 1) Dijkstra fertig lesen · 2) Big-O & optimieren · 3) automatisieren.
Pseudocode liest sich abstrakt — deshalb zuerst der Fluss. Der Kern ist eine Schleife: schnapp dir den billigsten offenen Knoten, verbessere seine Nachbarn, wiederhole.
dist = ∞, dist[Start] = 0dist[u] + w < dist[v]u = der Knoten, den wir gerade fertig machen (der aktuell billigste offene). Merkhilfe: u = untersucht.
v = ein Nachbar von u, den man über u erreichen kann. v = verbunden.
w = das Gewicht der Kante u→v (km, Minuten, €). w = weight / Weg-Kosten.
dist[x] = die bisher billigste bekannte Distanz vom Start zu Knoten x. Startet bei ∞ und wird durch RELAX immer kleiner.
Zuerst der Algorithmus ohne Sprache. Wer in Python, Java oder Rust programmiert, kann das 1:1 übersetzen — die Idee ist überall gleich.
u = aktueller (billigster) Knoten · v = ein Nachbar von u · w = Gewicht der Kante u→v · dist[x] = billigste bekannte Distanz zu x · fertig[x] = schon endgültig abgehakt? (Start: alle false) · offen = noch nicht fertig
Die billigste bekannte Distanz zu jedem Knoten. Start = 0, Rest = INF.
INF = das ∞ aus dem Pseudocode: INT_MAX = die größte int-Zahl (2 147 483 647). Als „unendlich" nutzbar, weil keine echte Distanz je so groß wird. Bedeutet: noch kein Weg bekannt.
Wer ist schon endgültig „abgehakt"? 0 = offen, 1 = fertig.
Die Adjazenzliste aus VL 11 — pro Knoten eine Liste seiner Kanten (Ziel + Gewicht).
start = Nummer des Startknotens (0 = A). Das ist genau die Tabelle von VL 11 — pro Knoten dist und fertig. Jetzt in einem kompakten Programm.
Kein Lab zum Üben — deshalb einmal kompakt statt in Häppchen: das komplette Programm mit den drei Schritten markiert. Ihr müsst es nicht auswendig tippen, sondern lesen können.
typedef struct Kante { int ziel, gewicht; struct Kante *next; } Kante;
int dist[N], fertig[N], start = 0;
// (1) INIT: alles unbekannt (INF), Start = 0
for (int i = 0; i < N; i++) { dist[i] = INF; fertig[i] = 0; }
dist[start] = 0;
for (int schritt = 0; schritt < N; schritt++) {
// (2) billigsten OFFENEN Knoten u waehlen
int u = -1;
for (int i = 0; i < N; i++) {
if (fertig[i]) continue; // schon abgehakt
if (dist[i] == INF) continue; // unerreichbar
if (u == -1 || dist[i] < dist[u]) // erster ODER billiger?
u = i;
}
if (u == -1) break; // nichts Offenes mehr -> fertig
fertig[u] = 1;
// (3) RELAX: Nachbarn von u ueber die Kanten verbessern
for (Kante *e = adj[u]; e != NULL; e = e->next)
if (dist[u] + e->gewicht < dist[e->ziel])
dist[e->ziel] = dist[u] + e->gewicht;
}
(1) Init · (2) den Kleinsten nehmen (greedy) · (3) relaxen — dann wiederholen. Exakt der Pseudocode, ~15 Zeilen.
u = billigster offener Knoten
e = Zeiger auf eine Kante von u
e->ziel = Nachbar, e->gewicht = Kosten
u=-1 = „noch keinen"; INF = ∞
O(n²). Hier setzen wir gleich den Hebel an.
Start A: das Programm gibt für jeden Knoten die kürzeste Distanz aus.
A -> A: 0
A -> B: 3 // via C, nicht direkt!
A -> C: 1
A -> D: 4
A -> E: 7
Nach B führt die direkte Kante A→B = 4. Aber der Umweg A→C→B = 1+2 = 3 ist billiger. Dijkstra findet das — kürzester Weg heißt nicht wenigste Kanten, sondern kleinste Summe.
Derselbe Algorithmus, den ihr gerade gelesen habt, läuft milliardenfach am Tag:
Google Maps, Auto-Navi, ÖPNV-Apps: kürzeste/schnellste Route. Kanten = Straßen, Gewicht = Minuten oder km.
Internet-Router bestimmen den günstigsten Weg für Datenpakete. Gewicht = Leitungskosten.
Das Protokoll OSPF = Open Shortest Path First („offener Kürzeste-Wege-zuerst") nutzt dafür direkt Dijkstra.
Wege-Findung für Figuren und Roboter (oft A*, eine Dijkstra-Erweiterung). Gewicht = Geländekosten.
Wir machen N Runden (einen Knoten pro Runde fertig). Um den Kleinsten zu finden, läuft die for-Schleife über alle N Plätze — fertige überspringt sie mit !fertig[i], aber anschauen muss sie jeden.
N Runden × ~N Blicke = O(n²).
Das ist der andere Teil des Algorithmus — und der ist billig. Jeder Knoten schaut, sobald er fertig wird, einmal seine eigenen Kanten an.
Beispiel: 5 Knoten, 7 Kanten. A guckt seine Kanten an, B seine, C seine … Über alle Knoten summiert wird jede Kante genau 1× angefasst → nur 7 Relax-Checks insgesamt (nicht 5×5).
So viele Schritte wie es Kanten gibt (E = Edges) → in Big-O: O(E). Gegen die 25 Blicke der Minimum-Suche ist das winzig.
O(n²). Das eigentliche Problem ist nicht welche Knoten wir ansehen, sondern dass wir überhaupt suchen müssen. Genau das nimmt uns gleich der Min-Heap ab: er liefert den Kleinsten ohne Suche.
Big-O hattet ihr schon in VL 5 — hier die Kurz-Auffrischung, weil wir es gleich für Dijkstra brauchen. Es beschreibt, wie die Laufzeit wächst, wenn die Eingabe größer wird — nicht die genaue Zeit in Sekunden.
1. Konstanten weg: O(2n), O(n+100) → alles O(n). Ob 2 oder 100 Schritte pro Element ist egal — es geht ums Wachstum.
2. Nur der größte Term zählt: O(n² + n) → O(n²). Bei großem n erdrückt n² alles andere.
O(1) — immer gleich, egal wie groß (Array-Zugriff).
O(log n) — wächst kaum (Halbieren / Heap).
O(n) — proportional (einmal durchlaufen).
O(n²) — jeder mit jedem (verschachtelte Schleife).
O(n²) von der Heap-Version.
Angenommen, ein Schritt dauert 1 Nanosekunde. So viele Schritte braucht jede Komplexität bei wachsendem n:
| n (Eingabegröße) | O(log n) | O(n) | O(n log n) | O(n²) |
|---|---|---|---|---|
| 10 | ~3 | 10 | ~33 | 100 |
| 1 000 | ~10 | 1 000 | ~10 000 | 1 000 000 |
| 1 000 000 | ~20 | 1 Mio | ~20 Mio | 1 000 000 000 000 |
| Bei 1 Mio: Zeit | 20 ns | 1 ms | 20 ms | ~17 Minuten |
O(n²) ≈ 17 Minuten, O(n log n) nur 20 ms — fünfzigtausendmal schneller. Nicht ein bisschen — eine andere Welt. Deshalb optimieren wir.
Wir tauschen das „jedes Mal alle durchsuchen" gegen eine clevere Warteschlange, die uns den Kleinsten sofort gibt.
„Zeig mir den Kleinsten" → alle N durchsehen → O(n) pro Runde.
Eine Warteschlange, die immer das kleinste Element vorne hat. „Zeig mir den Kleinsten" → sofort.
Ein Binärbaum, in dem jeder Elternknoten ≤ seine Kinder ist. Folge: der kleinste steht ganz oben.
Kleinsten ansehen: O(1). Einfügen & Kleinsten entfernen: O(log n) — nur den Baum-Pfad reparieren, nicht alles durchsuchen.
O(n)), kostet „gib den Kleinsten" nur noch O(log n) — man repariert nur einen Baum-Pfad. Warum das genau log n ist, sehen wir auf der nächsten Folie.
log n klingt abstrakt — ist aber ganz einfach: „Wie oft muss ich verdoppeln, um von 1 bis n zu kommen?" Genau das schauen wir jetzt an.
Verdopplungs-Leiter — zähle die Schritte:
Von 1 bis 1 000 000 sind es nur 20 Verdopplungen → log₂(1 000 000) ≈ 20.
Jede Ebene nach unten verdoppelt die Zahl der Knoten: 1, 2, 4, 8, … Also ist „Anzahl Ebenen" genau „Anzahl Verdopplungen" = log₂ n. Ein Baum mit 1 Mio Knoten ist darum nur ~20 Ebenen hoch — nicht 1 Mio!
Die zwei Heap-Aktionen: Einfügen = eine neue Zahl in den Heap legen · Entfernen = den Kleinsten herausnehmen (das „gib den Kleinsten").
Danach kann die Regel „Eltern ≤ Kinder" kurz verletzt sein — der Heap repariert sich, aber nur entlang eines Wegs von oben nach unten (eine Ebene pro Schritt). Bei nur ~log n Ebenen sind das nur ~log n Schritte. Die anderen Knoten fasst man nie an — statt alle n zu durchsuchen.
log n). 1000× mehr Knoten = nur ~10 Ebenen mehr. Das ist der ganze Grund, warum der Heap so schnell ist.
| Minimum finden | Gesamtaufwand | |
|---|---|---|
| Einfach (Array-Scan) | O(n) pro Runde | O(n²) |
| Mit Min-Heap | O(log n) pro Operation | O(E · log V) |
E · log V viel kleiner als n² — der Unterschied zwischen „geht nicht" und „Millisekunden". Genau das steckt in eurem Navi.
V = Knoten, E = Kanten. (Den Min-Heap selbst zu bauen ist ein eigenes Thema — Idee & Nutzen zählen hier.)
Schneller ist nicht immer besser. Der Heap lohnt sich nicht überall:
Große Daten: Millionen Knoten (Landkarte, Netzwerk). Da entscheidet O(n²) vs. O(E·log V) über benutzbar oder nicht.
Heißer Pfad: Code, der oft läuft (jede Route-Anfrage von Millionen Nutzern).
Kleine Daten: 20 Knoten? Beide Versionen fühlen sich sofort an — die einfache ist kürzer & weniger fehleranfällig.
Läuft nur selten: ein Skript einmal pro Woche muss nicht schnell sein.
Was wir bei Dijkstra gemacht haben, ist ein allgemeines Muster — für jedes Programm:
Finde heraus, wo die Zeit wirklich draufgeht (Profiler, Stoppuhr). Menschen raten fast immer falsch.
Nur der teuerste Teil zählt. Bei Dijkstra: die Minimum-Suche — nicht das Relaxen.
Oft kommt der große Sprung nicht durch „schnelleren Code", sondern durch die richtige Struktur (Array-Scan → Min-Heap).
O(n²) → O(E·log V) gewonnen. Datenstrukturen sind der Hebel.
Beim Entwickeln macht ihr dieselben Schritte immer wieder: kompilieren, starten, testen. Automatisierung heißt: einmal aufschreiben, der Rechner wiederholt es.
gcc … tippen, starten, Ausgabe prüfen, Tippfehler, nochmal … bei jeder Änderung.
Ein Befehl — make oder ./build.sh — macht alles. Reproduzierbar, kein Vergessen.
Weniger Fehler, mehr Zeit fürs Denken. So arbeiten Profis (und CI-Systeme) wirklich.
Nicht alles — aber drei Arten von Aufgaben schreien geradezu danach:
Was man immer wieder gleich macht: bauen, starten, Dateien kopieren, umbenennen. Langweilig und fehleranfällig — perfekt fürs Skript.
Testen, ob nach einer Änderung noch alles stimmt. Der Rechner vergisst nie einen Test — der Mensch schon.
Dinge, die nachts, bei jeder Änderung oder bei einem Ereignis passieren sollen — ohne dass jemand dran denkt.
In echten Firmen macht niemand mehr „bauen, testen, hochladen" von Hand. Jede Code-Änderung läuft automatisch durch eine Kette — die CI/CD-Pipeline.
Jede Änderung wird sofort gebaut & getestet. Fehler fallen in Minuten auf — nicht Wochen später.
Ist alles grün, geht der Code automatisch live — ohne dass jemand von Hand etwas hochlädt.
„CI/CD" sind eigentlich drei Ausbaustufen — jede automatisiert einen Schritt mehr:
Bei jedem Commit wird automatisch gebaut & getestet. Ergebnis: „Code ist gesund" ✓ oder ✗.
Der Code ist jederzeit release-bereit. Das Live-Schalten ist nur noch ein Knopfdruck — ein Mensch gibt frei.
Sogar die Freigabe ist automatisch: besteht alles, geht es ohne Mensch direkt live.
Im „Test"-Schritt der Pipeline stecken mehrere Arten von Tests — von klein & schnell bis groß & langsam:
Testet eine kleine Funktion isoliert. Z.B.: „Gibt relax() bei diesen Zahlen das Richtige?" Millisekunden — tausende davon.
Testet, ob mehrere Teile zusammen funktionieren. Z.B.: „Lesen Graph-Loader + Dijkstra die Datei korrekt ein?"
Testet das ganze System wie ein echter Nutzer. Z.B.: „Nutzer gibt Start+Ziel ein → bekommt die richtige Route."
„Funktioniert es?" ist nur der Anfang. Moderne Pipelines prüfen bei jedem Commit auch:
| Test-Art | prüft … |
|---|---|
| Regressionstest | Kommt ein alter, schon gefixter Bug zurück? (sichert jeden gefundenen Fehler dauerhaft ab) |
| Smoke-Test | Läuft das Programm überhaupt an? (schneller Grob-Check vor allen anderen) |
| Last-/Performance-Test | Hält es 10.000 Nutzer gleichzeitig aus? Wird es langsamer? (genau hier zählt euer Big-O-Wissen!) |
| Security-Scan | Stecken bekannte Sicherheitslücken in den benutzten Bibliotheken? |
| Linter / Style-Check | Ist der Code sauber & einheitlich formatiert? (kein Fehler, aber Ordnung) |
Deployt tausende Male am Tag. Ein Programm („Chaos Monkey") legt absichtlich Server lahm, um automatisch zu prüfen, ob alles stabil bleibt.
Jede Änderung durchläuft Millionen automatischer Tests, bevor sie Nutzer erreicht — Deploy im Sekundentakt, rund um die Uhr.
Viele Nutzer? Das System startet von selbst mehr Server; nachts fährt es sie wieder herunter. Kein Mensch dreht am Regler.
Preise passen sich automatisch an, ein niedriger Lagerbestand löst Nachbestellungen aus, Empfehlungen werden für jeden Nutzer berechnet.
Nächtliche Reports und Abrechnungen laufen von selbst; die Betrugserkennung prüft jede Zahlung in Echtzeit — automatisch.
Roboter, Sensoren, Kamera-Qualitätskontrolle. Maschinen melden Wartungsbedarf selbst und sprechen miteinander — M2M (Maschine zu Maschine).
So fließt heute eine kleine Änderung durch eine Firma — fast ohne Handarbeit:
Neue Funktionen in Minuten statt Wochen ausliefern. Wer schneller liefert, gewinnt den Markt.
Automatische Tests fangen Fehler, bevor Nutzer sie sehen. Menschen vergessen — Skripte nicht.
Millionen Nutzer bedienen, ohne dass die Mannschaft mitwächst. Den Massen-Kram macht der Rechner.
Ihr müsst sie hier nicht können — nur wissen, dass es sie gibt und wofür sie da sind:
| Aufgabe | typische Werkzeuge |
|---|---|
| Bauen (Code → fertiges Programm) | Make, Gradle, npm |
| Versionieren (Historie & Teamarbeit) | Git (GitHub, GitLab) |
| Automatisch testen & ausliefern (CI/CD) | GitHub Actions, GitLab CI, Jenkins |
| Verpacken (überall gleich lauffähig) | Docker / Container |
| Zeitgesteuert starten (nachts, wöchentlich) | cron, Scheduler |
Was du 3× von Hand machst · Tests · Builds · alles Fehleranfällige oder Langweilige.
Einmalige Aufgaben · Dinge, die menschliches Urteil brauchen · wenn das Skript-Bauen länger dauert als die Handarbeit je kostet.
Ein Skript ist auch Dokumentation: es zeigt exakt, wie etwas gebaut/getestet wird — reproduzierbar für alle im Team.
O(n²) Minuten, O(n log n) Millisekunden.O(n²) → O(E·log V).Das war die letzte inhaltliche Vorlesung. Schaut, wie weit ihr gekommen seid:
Sortieren, Suchen, Rekursion, Graphen, BFS & Dijkstra — die Bausteine echter Software.
Ihr erkennt, warum etwas langsam ist, und wählt die passende Datenstruktur, um es zu lösen.
struct, malloc, Zeiger — und wie Firmen mit CI/CD automatisch bauen, testen & ausliefern.
Erst richtig, dann schnell, dann automatisch. ⚙️
Prof. Dr. Alexandra Mikityuk
HTW Berlin · Büro Raum 308
Nächste VL: Rückblick auf die ganze Vorlesungsreihe & Klausurvorbereitung.