Heap (binary heap)
Cập nhật lần cuối
Binary heap là một cây nhị phân đầy đủ (complete binary tree) luôn giữ giá trị nhỏ nhất (min-heap) hoặc lớn nhất (max-heap) ở gốc. Mô phỏng này là một min-heap: mỗi nút cha luôn nhỏ hơn hoặc bằng các nút con. Để thêm một giá trị, bạn đặt nó vào vị trí trống tiếp theo, rồi "đẩy nó lên" (sift up), tức là đổi chỗ với nút cha chừng nào nó còn nhỏ hơn, cho đến khi tính chất heap được khôi phục. Nhấn Phát ở trên để xem từng giá trị mới nổi lên đúng vị trí.
Vì heap là cây đầy đủ, nó được lưu gọn trong một mảng: các con của nút i nằm ở 2i+1 và 2i+2. Chèn và lấy ra phần tử nhỏ nhất tốn O(log n) (một đường đi từ gốc đến lá), còn xem phần tử nhỏ nhất chỉ tốn O(1), đúng thứ mà một hàng đợi ưu tiên (priority queue) cần.
Độ phức tạp thời gian và không gian
| Thao tác | Độ phức tạp | Ghi chú |
|---|---|---|
| Xem min/max (peek) | O(1) | Luôn nằm ở gốc |
| Chèn (push) | O(log n) | Sift up trên một đường đi |
| Lấy ra min/max | O(log n) | Sift down trên một đường đi |
| Xây dựng heap | O(n) | Heapify một lần cho tất cả |
| Không gian | O(n) | Lưu bằng mảng, không cần con trỏ |
Từng bước (push)
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Thêm giá trị mới vào cuối (lá trống tiếp theo). |
| 2 | So sánh nó với nút cha. |
| 3 | Nếu nó nhỏ hơn (min-heap), đổi chỗ để đẩy nó lên. |
| 4 | Lặp lại cho đến khi nó không còn nhỏ hơn nút cha, hoặc đã lên tới gốc. |
Ví dụ minh họa
Xây dựng một min-heap bằng cách push lần lượt từng giá trị của [5, 3, 8, 1, 4]:
| Push | Mảng sau khi sift up | Thao tác |
|---|---|---|
5 | [5] | Giá trị đầu tiên trở thành gốc. |
3 | [3, 5] | 3 < cha 5, nên đổi chỗ để đưa nó lên gốc. |
8 | [3, 5, 8] | 8 > cha 5, nên nó ở lại làm lá. |
1 | [1, 3, 8, 5] | 1 < cha 5, đổi chỗ; rồi 1 < cha 3, đổi chỗ để lên gốc. |
4 | [1, 3, 8, 5, 4] | 4 > cha 3, nên nó ở yên; giá trị nhỏ nhất 1 vẫn ở gốc. |
Khi nào nên dùng heap
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Bạn cần lấy phần tử nhỏ nhất hoặc lớn nhất nhiều lần từ một tập dữ liệu luôn thay đổi. | Bạn cần tìm một giá trị bất kỳ, không chỉ giá trị cực trị: hãy dùng BST hoặc hash set. |
| Bạn đang cài đặt hàng đợi ưu tiên cho Dijkstra, A* hoặc một bộ lập lịch. | Bạn cần dữ liệu luôn được sắp xếp hoàn toàn ở mọi thời điểm. |
Bạn muốn chèn và lấy ra phần tử nhỏ nhất trong O(log n) với bố cục mảng gọn nhẹ. | Bạn cần tra cứu hoặc xóa nhanh một phần tử cụ thể (không phải gốc). |
| Bạn cần gộp một luồng dữ liệu và lấy chúng ra theo độ ưu tiên. | Tập dữ liệu rất nhỏ và duyệt tuyến tính vừa đơn giản hơn vừa đủ nhanh. |
Code Heap (Priority Queue)
Một cách cài đặt Heap (Priority Queue) 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 (Priority Queue) bằng Python
1class MinHeap:2 def __init__(self):3 self.data = []4
5 def push(self, value):6 # Append at the end, then bubble up to restore order7 self.data.append(value)8 i = len(self.data) - 19 while i > 0:10 parent = (i - 1) // 211 if self.data[parent] <= self.data[i]:12 break13 self.data[i], self.data[parent] = self.data[parent], self.data[i]14 i = parent15
16 def pop(self):17 # Move the last leaf to the root, then sift it down18 top = self.data[0]19 last = self.data.pop()20 if self.data:21 self.data[0] = last22 self._sift_down(0)23 return top24
25 def _sift_down(self, i):26 n = len(self.data)27 while True:28 smallest = i29 left, right = 2 * i + 1, 2 * i + 230 if left < n and self.data[left] < self.data[smallest]:31 smallest = left32 if right < n and self.data[right] < self.data[smallest]:33 smallest = right34 if smallest == i:35 return36 self.data[i], self.data[smallest] = self.data[smallest], self.data[i]37 i = smallest38
39
40heap = MinHeap()41for value in [5, 3, 8, 1, 9, 2]:42 heap.push(value)43
44print("Heap array: ", heap.data)45print("Popped in order:", [heap.pop() for _ in range(6)])Code Heap (Priority Queue) bằng JavaScript
1class MinHeap {2 constructor() {3 this.items = [];4 }5
6 push(value) {7 this.items.push(value);8 // Bubble the new value up while it is smaller than its parent9 let i = this.items.length - 1;10 while (i > 0) {11 const parent = Math.floor((i - 1) / 2);12 if (this.items[parent] <= this.items[i]) break;13 [this.items[parent], this.items[i]] = [this.items[i], this.items[parent]];14 i = parent;15 }16 }17
18 pop() {19 const top = this.items[0];20 const last = this.items.pop();21 if (this.items.length > 0) {22 this.items[0] = last;23 this.sinkDown(0);24 }25 return top;26 }27
28 sinkDown(i) {29 const n = this.items.length;30 while (true) {31 let smallest = i;32 const left = 2 * i + 1;33 const right = 2 * i + 2;34 if (left < n && this.items[left] < this.items[smallest]) smallest = left;35 if (right < n && this.items[right] < this.items[smallest]) smallest = right;36 if (smallest === i) return;37 [this.items[i], this.items[smallest]] = [this.items[smallest], this.items[i]];38 i = smallest;39 }40 }41}42
43const heap = new MinHeap();44for (const value of [5, 2, 9, 1, 7, 3]) heap.push(value);45const sorted = [];46while (heap.items.length > 0) sorted.push(heap.pop());47console.log("Popped in order:", sorted.join(" "));Code Heap (Priority Queue) bằng Java
1public class Main {2 static int[] heap = new int[32];3 static int size = 0;4
5 static void push(int value) {6 heap[size] = value;7 int i = size++;8 // Sift up while smaller than the parent9 while (i > 0 && heap[(i - 1) / 2] > heap[i]) {10 swap(i, (i - 1) / 2);11 i = (i - 1) / 2;12 }13 }14
15 static int pop() {16 int min = heap[0];17 heap[0] = heap[--size];18 int i = 0;19 // Sift down: swap with the smaller child until in place20 while (true) {21 int smallest = i, l = 2 * i + 1, r = 2 * i + 2;22 if (l < size && heap[l] < heap[smallest]) smallest = l;23 if (r < size && heap[r] < heap[smallest]) smallest = r;24 if (smallest == i) break;25 swap(i, smallest);26 i = smallest;27 }28 return min;29 }30
31 static void swap(int a, int b) {32 int tmp = heap[a];33 heap[a] = heap[b];34 heap[b] = tmp;35 }36
37 public static void main(String[] args) {38 int[] values = {9, 4, 7, 1, 8, 2};39 for (int v : values) push(v);40 System.out.print("Popped in order:");41 while (size > 0) System.out.print(" " + pop());42 System.out.println();43 }44}Code Heap (Priority Queue) bằng C++
1#include <iostream>2#include <utility>3#include <vector>4
5struct MinHeap {6 std::vector<int> data;7
8 void push(int value) {9 data.push_back(value);10 // Sift up until the parent is no larger11 size_t i = data.size() - 1;12 while (i > 0) {13 size_t parent = (i - 1) / 2;14 if (data[parent] <= data[i]) break;15 std::swap(data[parent], data[i]);16 i = parent;17 }18 }19
20 int pop() {21 int top = data[0];22 data[0] = data.back();23 data.pop_back();24 // Sift down: swap with the smaller child25 size_t i = 0;26 while (true) {27 size_t l = 2 * i + 1, r = 2 * i + 2, smallest = i;28 if (l < data.size() && data[l] < data[smallest]) smallest = l;29 if (r < data.size() && data[r] < data[smallest]) smallest = r;30 if (smallest == i) break;31 std::swap(data[i], data[smallest]);32 i = smallest;33 }34 return top;35 }36
37 int peek() const { return data[0]; }38 bool empty() const { return data.empty(); }39};40
41int main() {42 MinHeap heap;43 for (int value : {5, 3, 8, 1, 9, 2}) heap.push(value);44 std::cout << "Min: " << heap.peek() << "\n";45 std::cout << "Popped in order: ";46 while (!heap.empty()) std::cout << heap.pop() << " ";47 std::cout << "\n";48 return 0;49}Code Heap (Priority Queue) bằng C
1#include <stdio.h>2
3#define CAP 644
5int heap[CAP];6int heapSize = 0;7
8void push(int value) {9 // Sift up until the parent is no larger10 int i = heapSize++;11 heap[i] = value;12 while (i > 0) {13 int parent = (i - 1) / 2;14 if (heap[parent] <= heap[i]) break;15 int tmp = heap[parent];16 heap[parent] = heap[i];17 heap[i] = tmp;18 i = parent;19 }20}21
22int pop(void) {23 int top = heap[0];24 heap[0] = heap[--heapSize];25 // Sift down: swap with the smaller child26 int i = 0;27 while (1) {28 int l = 2 * i + 1, r = 2 * i + 2, smallest = i;29 if (l < heapSize && heap[l] < heap[smallest]) smallest = l;30 if (r < heapSize && heap[r] < heap[smallest]) smallest = r;31 if (smallest == i) break;32 int tmp = heap[i];33 heap[i] = heap[smallest];34 heap[smallest] = tmp;35 i = smallest;36 }37 return top;38}39
40int main(void) {41 int values[] = {5, 3, 8, 1, 9, 2};42 for (int i = 0; i < 6; i++) push(values[i]);43 printf("Min: %d\n", heap[0]);44 printf("Popped in order: ");45 while (heapSize > 0) printf("%d ", pop());46 printf("\n");47 return 0;48}Câu hỏi thường gặp về heap
Heap dùng để làm gì?
Heap dùng để cài đặt hàng đợi ưu tiên, nền tảng của thuật toán đường đi ngắn nhất Dijkstra, các bộ lập lịch tác vụ và mô phỏng sự kiện. Heap cũng là động cơ của heapsort. Bất cứ khi nào bạn cần lấy phần tử nhỏ nhất hoặc lớn nhất nhiều lần từ một tập dữ liệu luôn thay đổi, heap là công cụ phù hợp.
Heap khác cây tìm kiếm nhị phân như thế nào?
Cả hai đều là cây nhị phân, nhưng BST giữ thứ tự sắp xếp đầy đủ từ trái sang phải (cho phép tìm kiếm có thứ tự), còn heap chỉ bảo đảm quan hệ cha con (min hoặc max ở gốc). Heap cho phép truy cập giá trị cực trị trong O(1); BST cho phép tìm bất kỳ giá trị nào trong O(log n).
Tại sao heap được lưu trong mảng?
i nằm ở 2i+1 và 2i+2, còn cha nằm ở (i-1)/2. Cách này không cần lưu con trỏ đến nút con và tận dụng bộ nhớ đệm (cache) rất tốt.Heap có giống mảng đã sắp xếp không?
O(n), trong khi heap chèn trong O(log n) mà vẫn cho truy cập ngay giá trị cực trị.Khi nào nên dùng heap thay vì chỉ sắp xếp mảng?
O(log n), thay vì sắp xếp lại cả mảng. Nếu dữ liệu cố định và bạn cần mọi phần tử theo thứ tự, một lần sắp xếp O(n log n) đơn giản hơn và thường nhanh hơn.Xây dựng heap từ n phần tử có tốn O(n log n) không?
n phần tử từng cái một tốn O(n log n), nhưng heapify từ dưới lên, tức là sift down từ nút cha cuối cùng đến gốc, chỉ tốn O(n) tổng cộng, vì phần lớn các nút nằm gần đáy và chỉ phải đi xuống một quãng ngắn.