Heap sort
Sau bài này bạn sẽ làm được
- Cài thao tác đẩy xuống cho đúng
- Dựng max heap trong O(n)
- Cài heap sort hoàn chỉnh
- Giải thích vì sao nó chậm hơn quicksort dù cùng O(n log n)
Heap sort là thuật toán duy nhất vừa bảo đảm O(n log n) trong mọi trường hợp, vừa sắp hoàn toàn tại chỗ. Đổi lại nó chậm hơn quick sort khoảng hai tới ba lần trong thực tế, và lý do rất đáng hiểu.
#Dựng heap rồi rút gốc
n - 1 lần, mảng đã sắp tăng dần./* Hai giai đoạn:
Giai đoạn 1: dựng max heap từ mảng bất kỳ. O(n)
Giai đoạn 2: rút gốc n-1 lần. O(n log n)
Tổng: O(n log n) trong MỌI trường hợp,
và KHÔNG dùng byte bộ nhớ phụ nào.
Dùng MAX heap chứ không phải min heap, vì ta muốn
phần tử lớn nhất đi về CUỐI mảng. */| Merge sort | Quick sort | Heap sort | |
|---|---|---|---|
| Tốt nhất | O(n log n) | O(n log n) | O(n log n) |
| Trung bình | O(n log n) | O(n log n) | O(n log n) |
| Xấu nhất | O(n log n) | O(n bình phương) | O(n log n) |
| Bộ nhớ phụ | O(n) | O(log n) | O(1) |
| Ổn định | Có | Không | Không |
| Tốc độ thực tế | Chuẩn | Nhanh nhất | Chậm nhất trong ba |
#Hàm đẩy xuống
#include <stddef.h>
static void doi_cho(int *a, int *b) { int t = *a; *a = *b; *b = t; }
/* Đẩy phần tử ở vị trí i xuống cho tới khi nó lớn hơn hoặc bằng cả hai con.
Chỉ xét vùng a[0..n), tức n là kích thước heap HIỆN TẠI.
Chạy nhiều nhất log2(n) bước. */
static void day_xuong(int *a, size_t i, size_t n)
{
for (;;) {
size_t trai = 2 * i + 1;
size_t phai = 2 * i + 2;
size_t lon = i;
if (trai < n && a[trai] > a[lon]) lon = trai;
if (phai < n && a[phai] > a[lon]) lon = phai;
if (lon == i) break; /* đã đúng chỗ */
doi_cho(&a[i], &a[lon]);
i = lon;
}
}Bản dùng dịch thay vì đổi chỗ
/* Mỗi phép đổi chỗ tốn 3 phép gán. Dịch thì chỉ tốn 1. */
static void day_xuong_dich(int *a, size_t i, size_t n)
{
int khoa = a[i];
size_t con;
while ((con = 2 * i + 1) < n) {
if (con + 1 < n && a[con + 1] > a[con]) ++con; /* con lớn hơn */
if (a[con] <= khoa) break;
a[i] = a[con]; /* dịch con lên, chưa ghi khoa */
i = con;
}
a[i] = khoa; /* ghi khoa vào chỗ cuối cùng */
}doi cho : 0.618 s dich : 0.442 s nhanh hon: 1.40 lan
#Dựng heap tại chỗ trong O(n)
/* Giai đoạn 1: biến mảng thành max heap.
Đi từ nút không phải lá cuối cùng lên gốc.
Chi phí O(n), không phải O(n log n). Bài 23.4 đã chứng minh. */
static void dung_heap(int *a, size_t n)
{
for (size_t i = n / 2; i-- > 0; )
day_xuong(a, i, n);
}Vết dựng heap
| i | Mảng trước | Việc làm | Mảng sau |
|---|---|---|---|
| - | 9 4 7 1 8 20 3 | bắt đầu, n/2 = 3 nên i chạy 2, 1, 0 | 9 4 7 1 8 20 3 |
| 2 | 9 4 7 1 8 20 3 | a[2]=7, con lớn hơn là a[5]=20, đổi | 9 4 20 1 8 7 3 |
| 1 | 9 4 20 1 8 7 3 | a[1]=4, con lớn hơn là a[4]=8, đổi | 9 8 20 1 4 7 3 |
| 0 | 9 8 20 1 4 7 3 | a[0]=9, con lớn hơn là a[2]=20, đổi | 20 8 9 1 4 7 3 |
Kết quả 20 8 9 1 4 7 3 là max heap hợp lệ: 20 lớn hơn 8 và 9, 8 lớn hơn 1 và 4, 9 lớn hơn 7 và 3. Nó không phải dãy sắp xếp, và không cần phải vậy.
#Rút gốc n trừ một lần
void heap_sort(int *a, size_t n)
{
if (n < 2) return;
dung_heap(a, n); /* giai đoạn 1: O(n) */
for (size_t i = n; i-- > 1; ) { /* giai đoạn 2: O(n log n) */
doi_cho(&a[0], &a[i]); /* lớn nhất về cuối vùng heap */
day_xuong(a, 0, i); /* vùng heap thu nhỏ còn i */
}
}Gốc là phần tử lớn nhất
Theo bất biến max heap. Nó thuộc về vị trí cuối cùng của vùng chưa sắp.
Đổi chỗ gốc với phần tử cuối vùng heap
Phần tử lớn nhất về đúng chỗ vĩnh viễn. Phần tử vừa lên gốc gần như chắc chắn sai chỗ.
Thu nhỏ vùng heap một ô
Vị trí vừa được điền không còn thuộc heap nữa. Đó chính là ý nghĩa của tham số
itruyền choday_xuong.Đẩy gốc mới xuống
Khôi phục bất biến max heap cho vùng còn lại. Tốn nhiều nhất
log2(i)bước.
Vết giai đoạn 2
| i | Mảng trước | Sau khi đổi chỗ | Sau khi đẩy xuống |
|---|---|---|---|
| 6 | 20 8 9 1 4 7 3 | 3 8 9 1 4 7 | 20 | 9 8 7 1 4 3 | 20 |
| 5 | 9 8 7 1 4 3 | 20 | 3 8 7 1 4 | 9 20 | 8 4 7 1 3 | 9 20 |
| 4 | 8 4 7 1 3 | 9 20 | 3 4 7 1 | 8 9 20 | 7 4 3 1 | 8 9 20 |
| 3 | 7 4 3 1 | 8 9 20 | 1 4 3 | 7 8 9 20 | 4 1 3 | 7 8 9 20 |
| 2 | 4 1 3 | 7 8 9 20 | 3 1 | 4 7 8 9 20 | 3 1 | 4 7 8 9 20 |
| 1 | 3 1 | 4 7 8 9 20 | 1 | 3 4 7 8 9 20 | 1 3 4 7 8 9 20 |
#include <stdio.h>
void heap_sort(int *a, size_t n);
int main(void)
{
int a[] = { 9, 4, 7, 1, 8, 20, 3 };
size_t n = sizeof a / sizeof a[0];
printf("truoc: ");
for (size_t i = 0; i < n; ++i) printf("%d ", a[i]);
heap_sort(a, n);
printf("\nsau : ");
for (size_t i = 0; i < n; ++i) printf("%d ", a[i]);
printf("\n");
return 0;
}truoc: 9 4 7 1 8 20 3 sau : 1 3 4 7 8 9 20
#Vì sao nó chậm hơn quick sort
10000000 phan tu ngau nhien:
thoi gian so sanh hoan doi
quick sort 1.62 s 238412047 79470682
merge sort 2.84 s 220352104 0
heap sort 4.12 s 478241093 239120546
heap sort cham hon quick sort 2.54 lan| Nguyên nhân | Giải thích |
|---|---|
| Truy cập bộ nhớ nhảy xa | Từ nút i xuống con 2i+1 là nhảy gấp đôi khoảng cách. Với heap lớn, mỗi bước là một lần trượt bộ nhớ đệm. |
| Số phép so sánh gấp đôi | Mỗi bước đẩy xuống cần hai phép so sánh: tìm con lớn hơn, rồi so với cha. |
| Nhánh khó dự đoán | Con nào lớn hơn là gần như ngẫu nhiên, nên bộ dự đoán nhánh sai khoảng một nửa số lần. |
| Số phép ghi gấp ba | Mỗi lần rút gốc gây ra một chuỗi đổi chỗ dài log n. |
#Khi nào nên dùng
/* k phần tử lớn nhất, đã sắp, nằm ở cuối mảng.
O(n) dựng heap, cộng O(k log n) rút gốc. */
void k_lon_nhat(int *a, size_t n, size_t k)
{
if (n < 2 || k == 0) return;
if (k > n) k = n;
dung_heap(a, n);
for (size_t i = n; i-- > n - k; ) {
doi_cho(&a[0], &a[i]);
day_xuong(a, 0, i);
}
/* a[n-k..n) là k phần tử lớn nhất, đã sắp tăng dần. */
}n = 10000000
k heap cat ngan sap ca mang
1 0.094 s 1.620 s
10 0.095 s 1.620 s
100 0.098 s 1.620 s
1000 0.112 s 1.620 s
100000 0.284 s 1.620 sTự làm thử
- Cài
day_xuongvàheap_sort, kiểm vết dựng heap khớp với bảng trong bài. - Truyền nhầm kích thước mảng thay vì kích thước heap cho
day_xuong, rồi giải thích kết quả nhận được. - Cài bản
day_xuong_dichvà đo chênh lệch so với bản đổi chỗ trên năm triệu phần tử. - Đo thời gian quick sort và heap sort với
ntừ một nghìn tới mười triệu, rồi giải thích vì sao tỷ lệ tăng dần. - Cài
k_lon_nhatbằng heap sort cắt ngắn và so với heap kích thướckở Bài 23.4, vớikbằng 10, 1000 và 100 nghìn.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Heap sort dựng max heap trong O(n) rồi rút gốc
n - 1lần, tổng O(n log n) trong mọi trường hợp. - Tham số
ncủaday_xuonglà kích thước heap hiện tại, không phải kích thước mảng. Đó là thứ giữ cho vùng đã sắp không bị đụng vào. - Nó là thuật toán duy nhất vừa bảo đảm O(n log n) vừa sắp tại chỗ với O(1) bộ nhớ phụ.
- Nó chậm hơn quick sort hai tới ba lần, chủ yếu vì truy cập nhảy xa làm trượt bộ nhớ đệm gấp mười ba lần.
- Công dụng phổ biến nhất của nó là làm lưới an toàn trong introsort, chứ không phải làm thuật toán sắp xếp chính.