Zum Inhalt springen
Algorithmen Anfänger 30 min

Sortier-Algorithmen

Die klassischen Sortier-Algorithmen: Bubble-Sort, Selection-Sort, Merge-Sort und Quicksort - wann du welchen nutzt.

Aktualisiert:
Inhaltsverzeichnis

Sortier-Algorithmen

Sortieren ist das Hallo-Welt der Algorithmik. Du wirst es in der Praxis selten selbst schreiben (dafuer gibt es fertige Funktionen), aber die Prinzipien sind fundamental.

Warum mehrere Algorithmen?

Warum nicht einfach der schnellste? Weil “schnellste” von der Situation abhaengt:

  • Kleine Listen: einfache Algorithmen sind konstanten-arm
  • Fast-sortiert: Insertion-Sort ist schneller als Quicksort
  • Stabile Sortierung: Merge-Sort, nicht Quicksort
  • In-place: Heap-Sort, nicht Merge-Sort
  • Parallel: Merge-Sort skaliert besser

Jeder Algorithmus hat Staerken und Schwaechen.

Bubble Sort - der Einsteiger

Idee: Gehe die Liste durch, tausche benachbarte Elemente, wenn sie in falscher Reihenfolge sind. Wiederhole, bis keine Vertauschung mehr noetig.

def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

Komplexitaet

  • Best: O(n) (mit Optimierung - wenn nichts getauscht wurde, ist fertig)
  • Average: O(n²)
  • Worst: O(n²)
  • Platz: O(1) - in-place

Wann nutzen?

Nie in Produktion. Zum Lernen und fuer sehr kleine Listen OK.

Selection Sort

Idee: Finde das kleinste Element und tausche es nach vorne. Wiederhole fuer den Rest.

def selection_sort(arr):
    n = len(arr)
    for i in range(n):
        min_idx = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr

Komplexitaet

  • Alles: O(n²) - immer gleich
  • Platz: O(1)

Wann nutzen?

Wenn Vertauschungen teuer sind (z.B. grosse Objekte) - Selection-Sort macht maximal n Swaps.

Insertion Sort

Idee: Baue eine sortierte Liste auf, indem du jedes Element in seine richtige Position einfuegst. Wie Karten im Fach sortieren.

def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
    return arr

Komplexitaet

  • Best: O(n) - schon sortiert
  • Average: O(n²)
  • Worst: O(n²)
  • Platz: O(1)

Wann nutzen?

  • Kleine Listen (< 20 Elemente) - schneller als Quicksort in der Praxis
  • Fast-sortierte Listen
  • Als Base-Case von Hybrid-Sortieralgorithmen (Timsort, Introsort)

Merge Sort

Idee: Teile die Liste in zwei Haelften, sortiere beide rekursiv, dann merge zusammen.

def merge_sort(arr):
    if len(arr) <= 1:
        return arr

    mitte = len(arr) // 2
    links = merge_sort(arr[:mitte])
    rechts = merge_sort(arr[mitte:])

    return merge(links, rechts)

def merge(links, rechts):
    ergebnis = []
    i = j = 0
    while i < len(links) and j < len(rechts):
        if links[i] <= rechts[j]:
            ergebnis.append(links[i])
            i += 1
        else:
            ergebnis.append(rechts[j])
            j += 1
    ergebnis.extend(links[i:])
    ergebnis.extend(rechts[j:])
    return ergebnis

Komplexitaet

  • Alles: O(n log n) - konsistent schnell
  • Platz: O(n) - braucht Hilfsspeicher

Eigenschaften

  • Stabil: gleiche Werte behalten ihre Reihenfolge
  • Vorhersehbar: immer O(n log n)
  • Parallelisierbar: beide Haelften koennen parallel sortiert werden

Wann nutzen?

  • Wenn Stabilitaet wichtig ist
  • Wenn Worst-Case wichtig ist (bei Real-Time-Anwendungen)
  • Fuer Linked Lists (einfach implementierbar)

Quicksort

Idee: Waehle einen Pivot, partitioniere die Liste in “kleiner als Pivot” und “groesser”, sortiere beide rekursiv.

