Sortowanie przez zliczanie (counting sort)
Ostatnia aktualizacja
Sortowanie przez zliczanie to sortowanie bez porównań dla liczb całkowitych ze znanego, ograniczonego zakresu. Zlicza, ile razy pojawia się każda wartość, a potem na podstawie tych liczników zapisuje każdą wartość bezpośrednio na jej miejscu w posortowanym wyniku, bez żadnych porównań. Kliknij odtwarzanie powyżej i zobacz, jak wartości są zliczane, a potem odkładane z powrotem w kolejności.
Sortowanie przez zliczanie działa w czasie O(n + k), gdzie k to zakres wartości wejściowych. Gdy k nie jest dużo większe od n, algorytm jest niezwykle szybki i może pokonać sortowania przez porównania o złożoności O(n log n), ale przy ogromnym zakresie wartości tablica liczników O(k) czyni go niepraktycznym.
Złożoność czasowa i pamięciowa
| Przypadek | Złożoność | Uwagi |
|---|---|---|
| Czas | O(n + k) | n elementów, k = zakres wartości |
| Pamięć | O(n + k) | Tablica liczników + tablica wynikowa |
| Stabilne | Tak | Przy umieszczaniu od prawej do lewej z sumami prefiksowymi |
| Porównania? | Nie | Sortuje przez zliczanie, nie przez porównywanie |
| Najlepsze dla | Mały zakres liczb całkowitych | k bliskie n |
Krok po kroku
| Krok | Co się dzieje |
|---|---|
| 1 | Znajdź wartość maksymalną, aby ustalić rozmiar tablicy liczników. |
| 2 | Policz, ile razy występuje każda wartość. |
| 3 | (Opcjonalnie) Zamień liczniki na sumy prefiksowe, aby zapewnić stabilność. |
| 4 | Zapisz każdą wartość w wyniku tyle razy, ile się pojawia. |
| 5 | Tablica wynikowa jest teraz w pełni posortowana. |
Przykład krok po kroku
Sortowanie [1, 4, 1, 2, 4] (wartości od 0 do 4, więc tablica liczników ma 5 pól):
| Przebieg | Stan | Działanie |
|---|---|---|
| Skanowanie wejścia | count = [0, 2, 1, 0, 2] | Zlicz wystąpienia: 1 pojawia się dwa razy, 2 raz, 4 dwa razy. |
| Sumy prefiksowe | count = [0, 2, 3, 3, 5] | Każde pole zawiera teraz liczbę wartości <= jego indeksowi, co daje pozycje końcowe. |
Umieść 4 | output = [_, _, _, _, 4] | Czytaj od prawej do lewej: count[4] = 5, więc 4 trafia pod indeks 4; zmniejsz do 4. |
Umieść 2 | output = [_, _, 2, _, 4] | count[2] = 3, więc 2 trafia pod indeks 2; zmniejsz do 2. |
Umieść 1 | output = [_, 1, 2, _, 4] | count[1] = 2, więc 1 trafia pod indeks 1; zmniejsz do 1. |
Umieść 4 | output = [_, 1, 2, 4, 4] | count[4] = 4, więc ta 4 trafia pod indeks 3; zmniejsz do 3. |
Umieść 1 | output = [1, 1, 2, 4, 4] | count[1] = 1, więc ta 1 trafia pod indeks 0. Tablica jest posortowana. |
Kiedy używać sortowania przez zliczanie
| Używaj, gdy | Unikaj, gdy |
|---|---|
| Sortujesz liczby całkowite (lub klucze, które da się na nie odwzorować) z małego, znanego zakresu. | Zakres wartości k jest dużo większy niż liczba elementów n. |
Potrzebujesz liniowego czasu O(n + k) i stać cię na dodatkowe tablice. | Pamięci jest mało: tablica liczników kosztuje O(k) niezależnie od n. |
| Potrzebujesz stabilnego sortowania jako podprocedury (np. w radix sort). | Klucze to liczby zmiennoprzecinkowe, napisy lub dowolne obiekty bez odwzorowania na liczby całkowite. |
| Wartość maksymalna jest ograniczona i łatwo ją wcześniej obliczyć. | Zakres jest nieznany lub nieograniczony, więc nie da się ustalić rozmiaru tablicy liczników. |
Counting Sort: kod
Przejrzysta, gotowa do uruchomienia implementacja algorytmu Counting Sort w językach: Python, JavaScript, Java, C++, C. Wybierz język, skopiuj kod albo otwórz go od razu w edytorze online Coddy.
Counting Sort: kod (Python)
1def counting_sort(a):2 # Works for non-negative integers with a small max value3 counts = [0] * (max(a) + 1)4 for value in a:5 counts[value] += 16 # Prefix sums turn counts into final positions7 for i in range(1, len(counts)):8 counts[i] += counts[i - 1]9 out = [0] * len(a)10 for value in reversed(a): # reversed keeps equal values stable11 counts[value] -= 112 out[counts[value]] = value13 return out14
15
16nums = [4, 2, 2, 8, 3, 3, 1]17print("Before:", nums)18print("After: ", counting_sort(nums))Counting Sort: kod (JavaScript)
1function countingSort(arr) {2 // Count occurrences of each value, then rebuild in order3 const max = Math.max(...arr);4 const count = new Array(max + 1).fill(0);5 for (const x of arr) count[x]++;6 const out = [];7 count.forEach((c, value) => {8 for (let k = 0; k < c; k++) out.push(value);9 });10 return out;11}12
13const data = [4, 2, 9, 2, 7, 4, 1, 4];14console.log("Before:", data);15console.log("Sorted:", countingSort(data));Counting Sort: kod (Java)
1import java.util.Arrays;2
3public class Main {4 static int[] countingSort(int[] arr) {5 int max = 0;6 for (int v : arr) max = Math.max(max, v);7 int[] count = new int[max + 1];8 for (int v : arr) count[v]++;9 // Prefix sums turn counts into final positions10 for (int i = 1; i <= max; i++) count[i] += count[i - 1];11 int[] out = new int[arr.length];12 // Walk backwards so equal values keep their order (stable)13 for (int i = arr.length - 1; i >= 0; i--) {14 out[--count[arr[i]]] = arr[i];15 }16 return out;17 }18
19 public static void main(String[] args) {20 int[] arr = {4, 2, 2, 8, 3, 3, 1};21 System.out.println("Before: " + Arrays.toString(arr));22 int[] sorted = countingSort(arr);23 System.out.println("After: " + Arrays.toString(sorted));24 }25}Counting Sort: kod (C++)
1#include <algorithm>2#include <iostream>3#include <vector>4
5void printVec(const std::vector<int>& a) {6 for (int x : a) std::cout << x << " ";7 std::cout << "\n";8}9
10void countingSort(std::vector<int>& a) {11 if (a.empty()) return;12 int maxVal = *std::max_element(a.begin(), a.end());13 // count[v] = how many times v appears14 std::vector<int> count(maxVal + 1, 0);15 for (int x : a) ++count[x];16 // Rebuild the array from the counts17 size_t idx = 0;18 for (int v = 0; v <= maxVal; ++v) {19 while (count[v]-- > 0) a[idx++] = v;20 }21}22
23int main() {24 std::vector<int> data = {4, 2, 2, 8, 3, 3, 1, 7};25 std::cout << "Before: ";26 printVec(data);27 countingSort(data);28 std::cout << "After: ";29 printVec(data);30 return 0;31}Counting Sort: kod (C)
1#include <stdio.h>2#include <stdlib.h>3
4void printArr(const int a[], int n) {5 for (int i = 0; i < n; i++) printf("%d ", a[i]);6 printf("\n");7}8
9void countingSort(int a[], int n) {10 int maxVal = a[0];11 for (int i = 1; i < n; i++) {12 if (a[i] > maxVal) maxVal = a[i];13 }14 // count[v] = how many times v appears15 int* count = calloc(maxVal + 1, sizeof(int));16 for (int i = 0; i < n; i++) count[a[i]]++;17 // Rebuild the array from the counts18 int idx = 0;19 for (int v = 0; v <= maxVal; v++) {20 while (count[v]-- > 0) a[idx++] = v;21 }22 free(count);23}24
25int main(void) {26 int data[] = {4, 2, 2, 8, 3, 3, 1, 7};27 int n = sizeof(data) / sizeof(data[0]);28 printf("Before: ");29 printArr(data, n);30 countingSort(data, n);31 printf("After: ");32 printArr(data, n);33 return 0;34}Sortowanie przez zliczanie: najczęstsze pytania
Jaka jest złożoność czasowa sortowania przez zliczanie?
O(n + k), gdzie n to liczba elementów, a k to zakres możliwych wartości. Gdy k = O(n), jest to czas liniowy. Zużywa O(n + k) dodatkowej pamięci.Czy sortowanie przez zliczanie jest stabilne?
Kiedy używać sortowania przez zliczanie?
k jest dużo większy niż liczba elementów, tablica liczników marnuje pamięć i lepsze jest sortowanie przez porównania.Czym różni się sortowanie przez zliczanie od sortowania pozycyjnego (radix sort)?
10 dla cyfr dziesiętnych). Radix sort radzi sobie z dużymi zakresami wartości, przy których pojedyncze sortowanie przez zliczanie byłoby niepraktyczne.Dlaczego sortowanie przez zliczanie nie zawsze jest szybsze od quicksorta?
O(n + k), więc wygrywa tylko wtedy, gdy zakres wartości k jest porównywalny z n. Jeśli k jest ogromne, na przykład przy sortowaniu 100 wartości z zakresu od 0 do 1,000,000,000, dominuje tablica liczników O(k) i marnuje pamięć, a sortowanie przez porównania O(n log n), takie jak quicksort, pozostaje szybkie i oszczędne.Czy sortowanie przez zliczanie obsługuje liczby ujemne?
value - min, aby najmniejsza wartość trafiała pod indeks 0. Rozmiar tablicy liczników wynosi wtedy max - min + 1. Pominięcie tego przesunięcia to częsty błąd, który powoduje awarię przy ujemnych danych.