1. Khái niệm
Sắp xếp vun đống hay Heap Sort là một thuật toán sắp xếp phổ biến và hiệu quả trong lập trình. Học cách viết thuật toán sắp xếp vun đống đòi hỏi kiến thức về hai loại cấu trúc dữ liệu là mảng và cây.
Tập hợp số ban đầu mà chúng ta muốn sắp xếp được lưu trữ trong một mảng, ví dụ, [12, 5, 78, 36, 25, 34] và sau khi sắp xếp theo thứ tự giảm dần, chúng ta nhận được một mảng đã sắp xếp là [78, 36, 34, 25, 12, 5]
Sắp xếp vun đống hoạt động bằng cách coi các phần tử của mảng như một loại cây nhị phân hoàn chỉnh đặc biệt được gọi là Heap. Điều kiện tiên quyết là bạn phải biết về cấu trúc dữ liệu Heap và cây nhị phân hoàn chỉnh.
2. Mối quan hệ giữa chỉ số của mảng và phần tử của cây
Một cây nhị phân hoàn chỉnh có một đặc tính mà chúng ta có thể sử dụng để tìm nút con và nút cha của bất kỳ nút nào.
Nếu chỉ số của bất kỳ phần tử nào trong mảng là i, phần tử trong chỉ số 2i+1 sẽ trở thành nút con bên trái và phần tử trong chỉ số 2i+2 sẽ trở thành nút con bên phải. Ngoài ra, nút cha của bất kỳ phần tử nào tại chỉ số i được cho bởi giới hạn dưới là (i-1)/2.
3. Cấu trúc dữ liệu Heap là gì?
Heap là một cấu trúc dữ liệu đặc biệt dựa trên cấu trúc cây. Cây nhị phân được cho là tuân theo cấu trúc dữ liệu đống nếu
Nó là một cây nhị phân hoàn chỉnh.
Tất cả các nút trong cây tuân theo thuộc tính mà chúng lớn hơn phần tử con của chúng, tức là phần tử lớn nhất nằm ở nút gốc và các phần tử con của nó nhỏ hơn nút gốc,…. Một Heap như vậy được gọi là Max heap. Nếu thay vào đó, tất cả các nút đều nhỏ hơn nút con của chúng, nó được gọi là Min Heap.
Hình bên dưới đây sẽ minh họa về cấu trúc dữ liệu Max Heap và Min Heap.
4. Sắp xếp vun đống hoạt động như nào?
Vì cây thỏa mãn thuộc tính Min Heap, nên phần tử nhỏ nhất được lưu trữ tại nút gốc.
Hoán đổi: Loại bỏ phần tử gốc và đặt ở cuối mảng (vị trí thứ n). Đặt phần tử cuối cùng của cây (đống) vào chỗ trống.
Xóa: Giảm kích thước của Heap đi 1 đơn vị.
Tạo cấu trúc Heap cho phần tử gốc để chúng ta có phần tử bé nhất ở nút gốc.
Quá trình này được lặp lại cho đến khi tất cả các phần tử của danh sách được sắp xếp.
Ví dụ : Sắp xếp mảng a = [3, 14, 11, 7, 8, 12] theo thứ tự giảm dần:
Cấu trúc Min heap :
- Mảng a = [ 3, 7, 11, 12, 8, 14]
- Hoán vị a[0] với a[6] ta có a = [14, 7, 11, 12, 8, 3], giảm kích thước heap đi 1 đơn vị.
- Hoán vị a[0] với a[1] ta có a = [7, 14, 11, 12, 8, 3]
- Hoán vị a[0] vơi a[5] ta có a = [8, 14 , 11, 12, 7, 3], giảm kích thước heap đi 1 đơn vị.
- Hoán vị a[0] với a[4] ta có a = [12, 14, 11, 8, 7, 3], giảm kích thước heap đi 1 đơn vị.
- Hoán vị a[0] với a[2] ta có a = [11, 14, 12, 8, 7, 3]
- Hoán vị a[0] với a[2] ta có a= [12, 14, 11, 8, 7, 3], giảm kích thước heap đi 1 đươn vị.
- Hoán vị a[0] với a[1] ta có kết quả sau khi sắp xếp a = [14, 12, 11, 8, 7, 3]
7 11
12 8 3
14
12 8
8
14
12 7
12
14 11
8
11 12 14
1 7
4
for (int i = n - 1; i >= 0; i--) { swap(&arr[0], &arr[i]);
//Tạo cấu trúc heap cho phần tử gốc để lấy ra phần tử bé nhất heapify(arr, i, 0);
}
#include <stdio.h>
void swap(int *a, int *b) { int c = *a;
*a = *b;
*b = c;
} void heapify(int arr[], int n, int i) { int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) largest = left;
if (right < n && arr[right] < arr[largest]) largest = right;
if (largest != i) { swap(&arr[i], &arr[largest]);
heapify(arr, n, largest);
} }
void sort_heap(int arr[], int n) { for (int i = n / 2 - 1; i >= 0; i--) heapify(arr, n, i);
for (int i = n - 1; i >= 0; i--) { swap(&arr[0], &arr[i]);
heapify(arr, i, 0);
} } void print(int arr[], int n) { for (int i = 0; i < n; ++i) printf("%d ", arr[i]);
printf("\n");
} int main() {
int n = sizeof(arr) / sizeof(arr[0]);
sort_heap(arr, n);
printf("Mảng sau khi sắp xếp là: \n");
print(arr, n);
} Kết quả:
Mảng sau khi sắp xếp là:
14 12 11 8 7 3
7. Độ phức tạp của thuật toán sắp xếp vun đống
Heap Sort có độ phức tạp về thời gian là O(nlogn) cho tất cả các trường hợp (trường hợp tốt nhất, trường hợp trung bình và trường hợp xấu nhất).
Chiều cao của một cây nhị phân hoàn chỉnh chứa n phần tử là logn
Như chúng ta đã thấy trước đó, để tạo cấu trúc Heap cho một phần tử có các cây con đã là Max Heap, chúng ta cần tiếp tục so sánh phần tử với các phần tử con bên trái và bên phải của nó và đẩy nó xuống dưới cho đến khi nó đạt đến điểm mà cả hai cây con của nó đều nhỏ hơn nó.
Trong trường hợp xấu nhất, chúng ta sẽ cần phải di chuyển một phần tử từ nút gốc đến nút lá để thực hiện nhiều phép so sánh và hoán đổi log(n).
Trong giai đoạn xây dựng cấu trúc Max Heap, chúng ta đã thực hiện điều đó cho n2 phần tử nên độ phức tạp trong trường hợp xấu nhất của bước này là n2×log(n)≈nlog(n).
Trong bước sắp xếp, chúng ta hoán đổi phần tử gốc với phần tử cuối cùng và tạo cấu trúc Heap cho phần tử gốc. Đối với mỗi phần tử, điều này lại làm tốn thời gian là log(n) vì chúng ta có thể phải đưa phần tử đó từ nút gốc đến nút lá. Vì chúng ta lặp lại n lần này nên bước sắp xếp vun đống cũng là nlog(n).
Cũng vì các bước xây dựng cấu trúc Max Heap và sắp xếp vun đống được thực hiện lần lượt nên độ phức tạp của thuật toán không được nhân lên và nó vẫn theo thứ tự nlog(n).
Ngoài ra, nó thực hiện sắp xếp theo độ phức tạp về không gian là O(1). So với Sắp xếp nhanh, nó có trường hợp xấu nhất tốt hơn là (O(nlogn)). Sắp xếp nhanh có độ phức tạp O(n2) cho trường hợp xấu nhất. Nhưng trong các trường hợp khác, thuật toán sắp xếp nhanh hay QuickSort sẽ nhanh hơn.
8. Ứng dụng của thuật toán sắp xếp vun đống
Các hệ thống liên quan đến bảo mật và các hệ thống nhúng như nhân Linux sử dụng thuật toán sắp xếp vun đống vì giới hạn trên là O(nlogn) cho thời gian chạy của HeapSort và giới hạn trên không đổi là O(1) cho bộ nhớ phụ của nó.
Mặc dù HeapSort có độ phức tạp về thời gian là O(nlogn) ngay cả trong trường hợp xấu nhất, nó không có nhiều ứng dụng (so với các thuật toán sắp xếp khác như QuickSort, MergeSort). Tuy nhiên, cấu trúc dữ liệu cơ bản của nó, Heap, có thể được sử dụng hiệu quả nếu chúng ta muốn trích xuất phần nhỏ nhất (hoặc lớn nhất) từ danh sách