Sắp xếp vun đống (heap sort)
Cập nhật lần cuối
Sắp xếp vun đống (heap sort) coi mảng như một binary heap. Đầu tiên, thuật toán dựng một max-heap để phần tử lớn nhất nằm ở gốc (chỉ số 0). Sau đó nó liên tục đổi chỗ gốc với phần tử cuối cùng chưa được sắp xếp, cố định giá trị lớn nhất vào đúng chỗ, rồi đẩy gốc mới xuống (sift down) để khôi phục tính chất heap. Nhấn Phát ở trên để xem quá trình dựng heap và các lần lấy phần tử ra.
Heap sort bảo đảm thời gian O(n log n) như merge sort, nhưng sắp xếp tại chỗ với chỉ O(1) bộ nhớ phụ. Thuật toán không ổn định và thường tận dụng bộ nhớ đệm kém hơn quicksort, nên hay được chọn khi cần cả cận thời gian được bảo đảm lẫn bộ nhớ cố định.
Độ phức tạp thời gian và không gian
| Trường hợp | Độ phức tạp | Ghi chú |
|---|---|---|
| Tốt nhất | O(n log n) | Dựng heap + n lần lấy ra |
| Trung bình | O(n log n) | Thứ tự ngẫu nhiên |
| Xấu nhất | O(n log n) | Được bảo đảm |
| Không gian | O(1) | Sắp xếp tại chỗ |
| Ổn định | Không | Sift down làm đảo thứ tự các phần tử bằng nhau |
Từng bước thực hiện
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Dựng max-heap từ mảng (sift down bắt đầu từ nút cha cuối cùng). |
| 2 | Đổi chỗ gốc (giá trị lớn nhất) với phần tử cuối cùng của heap. |
| 3 | Thu nhỏ heap đi một phần tử: ô cuối đó giờ đã được sắp xếp. |
| 4 | Sift down gốc mới để khôi phục tính chất max-heap. |
| 5 | Lặp lại cho đến khi heap chỉ còn một phần tử. |
Ví dụ minh họa
Sắp xếp [3, 1, 6, 5, 2, 4]. Dấu | đánh dấu ranh giới giữa heap đang thu nhỏ và phần đuôi đã sắp xếp:
| Lượt | Mảng | Thao tác |
|---|---|---|
| Dựng heap | [6, 5, 4, 1, 2, 3] | Sift down từ nút cha cuối cùng để dựng max-heap; 6 giờ nằm ở gốc. |
| 1 | [5, 3, 4, 1, 2 | 6] | Đổi gốc 6 với ô cuối, thu nhỏ heap và sift down 3. |
| 2 | [4, 3, 2, 1 | 5, 6] | Đổi gốc 5 ra, rồi sift down 2 để 4 lên gốc. |
| 3 | [3, 1, 2 | 4, 5, 6] | Đổi gốc 4 ra, rồi sift down 1 để 3 lên gốc. |
| 4 | [2, 1 | 3, 4, 5, 6] | Đổi gốc 3 ra; 2 đã thỏa mãn tính chất heap. |
| 5 | [1 | 2, 3, 4, 5, 6] | Đổi gốc 2 ra; chỉ còn một phần tử, nên mảng đã được sắp xếp. |
Khi nào nên dùng heap sort
| Nên dùng khi | Tránh dùng khi |
|---|---|
Bạn cần bảo đảm trường hợp xấu nhất O(n log n), không có rủi ro O(n²). | Bạn cần một thuật toán sắp xếp ổn định, giữ nguyên thứ tự các khóa bằng nhau. |
Bộ nhớ eo hẹp: thuật toán sắp xếp tại chỗ với chỉ O(1) bộ nhớ phụ. | Hiệu năng bộ nhớ đệm quan trọng và dữ liệu vừa trong bộ nhớ: quicksort thường nhanh hơn. |
| Bạn vốn đang duy trì một heap (ví dụ hàng đợi ưu tiên) trên dữ liệu. | Bạn muốn ít phép so sánh nhất: merge sort và quicksort thường so sánh ít hơn trong thực tế. |
| Dữ liệu đầu vào không đáng tin có thể kích hoạt trường hợp xấu nhất của quicksort và bạn không thể ngẫu nhiên hóa. | Dữ liệu gần như đã sắp xếp: sắp xếp chèn chạy trong thời gian gần tuyến tính với loại dữ liệu này. |
Code Heap Sort
Một cách cài đặt Heap Sort gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Heap Sort bằng 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)Code Heap Sort bằng 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]));Code Heap Sort bằng 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}Code Heap Sort bằng 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}Code Heap Sort bằng 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}Câu hỏi thường gặp về heap sort
Độ phức tạp thời gian của heap sort là bao nhiêu?
O(n log n) trong trường hợp tốt nhất, trung bình và xấu nhất. Dựng heap tốn O(n) và mỗi lần trong n lần lấy ra tốn O(log n). Thuật toán dùng thêm O(1) bộ nhớ.Heap sort có ổn định không?
Không. Thao tác sift down có thể đưa các phần tử bằng nhau vượt qua nhau, nên heap sort không giữ nguyên thứ tự tương đối của các khóa bằng nhau.
Khi nào nên dùng heap sort?
O(n log n) mà chỉ tốn thêm O(1) bộ nhớ. Thuật toán tránh được rủi ro O(n²) của quicksort mà không cần bộ đệm O(n) như merge sort, với cái giá là mất tính ổn định và hiệu năng bộ nhớ đệm kém hơn.Heap sort và quicksort khác nhau như thế nào?
O(n²) còn heap sort bảo đảm O(n log n). Trong thực tế quicksort thường nhanh hơn nhờ tính cục bộ bộ nhớ đệm tốt hơn và ít lần đổi chỗ hơn, nên heap sort chủ yếu được ưu tiên khi bắt buộc phải bảo đảm cận trong trường hợp xấu nhất.Heap sort liên quan thế nào đến hàng đợi ưu tiên?
Binary heap là cách cài đặt chuẩn của hàng đợi ưu tiên, và heap sort về bản chất là liên tục lấy phần tử lớn nhất ra khỏi hàng đợi đó. Nếu dữ liệu của bạn vốn đã nằm trong một heap, việc lấy ra từng phần tử sẽ tự động cho bạn thứ tự đã sắp xếp.
Heap sort cần max-heap hay min-heap?
Để sắp xếp tăng dần tại chỗ, hãy dùng max-heap: phần tử lớn nhất được đổi về cuối sau mỗi lượt, làm phần đuôi đã sắp xếp lớn dần từ bên phải. Min-heap sẽ cho thứ tự giảm dần nếu sắp xếp tại chỗ, hoặc tăng dần nếu bạn lấy các phần tử ra một mảng riêng.