Algorytmy sortowania danych – przegląd i porównanie wydajności
Sortowanie jest jednym z podstawowych problemów w informatyce. Polega na uporządkowaniu elementów zbioru według określonego kryterium (np. rosnąco lub malejąco). Wybór odpowiedniego algorytmu sortowania ma duże znaczenie, zwłaszcza w przypadku pracy z dużymi zbiorami danych. W artykule przedstawiono najważniejsze algorytmy sortowania, ich działanie oraz porównanie złożoności obliczeniowej.
1. Algorytmy proste
1.1 Sortowanie bąbelkowe (Bubble Sort)
- Idea: porównywanie sąsiednich elementów i zamiana ich miejscami, aż do uzyskania uporządkowanej listy.
- Złożoność:
- Czasowa:
- najlepszy przypadek: O(n) (dla już posortowanej tablicy),
- średni i najgorszy przypadek: O(n²).
- Pamięciowa: O(1).
- Czasowa:
- Zalety: prostota implementacji.
- Wady: bardzo niska wydajność przy większych zbiorach danych.
function bubbleSort(arr) {
const a = [...arr];
for (let i = 0; i < a.length; i++) {
for (let j = 0; j < a.length - i - 1; j++) {
if (a[j] > a[j+1]) {
[a[j], a[j+1]] = [a[j+1], a[j]];
}
}
}
return a;
}
1.2 Sortowanie przez wstawianie (Insertion Sort)
- Idea: elementy są wstawiane kolejno na odpowiednie miejsce w już częściowo posortowanej tablicy.
- Złożoność:
- najlepszy przypadek: O(n),
- średni i najgorszy przypadek: O(n²).
- Zalety: dobre działanie dla małych lub częściowo posortowanych zbiorów.
- Wady: nieefektywne dla dużych zbiorów.
function insertionSort(arr) {
const a = [...arr];
for (let i = 1; i < a.length; i++) {
let key = a[i];
let j = i - 1;
while (j >= 0 && a[j] > key) {
a[j+1] = a[j];
j--;
}
a[j+1] = key;
}
return a;
}
1.3 Sortowanie przez wybieranie (Selection Sort)
- Idea: w każdej iteracji wybierany jest najmniejszy element i przenoszony na początek.
- Złożoność:
- zawsze: O(n²).
- Zalety: prostota implementacji, niewielka liczba zamian elementów.
- Wady: duża liczba porównań, mała wydajność.
function selectionSort(arr) {
const a = [...arr];
for (let i = 0; i < a.length; i++) {
let minIdx = i;
for (let j = i + 1; j < a.length; j++) {
if (a[j] < a[minIdx]) minIdx = j;
}
if (minIdx !== i) {
[a[i], a[minIdx]] = [a[minIdx], a[i]];
}
}
return a;
}
1.4 Comb Sort
- Idea: Rozwinięcie Bubble Sort. Zamiast porównywać sąsiednie elementy, porównuje elementy w pewnym odstępie („gap”), który maleje z każdą iteracją (najczęściej przez współczynnik ok. 1.3). Usuwa tzw. „żółwie” — małe wartości przesuwające się bardzo wolno na początek.
- Złożoność:
- Średnia: około O(n² / 2ᵖ), gdzie p to liczba iteracji (zwykle lepsza niż Bubble),
- Najgorszy przypadek: O(n²),
- Najlepszy przypadek: O(n log n) (dla bardzo dobrze dobranego współczynnika).
- Wady: Nadal kwadratowa złożoność dla dużych danych. Nie jest stabilny. Wymaga dobrania optymalnego współczynnika „shrink”.
function combSort(arr) {
const a = [...arr];
const shrink = 1.3;
let gap = a.length;
let swapped = true;
while (gap > 1 || swapped) {
gap = Math.floor(gap / shrink);
if (gap < 1) gap = 1;
swapped = false;
for (let i = 0; i + gap < a.length; i++) {
if (a[i] > a[i + gap]) {
[a[i], a[i + gap]] = [a[i + gap], a[i]];
swapped = true;
}
}
}
return a;
}
1.5 Shell Sort
- Idea: Ulepszony Insertion Sort — zaczyna od porównywania elementów oddzielonych dużym odstępem (tzw. gap), który w każdej iteracji maleje. Dzięki temu szybko przesuwa elementy blisko właściwego miejsca.
- Złożoność:
- Średnia: od O(n log n) do O(n^1.5) w zależności od przyjętej sekwencji odstępów,
- Najgorszy przypadek: O(n²), *Najlepszy przypadek (już prawie posortowana tablica): O(n log n).
- Wady: Złożoność trudna do dokładnego określenia (zależna od sekwencji odstępów). Nie jest algorytmem stabilnym (elementy równe mogą zmienić kolejność).
function shellSort(arr) {
const a = [...arr];
let gap = Math.floor(a.length / 2);
while (gap > 0) {
for (let i = gap; i < a.length; i++) {
const temp = a[i];
let j = i;
while (j >= gap && a[j - gap] > temp) {
a[j] = a[j - gap];
j -= gap;
}
a[j] = temp;
}
gap = Math.floor(gap / 2);
}
return a;
}
1.6 Bogo Sort
- Idea: Najbardziej nieefektywny (i żartobliwy) algorytm sortowania. Polega na losowym tasowaniu elementów tablicy aż do momentu, gdy tablica przypadkowo stanie się posortowana.
- Złożoność: Średnio i w najgorszym przypadku ma złożoność O(n!), ponieważ w najgorszym razie trzeba sprawdzić wszystkie możliwe permutacje.
- Wady: Ekstremalnie wolny i nieprzewidywalny.
W praktyce bezużyteczny (stosowany tylko edukacyjnie lub humorystycznie). Może nie zakończyć się w rozsądnym czasie dla nawet małych tablic (np. n > 6).
function bogoSort(arr) {
const a = [...arr];
function isSorted(a) {
for (let i = 1; i < a.length; i++) {
if (a[i - 1] > a[i]) return false;
}
return true;
}
function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
}
let safety = 10000; // zabezpieczenie przed nieskończoną pętlą
while (!isSorted(a) && safety-- > 0) {
shuffle(a);
}
return a;
}
2. Algorytmy zaawansowane
2.1 Sortowanie szybkie (Quick Sort)
- Idea: wybór elementu (pivot), podział tablicy na dwie części i rekurencyjne sortowanie podtablic.
- Złożoność:
- najlepszy i średni przypadek: O(n log n),
- najgorszy przypadek: O(n²) (dla źle dobranego pivota).
- Pamięciowa: O(log n) (rekurencja).
- Zalety: bardzo szybki w praktyce.
- Wady: wrażliwy na wybór pivota.
function quickSort(arr) {
const a = [...arr];
quickRec(a, 0, a.length - 1);
return a;
function quickRec(a, lo, hi) {
if (lo < hi) {
const p = partition(a, lo, hi);
quickRec(a, lo, p - 1);
quickRec(a, p + 1, hi);
}
}
function partition(a, lo, hi) {
const pivot = a[hi];
let i = lo;
for (let j = lo; j < hi; j++) {
if (a[j] < pivot) {
[a[i], a[j]] = [a[j], a[i]];
i++;
}
}
[a[i], a[hi]] = [a[hi], a[i]];
return i;
}
}
2.2 Sortowanie przez scalanie (Merge Sort)
- Idea: dziel i zwyciężaj – dzielenie tablicy na mniejsze części i scalanie posortowanych fragmentów.
- Złożoność:
- zawsze: O(n log n).
- Pamięciowa: O(n) (potrzebne dodatkowe tablice).
- Zalety: stabilność, przewidywalny czas działania.
- Wady: wymaga dodatkowej pamięci.
function mergeSort(arr) {
const a = [...arr];
mergeRec(a, 0, a.length - 1);
return a;
function mergeRec(a, l, r) {
if (l >= r) return;
const m = Math.floor((l + r) / 2);
mergeRec(a, l, m);
mergeRec(a, m+1, r);
merge(a, l, m, r);
}
function merge(a, l, m, r) {
const left = a.slice(l, m+1);
const right = a.slice(m+1, r+1);
let i = 0, j = 0, k = l;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
a[k++] = left[i++];
} else {
a[k++] = right[j++];
}
}
while (i < left.length) { a[k++] = left[i++]; }
while (j < right.length) { a[k++] = right[j++]; }
}
}
2.3 Sortowanie przez kopcowanie (Heap Sort)
- Idea: budowa kopca i wyciąganie największego elementu, aż do uzyskania posortowanej listy.
- Złożoność:
- zawsze: O(n log n).
- Pamięciowa: O(1).
- Zalety: przewidywalność, brak dodatkowej pamięci.
- Wady: wolniejszy od Quick Sorta w praktyce.
function heapSort(arr) {
const a = [...arr];
const n = a.length;
function heapify(n, i) {
let largest = i;
const l = 2*i + 1;
const r = 2*i + 2;
if (l < n) { if (a[l] > a[largest]) largest = l; }
if (r < n) { if (a[r] > a[largest]) largest = r; }
if (largest !== i) {
[a[i], a[largest]] = [a[largest], a[i]];
heapify(n, largest);
}
}
// build heap
for (let i = Math.floor(n/2) - 1; i >= 0; i--) heapify(n, i);
// extract elements
for (let i = n - 1; i > 0; i--) {
[a[0], a[i]] = [a[i], a[0]];
heapify(i, 0);
}
return a;
}
3. Algorytmy specjalne
3.1 Sortowanie przez zliczanie (Counting Sort)
- Idea: Zlicza, ile razy występuje każda wartość w tablicy, a następnie na tej podstawie odtwarza posortowany wynik. Działa tylko dla liczb całkowitych z ograniczonego zakresu.
- Złożoność:
- Czasowa: O(n + k), gdzie k to zakres wartości (max - min + 1),
- Przestrzenna: O(k) (dodatkowa tablica zliczeń).
- Wady: Nie działa na liczbach zmiennoprzecinkowych ani ciągach znaków bez modyfikacji. Nieskuteczny, gdy zakres wartości (k) jest dużo większy od liczby elementów (n).
function countingSort(arr) {
const a = [...arr];
const min = Math.min(...a);
const max = Math.max(...a);
const count = Array(max - min + 1).fill(0);
for (let i = 0; i < a.length; i++) {
count[a[i] - min]++;
}
let index = 0;
for (let i = 0; i < count.length; i++) {
while (count[i]-- > 0) {
a[index++] = i + min;
}
}
return a;
}
3.2 Sortowanie kubełkowe (Bucket Sort)
- Idea: rozdzielanie elementów do kubełków (przedziałów), a następnie sortowanie w każdym kubełku i scalanie wyników.
- Złożoność: O(n + k), gdzie k to liczba kubełków.
- Wady: zależne od rozkładu danych.
function bucketSort(arr) {
if (arr.length === 0) return [];
// Tworzymy puste kubełki
const numBuckets = Math.floor(Math.sqrt(arr.length)) || 1;
const buckets = Array.from({ length: numBuckets }, () => []);
// Znajdujemy minimalną i maksymalną wartość, by rozłożyć elementy proporcjonalnie
const minValue = Math.min(...arr);
const maxValue = Math.max(...arr);
const range = maxValue - minValue || 1;
// Rozdzielamy elementy do kubełków
for (let num of arr) {
const index = Math.floor(((num - minValue) / range) * (numBuckets - 1));
buckets[index].push(num);
}
// Sortujemy każdy kubełek i łączymy w jedną tablicę
return buckets.flatMap(bucket => bucket.sort((a, b) => a - b));
}
3.3 Sortowanie pozycyjne (Radix Sort)
- Idea: sortowanie według kolejnych cyfr/liczb znaczących.
- Złożoność: O(nk), gdzie k to liczba cyfr.
- Zalety: bardzo szybkie dla liczb całkowitych.
- Wady: ograniczone zastosowanie (nie działa bezpośrednio na wszystkich typach danych).
function radixSort(arr) {
if (arr.length === 0) return [];
const maxNum = Math.max(...arr);
let divisor = 1;
let result = [...arr];
// Pętla przez kolejne miejsca dziesiętne
while (Math.floor(maxNum / divisor) > 0) {
const buckets = Array.from({ length: 10 }, () => []);
for (let num of result) {
const digit = Math.floor((num / divisor) % 10);
buckets[digit].push(num);
}
result = buckets.flat();
divisor *= 10;
}
return result;
}
4. Porównanie algorytmów sortowania
| Algorytm | Najlepszy przypadek | Średni przypadek | Najgorszy przypadek | Pamięć dodatkowa |
|---|---|---|---|---|
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) |
| Comb Sort | O(n log n) | O(n² / 2ᵖ) | O(n²) | O(1) |
| Shell Sort | O(n log n) | O(n^1.5) | O(n²) | O(1) |
| Bogo Sort | O(n) (teoretycznie) | O(n!) | O(n!) | O(1) |
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) |
| Counting Sort | O(n + k) | O(n + k) | O(n + k) | O(k) |
| Bucket Sort | O(n + k) | O(n + k) | O(n²) | O(n + k) |
| Radix Sort | O(nk) | O(nk) | O(nk) | O(n + k) |
Porównanie złożoności obliczeniowej
Wizualizacja teoretycznych złożoności: O(n), O(n log n), O(n²). Możesz przełączać konkretne algorytmy.
Włącz/wyłącz algorytmy
Algorytmy zaawansowane
Legenda i wskazówki
Tutaj wykres przedstawia krzywe znormalizowane (domyślnie). Możesz zmienić Maksymalny rozmiar n, przełączyć skalę osi Y na logarytmiczną, lub wyłączyć normalizację, by zobaczyć surowe kształty krzywych.
- O(n²) — algorytmy proste (bardzo strome dla dużych n).
- O(n log n) — algorytmy efektywne dostępne w standardowych bibliotekach.
- Pobierz wykres przyciskiem, aby dołączyć go do dokumentacji.
Wizualizacja algorytmów sortowania
5. Playground - wypróbuj kod!
Wpisz JavaScript i sprawdź wynik bezpośrednio na stronie.
W tym edytorze console.log() wypisuje wynik w okienku poniżej. Możesz też użyć print() – działa identycznie.
W funkcjach asynchronicznych np. setTimeout() używaj print() ponieważ console.log() nie zadziała w okienku poniżej.
Wynik:
6. Dla jakich danych najlepszy jest dany algorytm sortowania
| Algorytm | Najlepiej działa dla... | Uzasadnienie / Charakterystyka |
|---|---|---|
| Bubble Sort | Małe lub prawie posortowane zbiory | Bardzo prosty, ale powolny dla dużych danych; stabilny. |
| Insertion Sort | Małe lub prawie posortowane tablice | Działa bardzo szybko przy niewielkich przesunięciach; stabilny. |
| Selection Sort | Małe tablice, gdzie stabilność nie jest ważna | Prosty i przewidywalny, ale zawsze O(n²); mało zamian. |
| Comb Sort | Średniej wielkości dane, losowe wartości | Szybszy od Bubble; dobrze radzi sobie z danymi bez wzorca. |
| Shell Sort | Średniej wielkości tablice, dowolne dane | Działa dobrze przy różnym rozkładzie; prosty i szybki bez dodatkowej pamięci. |
| Bogo Sort | Tylko cele edukacyjne lub humorystyczne | Bezużyteczny praktycznie – losuje aż trafi porządek. |
| Quick Sort | Duże zbiory losowych danych | Bardzo szybki w praktyce; mała pamięć dodatkowa; niestabilny. |
| Merge Sort | Duże dane wymagające stabilności | Świetny dla dużych tablic lub danych na dysku; stabilny, ale wymaga O(n) pamięci. |
| Heap Sort | Duże tablice, gdy ważna jest mała pamięć | Bardzo przewidywalny, dobry kompromis między szybkością a zużyciem pamięci; niestabilny. |
| Counting Sort | Dane całkowite w małym, znanym zakresie | Bardzo szybki (O(n+k)), nieporównawczy, ale wymaga ograniczonego zakresu. |
| Bucket Sort | Dane rzeczywiste równomiernie rozłożone | Bardzo wydajny dla danych np. z [0,1); wymaga odpowiedniego rozkładu. |
| Radix Sort | Duże zbiory liczb całkowitych o podobnej długości | Bardzo szybki, stabilny, nieporównawczy; świetny dla np. ID, kodów, ZIPów. |
7. Szybkie podsumowanie typu zastosowań
| Typ danych / sytuacja | Najlepsze algorytmy | Dlaczego |
|---|---|---|
| Małe tablice (n < 50) | Insertion, Bubble, Selection | Proste i intuicyjne; brak dużej różnicy czasowej. |
| Prawie posortowane dane | Insertion, Bubble, Shell | Minimalna liczba porównań i przesunięć. |
| Losowe duże dane | Quick, Merge, Heap | O(n log n) — najbardziej wydajne dla ogólnych przypadków. |
| Duże dane, gdy stabilność jest ważna | Merge, Radix, Counting | Zachowują kolejność elementów równych. |
| Liczby całkowite z małego zakresu | Counting, Radix | Nieporównawcze — liniowy czas. |
| Liczby rzeczywiste w zakresie [0,1) | Bucket | Równomierny rozkład = niemal liniowy czas. |
| Dane o nieznanym rozkładzie | Quick, Heap, Shell | Dobre wyniki bez wcześniejszej wiedzy o danych. |
| Cele dydaktyczne / żart | Bogo | Pokazuje, jak NIE sortować danych |
8. Szybkie wnioski
- Najbardziej uniwersalne: Quick Sort, Merge Sort, Heap Sort
- Najlepsze dla małych danych: Insertion Sort
- Najlepsze dla danych numerycznych z ograniczonym zakresem: Counting / Radix / Bucket
- Najgorszy w praktyce: Bogo Sort (czysto pokazowy)