Heap sort (sortowanie przez kopcowanie)
Ostatnia aktualizacja
Sortowanie przez kopcowanie traktuje tablicę jak kopiec binarny. Najpierw buduje kopiec max, więc największy element trafia do korzenia (indeks 0). Potem wielokrotnie zamienia korzeń z ostatnim nieposortowanym elementem, co ustala maksimum na jego miejscu, i przesiewa nowy korzeń w dół, aby przywrócić własność kopca. Kliknij odtwarzanie powyżej i zobacz budowę kopca i kolejne wyciągnięcia.
Heap sort gwarantuje czas O(n log n) jak merge sort, ale sortuje w miejscu, zużywając tylko O(1) dodatkowej pamięci. Nie jest stabilny i zwykle gorzej współpracuje z pamięcią podręczną niż quicksort, dlatego wybiera się go często wtedy, gdy liczą się zarówno gwarantowane ograniczenie, jak i stała pamięć.
Złożoność czasowa i pamięciowa
| Przypadek | Złożoność | Uwagi |
|---|---|---|
| Najlepszy przypadek | O(n log n) | Budowa + n wyciągnięć |
| Średni przypadek | O(n log n) | Losowa kolejność |
| Najgorszy przypadek | O(n log n) | Gwarantowany |
| Pamięć | O(1) | W miejscu |
| Stabilne | Nie | Przesiewanie w dół zmienia kolejność równych elementów |
Krok po kroku
| Krok | Co się dzieje |
|---|---|
| 1 | Zbuduj kopiec max z tablicy (przesiewając w dół od ostatniego rodzica). |
| 2 | Zamień korzeń (maksimum) z ostatnim elementem kopca. |
| 3 | Zmniejsz kopiec o jeden: ostatnie pole jest już posortowane. |
| 4 | Przesiej nowy korzeń w dół, aby przywrócić własność kopca max. |
| 5 | Powtarzaj, aż w kopcu zostanie jeden element. |
Przykład krok po kroku
Sortowanie [3, 1, 6, 5, 2, 4]. Kreska | oznacza granicę między kurczącym się kopcem a posortowanym ogonem:
| Przebieg | Tablica | Działanie |
|---|---|---|
| Budowa kopca | [6, 5, 4, 1, 2, 3] | Przesiewaj w dół od ostatniego rodzica, aby zbudować kopiec max; 6 jest teraz w korzeniu. |
| 1 | [5, 3, 4, 1, 2 | 6] | Zamień korzeń 6 z ostatnim polem, zmniejsz kopiec i przesiej 3 w dół. |
| 2 | [4, 3, 2, 1 | 5, 6] | Wyjmij korzeń 5, potem przesiej 2 w dół, aby 4 awansowało do korzenia. |
| 3 | [3, 1, 2 | 4, 5, 6] | Wyjmij korzeń 4, potem przesiej 1 w dół, aby 3 awansowało do korzenia. |
| 4 | [2, 1 | 3, 4, 5, 6] | Wyjmij korzeń 3; 2 już spełnia własność kopca. |
| 5 | [1 | 2, 3, 4, 5, 6] | Wyjmij korzeń 2; został jeden element, więc tablica jest posortowana. |
Kiedy używać sortowania przez kopcowanie
| Używaj, gdy | Unikaj, gdy |
|---|---|
Potrzebujesz gwarantowanego O(n log n) w najgorszym przypadku, bez ryzyka O(n²). | Potrzebujesz stabilnego sortowania, które zachowuje kolejność równych kluczy. |
Pamięci jest mało: sortuje w miejscu, zużywając tylko O(1) dodatkowej pamięci. | Liczy się wydajność pamięci podręcznej, a dane mieszczą się w pamięci: quicksort jest zwykle szybszy. |
| Już utrzymujesz kopiec (np. kolejkę priorytetową) na tych danych. | Chcesz jak najmniej porównań: merge sort i quicksort w praktyce często wykonują ich mniej. |
| Niezaufane dane wejściowe mogłyby wywołać najgorszy przypadek quicksorta, a nie możesz losować. | Dane są prawie posortowane: sortowanie przez wstawianie działa na nich w czasie bliskim liniowemu. |
Heap Sort: kod
Przejrzysta, gotowa do uruchomienia implementacja algorytmu Heap Sort w językach: Python, JavaScript, Java, C++, C. Wybierz język, skopiuj kod albo otwórz go od razu w edytorze online Coddy.
Heap Sort: kod (Python)
1def heap_sort(a):2 n = len(a)3 # Build a max-heap, deepest parent first4 for i in range(n // 2 - 1, -1, -1):5 sift_down(a, i, n)6 # Repeatedly move the max to the end and shrink the heap7 for end in range(n - 1, 0, -1):8 a[0], a[end] = a[end], a[0]9 sift_down(a, 0, end)10 return a11
12
13def sift_down(a, i, size):14 while True:15 largest = i16 left, right = 2 * i + 1, 2 * i + 217 if left < size and a[left] > a[largest]:18 largest = left19 if right < size and a[right] > a[largest]:20 largest = right21 if largest == i:22 return23 a[i], a[largest] = a[largest], a[i]24 i = largest25
26
27nums = [12, 11, 13, 5, 6, 7]28print("Before:", nums)29heap_sort(nums)30print("After: ", nums)Heap Sort: kod (JavaScript)
1function heapSort(a) {2 const n = a.length;3 // Build a max-heap, then repeatedly move the max to the end4 for (let i = Math.floor(n / 2) - 1; i >= 0; i--) siftDown(a, i, n);5 for (let end = n - 1; end > 0; end--) {6 [a[0], a[end]] = [a[end], a[0]];7 siftDown(a, 0, end);8 }9 return a;10}11
12function siftDown(a, i, size) {13 while (true) {14 const left = 2 * i + 1;15 const right = 2 * i + 2;16 let largest = i;17 if (left < size && a[left] > a[largest]) largest = left;18 if (right < size && a[right] > a[largest]) largest = right;19 if (largest === i) return;20 [a[i], a[largest]] = [a[largest], a[i]];21 i = largest;22 }23}24
25const data = [5, 2, 9, 1, 7, 3];26console.log("Before:", data);27console.log("Sorted:", heapSort([...data]));Heap Sort: kod (Java)
1import java.util.Arrays;2
3public class Main {4 static void heapSort(int[] arr) {5 int n = arr.length;6 // Build a max-heap, deepest parent first7 for (int i = n / 2 - 1; i >= 0; i--) siftDown(arr, i, n);8 // Repeatedly move the max to the end and shrink the heap9 for (int end = n - 1; end > 0; end--) {10 swap(arr, 0, end);11 siftDown(arr, 0, end);12 }13 }14
15 static void siftDown(int[] arr, int i, int size) {16 while (true) {17 int largest = i, l = 2 * i + 1, r = 2 * i + 2;18 if (l < size && arr[l] > arr[largest]) largest = l;19 if (r < size && arr[r] > arr[largest]) largest = r;20 if (largest == i) return;21 swap(arr, i, largest);22 i = largest;23 }24 }25
26 static void swap(int[] arr, int a, int b) {27 int tmp = arr[a];28 arr[a] = arr[b];29 arr[b] = tmp;30 }31
32 public static void main(String[] args) {33 int[] arr = {12, 11, 13, 5, 6, 7};34 System.out.println("Before: " + Arrays.toString(arr));35 heapSort(arr);36 System.out.println("After: " + Arrays.toString(arr));37 }38}Heap Sort: kod (C++)
1#include <iostream>2#include <utility>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 siftDown(std::vector<int>& a, int n, int i) {11 while (true) {12 int largest = i, l = 2 * i + 1, r = 2 * i + 2;13 if (l < n && a[l] > a[largest]) largest = l;14 if (r < n && a[r] > a[largest]) largest = r;15 if (largest == i) return;16 std::swap(a[i], a[largest]);17 i = largest;18 }19}20
21void heapSort(std::vector<int>& a) {22 int n = static_cast<int>(a.size());23 // Build a max-heap in place24 for (int i = n / 2 - 1; i >= 0; --i) siftDown(a, n, i);25 // Repeatedly move the max to the end and shrink the heap26 for (int end = n - 1; end > 0; --end) {27 std::swap(a[0], a[end]);28 siftDown(a, end, 0);29 }30}31
32int main() {33 std::vector<int> data = {12, 11, 13, 5, 6, 7};34 std::cout << "Before: ";35 printVec(data);36 heapSort(data);37 std::cout << "After: ";38 printVec(data);39 return 0;40}Heap Sort: kod (C)
1#include <stdio.h>2
3void printArr(const int a[], int n) {4 for (int i = 0; i < n; i++) printf("%d ", a[i]);5 printf("\n");6}7
8void siftDown(int a[], int n, int i) {9 while (1) {10 int largest = i, l = 2 * i + 1, r = 2 * i + 2;11 if (l < n && a[l] > a[largest]) largest = l;12 if (r < n && a[r] > a[largest]) largest = r;13 if (largest == i) return;14 int tmp = a[i];15 a[i] = a[largest];16 a[largest] = tmp;17 i = largest;18 }19}20
21void heapSort(int a[], int n) {22 // Build a max-heap in place23 for (int i = n / 2 - 1; i >= 0; i--) siftDown(a, n, i);24 // Repeatedly move the max to the end and shrink the heap25 for (int end = n - 1; end > 0; end--) {26 int tmp = a[0];27 a[0] = a[end];28 a[end] = tmp;29 siftDown(a, end, 0);30 }31}32
33int main(void) {34 int data[] = {12, 11, 13, 5, 6, 7};35 int n = sizeof(data) / sizeof(data[0]);36 printf("Before: ");37 printArr(data, n);38 heapSort(data, n);39 printf("After: ");40 printArr(data, n);41 return 0;42}Heap sort: najczęstsze pytania
Jaka jest złożoność czasowa sortowania przez kopcowanie?
O(n log n) w najlepszym, średnim i najgorszym przypadku. Budowa kopca kosztuje O(n), a każde z n wyciągnięć kosztuje O(log n). Zużywa O(1) dodatkowej pamięci.Czy sortowanie przez kopcowanie jest stabilne?
Kiedy używać sortowania przez kopcowanie?
O(n log n) w najgorszym przypadku przy zaledwie O(1) dodatkowej pamięci. Unika ryzyka O(n²) quicksorta bez bufora O(n) potrzebnego w merge sort, kosztem stabilności i wydajności pamięci podręcznej.Czym różni się heap sort od quicksorta?
O(n²), a heap sort gwarantuje O(n log n). W praktyce quicksort jest zwykle szybszy dzięki lepszej lokalności pamięci podręcznej i mniejszej liczbie zamian, więc heap sort wybiera się głównie wtedy, gdy ograniczenie najgorszego przypadku musi być zagwarantowane.