Sortowanie szybkie (Quick Sort)
1. Idea algorytmu
Quick Sort to jeden z najszybszych algorytmów sortowania w praktyce. Używa strategii "dziel i zwyciężaj":
- Wybierz pivot (element odniesienia)
- Partycjonuj: Podziel listę na dwie części:
- Elementy mniejsze od pivota (po lewej)
- Elementy większe od pivota (po prawej)
- Rekurencyjnie posortuj obie części
Wizualizacja:
[38, 27, 43, 3, 9, 82, 10]
↑ pivot = 43
Partycjonowanie:
[38, 27, 3, 9, 10] | 43 | [82]
mniejsze większe
Rekurencyjnie sortuj obie strony:
[3, 9, 10, 27, 38] | 43 | [82]
Wynik: [3, 9, 10, 27, 38, 43, 82]
2. Implementacja podstawowa (Lomuto partition)
Uwaga: Ta wersja jest prosta, ale używa O(n) dodatkowej pamięci.
3. Implementacja in-place (efektywniejsza)
4. Wizualizacja partycjonowania
Przykład: partition([38, 27, 43, 3, 9, 82, 10], 0, 6)
arr = [38, 27, 43, 3, 9, 82, 10]
pivot = 10 (ostatni element)
i = -1
j=0: arr[0]=38 > 10 → NIC
[38, 27, 43, 3, 9, 82, 10], i=-1
j=1: arr[1]=27 > 10 → NIC
[38, 27, 43, 3, 9, 82, 10], i=-1
j=2: arr[2]=43 > 10 → NIC
[38, 27, 43, 3, 9, 82, 10], i=-1
j=3: arr[3]=3 <= 10 → i++, ZAMIANA arr[0] ↔ arr[3]
[3, 27, 43, 38, 9, 82, 10], i=0
j=4: arr[4]=9 <= 10 → i++, ZAMIANA arr[1] ↔ arr[4]
[3, 9, 43, 38, 27, 82, 10], i=1
j=5: arr[5]=82 > 10 → NIC
[3, 9, 43, 38, 27, 82, 10], i=1
Koniec: Umieść pivot na miejscu i+1
[3, 9, 10, 38, 27, 82, 43]
↑ pivot na indeksie 2
Zwróć: 2 (indeks pivota)
Po partycjonowaniu: Wszystko po lewej <= 10, wszystko po prawej > 10.
5. Wizualizacja z debugiem
6. Złożoność czasowa
Najlepszy i średni przypadek: O(n log n) ✅
Gdy pivot dzieli listę mniej więcej na pół:
Wysokość drzewa rekurencji ≈ log(n)
Każdy poziom przetwarza n elementów
Razem: O(n log n)
Najgorszy przypadek: O(n²) ❌
Gdy pivot jest zawsze najmniejszy lub największy (lista już posortowana!):
Rozwiązanie: Losowy wybór pivota lub mediana z trzech.
7. Optymalizacja: Losowy pivot
Zalety: Średnio O(n log n) nawet dla posortowanych danych.
8. Optymalizacja: Median-of-three
Zamiast losowego – wybierz medianę z pierwszego, środkowego i ostatniego:
9. Hoare partition (alternatywny schemat)
Hoare partition to oryginalny schemat partycjonowania (szybszy niż Lomuto):
Zalety Hoare: - Mniej zamian niż Lomuto - Szybszy w praktyce
10. Złożoność pamięciowa
Standardowy (in-place): O(log n)
Najgorszy przypadek: O(n)
Gdy partycjonowanie zawsze niezrównoważone (np. już posortowane).
Optymalizacja: Tail recursion optimization (nie w Pythonie).
11. Stabilność: NIE ❌
Quick sort nie jest stabilny – może zmienić kolejność równych elementów.
Przykład:
Dlaczego? Zamiany na długim dystansie niszczą stabilność.
12. Optymalizacja: Hybrydowe podejście (Introsort)
Dla małych partycji (<10-20) – użyj insertion sort:
Zalety:
- Szybsze dla małych partycji (niski overhead insertion sort)
- Używane w C++ std::sort (Introsort)
13. Quick Select – znajdowanie k-tego najmniejszego
Quick sort można użyć do znajdowania k-tego elementu w O(n) średnio!
Złożoność: O(n) średnio, O(n²) najgorsza.
14. Benchmark
15. Podsumowanie
Quick Sort:
- Złożoność czasowa:
- Najlepszy: O(n log n)
- Średni: O(n log n) ← Najszybszy w praktyce!
- Najgorszy: O(n²) (rzadko z losowym pivotem)
- Złożoność pamięciowa: O(log n) (stos wywołań)
- Stabilny: NIE ❌
- In-place: TAK ✅
Zalety: - Najszybszy średnio ze wszystkich algorytmów sortowania - In-place (O(log n) pamięci) - Cache-friendly (dobra lokalność danych) - Prosty do zaimplementowania - Parallelizable (łatwy do zrównoleglenia)
Wady: - O(n²) w najgorszym przypadku (bez optymalizacji) - Niestabilny - Wymaga dobrego wyboru pivota
Optymalizacje: 1. Losowy pivot – unikaj O(n²) 2. Median-of-three – lepszy pivot 3. Insertion sort dla małych partycji 4. Hoare partition – mniej zamian 5. Tail recursion – oszczędność stosu (nie w Pythonie)
vs Merge Sort: - Quick: szybszy średnio, Merge: gwarantowane O(n log n) - Quick: niestabilny, Merge: stabilny - Quick: O(log n) pamięci, Merge: O(n) pamięci - Quick: in-place ✅
vs Heap Sort: - Quick: szybszy w praktyce - Quick: O(log n) pamięci, Heap: O(1) pamięci - Heap: gwarantowane O(n log n)
Kiedy używać:
✅ Ogólne sortowanie – najszybszy średnio ✅ Gdy wydajność krytyczna (duże dane) ✅ Gdy pamięć ograniczona (in-place) ✅ Quick Select – znajdowanie k-tego elementu
❌ Gdy gwarantowana wydajność wymagana (użyj merge/heap sort) ❌ Gdy stabilność wymagana (użyj merge sort)
Zastosowania w praktyce:
- C++
std::sort– Introsort (hybrydowy z quick sort) - Unix
qsort()– klasyczny quick sort - Bazy danych – sortowanie w pamięci
- Quick Select – mediana, percentyle
Co dalej warto poznać:
- Heap Sort (gwarantowane O(n log n), in-place)
- Introsort (hybrydowy: quick + heap + insertion)
- Dual-pivot Quick Sort (Java 7+)
- Parallel Quick Sort