Algorithmen & Komplexität von Grund auf lernen
1Was Komplexität misst
Ein Algorithmus ist eine endliche Liste präziser Schritte, die ein Problem löst, etwa das Sortieren einer Liste oder das Finden eines Namens in einem Telefonbuch.
Zwei Algorithmen können dasselbe Problem lösen, aber sehr unterschiedlich viel Zeit brauchen. Die Komplexität beschreibt, wie der Aufwand wächst, wenn die Eingabe größer wird.
Die Größe der Eingabe heißt n: die Anzahl der zu sortierenden Elemente, die Länge einer Liste, die Anzahl der Knoten in einem Graphen.
Stell dir vor, du suchst einen Namen in einem Telefonbuch mit 1000 Seiten. Jede Seite zu lesen dauert bis zu 1000 Schritte. Es in der Mitte aufzuschlagen und jedes Mal zu halbieren dauert etwa 10 Schritte.
Die Zeitkomplexität zählt die Schritte. Die Platzkomplexität zählt den zusätzlichen Speicher. Beide werden als Funktion von n geschrieben.
Meist betrachtet man den schlechtesten Fall (Worst Case): den größten Aufwand über alle Eingaben der Größe n. Manchmal ist stattdessen der durchschnittliche oder der beste Fall gefragt.
Selbsttest Ein Algorithmus, dessen Aufwand wie n² wächst, braucht für n = 1000 eine Sekunde. Wie lange braucht er ungefähr für n = 2000?
- 1 Sekunde
- 2 Sekunden
- 4 Sekunden
- 8 Sekunden
Antwort: 4 Sekunden. Eine Verdopplung von n multipliziert n² mit 2² = 4, die Zeit steigt also von etwa 1 auf etwa 4 Sekunden. 2 Sekunden wären die Antwort bei linearem Wachstum und 8 bei n³.
2O-Notation und die Wachstumsklassen
Die O-Notation behält nur das, was bei großem n zählt. Du lässt konstante Faktoren weg und behältst nur den am schnellsten wachsenden Term.
3n² + 5n + 2 ⇒ O(n²)
Für n = 1000 ist der n²-Term 3 000 000, während 5n nur 5000 ist. Die kleineren Terme verändern die Summe kaum.
Diese Klassen kommen immer wieder vor, vom langsamsten zum schnellsten Wachstum:
- O(1), konstant: ein Array-Element über seinen Index lesen.
- O(log n), logarithmisch: binäre Suche, die das Problem in jedem Schritt halbiert.
- O(n), linear: ein Durchlauf über alle Elemente, etwa um den größten Wert zu finden.
- O(n log n): gute Sortieralgorithmen wie Merge Sort.
- O(n²), quadratisch: zwei verschachtelte Schleifen über die Elemente, etwa Bubble Sort.
- O(2ⁿ), exponentiell: jede Teilmenge ausprobieren. Das wird schnell unbrauchbar.
Die O-Notation ist eine obere Schranke: Der Aufwand wächst höchstens so schnell. Ω (Omega) ist eine untere Schranke, und Θ (Theta) ist eine scharfe Schranke, sowohl oben als auch unten.
Die Basis eines Logarithmus spielt in der O-Notation keine Rolle, weil sich Logarithmen mit verschiedenen Basen nur um einen konstanten Faktor unterscheiden.
Selbsttest Was ist das O von 4n + 2n² + 100?
- O(100)
- O(2n²)
- O(n)
- O(n²)
Antwort: O(n²). Für großes n dominiert der n²-Term, und der konstante Faktor 2 entfällt. O(n) behält den falschen Term, und O(2n²) vergisst, die Konstante wegzulassen.
3Suchen, Sortieren und Rekursion
Einige Algorithmen kommen in fast jeder Vorlesung vor. Lerne ihre Komplexitäten als Fakten.
- Die lineare Suche prüft Elemente nacheinander: O(n). Die binäre Suche braucht ein sortiertes Array und halbiert den Bereich in jedem Schritt: O(log n).
- Bubble Sort, Insertion Sort und Selection Sort vergleichen und vertauschen Elemente in verschachtelten Schleifen: O(n²). Insertion Sort ist bei einem bereits sortierten Array O(n).
- Merge Sort teilt das Array in Hälften, sortiert sie und mischt sie zusammen: in jedem Fall O(n log n), mit O(n) zusätzlichem Speicher.
- Quicksort teilt um ein Pivotelement: im Mittel O(n log n), im schlechtesten Fall aber O(n²), zum Beispiel bei einem sortierten Array mit dem ersten Element als Pivot.
- Kein Sortierverfahren, das nur Elemente vergleicht, kann im schlechtesten Fall besser als Ω(n log n) sein. Counting Sort ist schneller, weil es nicht vergleicht.
Eine rekursive Funktion ruft sich selbst mit einer kleineren Eingabe auf, bis sie einen Basisfall erreicht, den sie direkt beantwortet. Ihre Laufzeit wird als Rekurrenz geschrieben:
- T(n) = T(n/2) + O(1) ergibt O(log n), wie bei der binären Suche.
- T(n) = 2T(n/2) + O(n) ergibt O(n log n), wie bei Merge Sort.
- T(n) = T(n − 1) + O(n) ergibt O(n²).
- T(n) = 2T(n − 1) + O(1) ergibt O(2ⁿ).
Drei Entwurfsideen sind wissenswert. Teile und herrsche (Divide and Conquer) teilt, löst und kombiniert. Gierige Algorithmen (Greedy) treffen in jedem Schritt die lokal beste Wahl. Dynamische Programmierung speichert die Antworten überlappender Teilprobleme, damit sie nur einmal berechnet werden.
Selbsttest Was ist die Zeitkomplexität von Quicksort im schlechtesten Fall?
- O(log n)
- O(n log n)
- O(n)
- O(n²)
Antwort: O(n²). Bei schlechten Pivotelementen lässt jede Teilung n − 1 Elemente auf einer Seite, was O(n²) ergibt. O(n log n) ist der durchschnittliche Fall von Quicksort und der schlechteste Fall von Merge Sort.
4Das Verfahren Schritt für Schritt
So findest du die Zeitkomplexität eines Codestücks:
- Lege fest, was n ist: die Größe der Eingabe.
- Zähle, wie oft jede Schleife läuft. Eine Schleife, die jedes Mal 1 addiert, läuft etwa n-mal. Eine Schleife, die verdoppelt oder halbiert, läuft etwa log₂ n-mal.
- Ineinander verschachtelte Schleifen multiplizieren sich. Hintereinander stehende Schleifen addieren sich.
- Schreibe bei einer rekursiven Funktion die Rekurrenz auf: wie viele Aufrufe, mit welcher Größe, plus der Aufwand pro Aufruf.
- Behalte den am schnellsten wachsenden Term und lasse die Konstanten weg.
- Prüfe mit einer kleinen Zahl, zum Beispiel n = 8 oder n = 1024.
Nenne bei einer Theoriefrage zuerst den Algorithmus oder die Idee, die gemeint ist, und rufe dann seine Komplexität und seine Bedingungen ab (sortierte Eingabe, nichtnegative Gewichte, überlappende Teilprobleme).
5Ein durchgerechnetes Beispiel
Folge jedem Schritt. Versuche, den nächsten vorherzusagen, bevor du ihn öffnest.
Durchgerechnetes BeispielWas ist die Zeitkomplexität dieses Codes? for i = 1 to n: (j = 1; while j < n: j = j × 2)
1n ist die Zahl, bis zu der die Schleifen laufen.
2i läuft von 1 bis n in Schritten von 1, die äußere Schleife läuft also n-mal.
3j nimmt die Werte 1, 2, 4, 8, … an und hört auf, wenn es n erreicht. Nach k Durchläufen ist j = 2k, die Schleife endet also nach etwa log₂ n Durchläufen.
4Die innere Schleife sitzt in der äußeren Schleife, die Anzahlen multiplizieren sich also.
n · log₂ n ⇒ O(n log n)
5Für n = 1024 läuft die innere Schleife log₂ 1024 = 10-mal, der Rumpf läuft also 1024 · 10 = 10 240-mal. Das ist weit weniger als n² = 1 048 576.
6Häufige Fehler
- Schleifen multiplizieren, die hintereinander laufen. n + n = 2n ist immer noch O(n).
- Konstanten behalten, wie in O(2n) oder O(n/2). Beide sind O(n).
- Eine Verdopplungsschleife als n Durchläufe zählen. Sie läuft etwa log₂ n-mal.
- Binäre Suche auf unsortierten Daten oder auf einer verketteten Liste verwenden, wo die Mitte nicht in einem Schritt erreichbar ist.
- Den durchschnittlichen Fall von Quicksort angeben, wenn nach dem schlechtesten Fall gefragt ist.
- Big O mit „genau“ verwechseln. O(n²) ist eine obere Schranke; Θ(n²) ist das genaue Wachstum.
Selbsttest Eine Funktion führt eine Schleife mit n Durchläufen aus und danach, getrennt davon, eine weitere Schleife mit n Durchläufen. Welche Zeitkomplexität hat sie?
- O(2n)
- O(log n)
- O(n)
- O(n²)
Antwort: O(n). Nacheinander ausgeführte Schleifen addieren sich: n + n = 2n, die Konstante entfällt, also O(n). O(n²) wäre nur richtig, wenn eine Schleife in der anderen läge.
7So sehen Prüfungsaufgaben aus
- Leicht: Begriffe und Fakten. Die Komplexität von binärer Suche, Mergesort oder Arrayzugriff, die Bedeutung von O, Ω und Θ, was Rekursion ist.
- Mittel: Anwendung der Konzepte. Die Komplexität eines Codefragments, eine zu lösende Rekurrenz, wie sich die Zeit bei Verdopplung von n ändert, welcher Algorithmus zu einer Aufgabe passt.
- Schwer: Fälle und Analyse. Das Master-Theorem, schlechteste Fälle von Quicksort, Greedy gegenüber dynamischer Programmierung sowie die Klassen P, NP und NP-vollständig.
P enthält Probleme, die in polynomieller Zeit lösbar sind. NP enthält Probleme, deren Lösungen sich in polynomieller Zeit überprüfen lassen. Jedes Problem in P liegt in NP; ob P = NP gilt, ist weiterhin unbekannt.
Lerne Schritt für Schritt in der AppLerne Schritt für Schritt in der App