def quicksort(arr):
    if len(arr) <= 1:
        return arr

    pivot = arr[len(arr) // 2]
    links = [x for x in arr if x < pivot]
    mitte = [x for x in arr if x == pivot]
    rechts = [x for x in arr if x > pivot]

    return quicksort(links) + mitte + quicksort(rechts)

Komplexitaet

  • Best/Average: O(n log n)
  • Worst: O(n²) - wenn der Pivot immer schlecht ist (sortierte Liste, naiver Pivot)
  • Platz: O(log n) fuer den Rekursions-Stack

Vorteile in der Praxis

  • Schnellste Konstanten - in der Praxis oft schneller als Merge-Sort
  • In-place moeglich (mit clevererer Implementation)
  • Gute Cache-Lokalitaet

Nachteile

  • Nicht stabil
  • Worst-Case O(n²) - in boesen Faellen langsam

In-Place-Variante

def quicksort_inplace(arr, low=0, high=None):
    if high is None:
        high = len(arr) - 1
    if low < high:
        p = partition(arr, low, high)
        quicksort_inplace(arr, low, p - 1)
        quicksort_inplace(arr, p + 1, high)

def partition(arr, low, high):
    pivot = arr[high]
    i = low - 1
    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1

Heap Sort

Idee: Baue aus den Elementen einen Max-Heap, dann nimm immer das Maximum ans Ende.

import heapq

def heap_sort(arr):
    heap = arr.copy()
    heapq.heapify(heap)   # O(n)
    return [heapq.heappop(heap) for _ in range(len(heap))]

Komplexitaet

  • Alles: O(n log n) - wie Merge-Sort
  • Platz: O(1) - in-place moeglich

Wann nutzen?

  • Wenn du garantiertes O(n log n) und O(1) Platz brauchst
  • Nicht stabil - deshalb seltener genutzt als Merge oder Quick

Counting Sort

Fuer kleine Zahlenbereiche (z.B. Alter, Noten) - extrem schnell:

def counting_sort(arr, max_val):
    counts = [0] * (max_val + 1)
    for x in arr:
        counts[x] += 1

    ergebnis = []
    for wert, anzahl in enumerate(counts):
        ergebnis.extend([wert] * anzahl)
    return ergebnis

print(counting_sort([3, 1, 4, 1, 5, 9, 2, 6], 10))
# [1, 1, 2, 3, 4, 5, 6, 9]

Komplexitaet

  • Zeit: O(n + k) - k = max_val
  • Platz: O(k)

Wann nutzen?

Wenn k klein ist (z.B. Noten 1-6, Alter 0-120). Bei grossen k-Werten ineffizient.

Radix Sort

Sortiert Zahlen digit-by-digit - O(nk) wobei k = Anzahl Ziffern. Fuer Integer oft schneller als Vergleichs-basierte Algorithmen.

Pythons Timsort

Pythons sorted() und list.sort() nutzen Timsort - einen Hybrid aus Merge-Sort und Insertion-Sort, optimiert fuer real-world data (oft schon teilweise sortiert).

sorted(arr)        # O(n log n), stabil
arr.sort()          # gleiche, aber in-place

Immer, wenn du in Python sortierst: nutze die eingebaute Funktion, nicht deine eigene. Sie ist optimiert und korrekt.

Key-Funktion

woerter = ["Anna", "max", "Leo"]

sorted(woerter, key=str.lower)     # case-insensitive
sorted(woerter, key=len)           # nach Laenge
sorted(objekte, key=lambda x: x.alter)  # nach Attribut
sorted(data, key=lambda x: (x.alter, x.name))  # nach mehreren

Vergleich

AlgorithmusBestAverageWorstPlatzStabil
Bubble SortO(n)O(n²)O(n²)O(1)Ja
Selection SortO(n²)O(n²)O(n²)O(1)Nein
Insertion SortO(n)O(n²)O(n²)O(1)Ja
Merge SortO(n log n)O(n log n)O(n log n)O(n)Ja
QuicksortO(n log n)O(n log n)O(n²)O(log n)Nein
Heap SortO(n log n)O(n log n)O(n log n)O(1)Nein
TimsortO(n)O(n log n)O(n log n)O(n)Ja

Welcher wann?

  • In Python/JS/allem Modernen: eingebaute Funktion (Timsort, Dual-Pivot-Quicksort, …)
  • Selbst schreiben?: Merge-Sort zum Lernen, Quicksort fuer Performance
  • Sehr klein (< 20): Insertion-Sort
  • Kleine Integer-Bereiche: Counting/Radix

Praktisches Beispiel

Sortiere Personen nach mehreren Kriterien:

leute = [
    {"name": "Anna", "alter": 28, "stadt": "Berlin"},
    {"name": "Max",  "alter": 34, "stadt": "Muenchen"},
    {"name": "Leo",  "alter": 28, "stadt": "Koeln"},
    {"name": "Mia",  "alter": 28, "stadt": "Berlin"},
]

# Primaer nach Alter, sekundaer nach Stadt
sortiert = sorted(leute, key=lambda p: (p["alter"], p["stadt"]))

for p in sortiert:
    print(p["name"], p["alter"], p["stadt"])

Das nutzt Timsort + Tupel-Vergleich. Elegant.

Zusammenfassung

  • Quadratische Algorithmen (Bubble, Selection, Insertion) fuer Lernen und kleine Listen
  • O(n log n): Merge-Sort (stabil, braucht Platz), Quicksort (schnell, nicht stabil)
  • Heap-Sort fuer garantiertes O(n log n) mit O(1) Platz
  • Counting/Radix fuer spezielle Faelle mit kleinen Zahlen
  • Immer eingebaute Sortierung nutzen - Timsort/dual-pivot sind sehr gut

Im naechsten Kapitel: Such-Algorithmen - linear, binaer und mit Hash.

Zurück zum Algorithmen Kurs