Heap (priority queue) giải hiệu quả các bài Top-K, K-way Merge và Median Stream. Kết hợp với BST operations từ Tuần 8 — bộ công cụ hoàn chỉnh cho cấu trúc dữ liệu nâng cao.
Heap được lưu dưới dạng mảng: cha tại i, con trái tại 2i+1, con phải tại 2i+2. Mảng nhỏ gọn → ít overhead so với cây thực sự.
Module heapq chỉ cài min-heap. Để có max-heap, dùng mẹo nhân âm:
import heapq
# Min-heap
h = []
heapq.heappush(h, 3)
heapq.heappush(h, 1)
heapq.heappush(h, 5)
heapq.heappop(h) # 1 (nhỏ nhất)
h[0] # 3 (peek không pop)
# Max-heap idiom — nhân -1
h = []
heapq.heappush(h, -3)
heapq.heappush(h, -1)
heapq.heappush(h, -5)
-heapq.heappop(h) # 5 (lớn nhất, đảo dấu khi đọc)
arr = [9, 5, 6, 2, 3]
heapq.heapify(arr) # In-place, O(n)
arr[0] # 2 — phần tử nhỏ nhất sau heapify
heapify là O(n), không phải O(n log n):| Operation | Complexity | Mô tả |
|---|---|---|
heappush(h, x) | O(log n) | Thêm x, duy trì heap property |
heappop(h) | O(log n) | Lấy & xoá phần tử cực đoan (root) |
h[0] | O(1) | Peek root, không xoá |
heapify(arr) | O(n) | Convert list thành heap in-place |
heappushpop(h, x) | O(log n) | Push x rồi pop — gọn hơn 2 thao tác |
nlargest(k, iter) | O(n log k) | k phần tử lớn nhất |
nsmallest(k, iter) | O(n log k) | k phần tử nhỏ nhất |
heapq.heappush(h, (priority, item)) # heap sort theo priority
heapq.heappush(h, (priority, idx, item)) # tiebreaker = idx khi priority bằng
idx để tránh
"< not supported between TreeNode" error. Đây là pattern bắt buộc cho Merge K Sorted Lists.
| Tín hiệu trong đề | Pattern |
|---|---|
| "Top K", "K largest/smallest" | Heap size K (min-heap cho top-K largest) |
| "K closest", "K most frequent" | Heap size K |
| "Merge K sorted X" | K-way Merge với min-heap |
| "Find median in stream" | Two heaps (max + min) |
| "Schedule with priority" | Priority queue |
| "Dijkstra's shortest path" (Tuần 10) | Min-heap |
| "Lúc nào cũng cần biết min/max" | Heap (ưu tiên) hoặc deque monotonic |
Sort O(n log n) toàn bộ; Heap O(n log K) chỉ cho K phần tử quan tâm — lợi rõ rệt khi K << n.
def top_k_largest(nums, k):
h = []
for x in nums:
heapq.heappush(h, x)
if len(h) > k:
heapq.heappop(h) # bỏ phần tử nhỏ nhất ngoài top-K
return h # K phần tử lớn nhất, không sort
Time O(n log K), Space O(K).
Đề (LeetCode #215): Tìm phần tử lớn thứ k trong mảng (không khác nhau).
def findKthLargest(nums, k):
return sorted(nums)[-k]
# Time O(n log n)
def findKthLargest(nums, k):
h = []
for x in nums:
heapq.heappush(h, x)
if len(h) > k:
heapq.heappop(h)
return h[0] # nhỏ nhất trong top-K = phần tử thứ k lớn nhất
# Time O(n log K)
# Trung bình O(n), worst case O(n²) — partition-based
So sánh: Sort đơn giản nhất, Heap tốt nhất cho stream, Quick Select nhanh nhất trung bình.
nums = [3, 2, 1, 5, 6, 4], k = 2. Kỳ vọng: 5.
| Bước | x | heap sau push | len > k? | heap sau pop |
|---|---|---|---|---|
| 1 | 3 | [3] | 1 ≤ 2 | [3] |
| 2 | 2 | [2, 3] | 2 ≤ 2 | [2, 3] |
| 3 | 1 | [1, 3, 2] | 3 > 2 → pop 1 | [2, 3] |
| 4 | 5 | [2, 3, 5] | 3 > 2 → pop 2 | [3, 5] |
| 5 | 6 | [3, 5, 6] | 3 > 2 → pop 3 | [5, 6] |
| 6 | 4 | [4, 6, 5] | 3 > 2 → pop 4 | [5, 6] |
Trả h[0] = 5 ✓.
Đề (LeetCode #347): Top K phần tử xuất hiện nhiều nhất.
Tuần 3 đã làm với Bucket Sort O(n). Giờ thêm cách Heap O(n log K):
from collections import Counter
import heapq
def topKFrequent(nums, k):
cnt = Counter(nums)
return heapq.nlargest(k, cnt.keys(), key=cnt.get)
nlargestdef topKFrequent(nums, k):
cnt = Counter(nums)
h = []
for num, freq in cnt.items():
heapq.heappush(h, (freq, num)) # min-heap theo freq
if len(h) > k:
heapq.heappop(h)
return [num for freq, num in h]
Đề (LeetCode #973): Mảng các điểm points[i] = [x, y]. Trả K điểm gần gốc toạ độ nhất.
"K nhỏ nhất" theo khoảng cách → dùng max-heap size K. Khi heap quá K, pop phần tử xa nhất.
def kClosest(points, k):
h = []
for x, y in points:
d = x*x + y*y # bình phương khoảng cách (đủ để so sánh)
heapq.heappush(h, (-d, x, y)) # nhân -1 để max-heap
if len(h) > k:
heapq.heappop(h)
return [[x, y] for _, x, y in h]
Time O(n log K), Space O(K).
sqrt chậm và có sai số floating-point.
Đây là tối ưu nhỏ nhưng quan trọng — luôn áp dụng khi có thể.
Đề (LeetCode #1046): Mảng stones. Mỗi vòng, chọn 2 viên nặng nhất:
nếu bằng → cả hai biến mất; nếu khác → viên mới = hiệu. Trả về viên cuối còn lại (0 nếu hết).
def lastStoneWeight(stones):
h = [-s for s in stones] # max-heap idiom
heapq.heapify(h) # O(n)
while len(h) > 1:
a = -heapq.heappop(h) # nặng nhất
b = -heapq.heappop(h) # nặng thứ hai
if a != b:
heapq.heappush(h, -(a - b))
return -h[0] if h else 0
Time O(n log n), Space O(n).
-x push vào và -heapq.heappop(h) đọc ra → đây là max-heap.
Pattern xuất hiện ở nhiều bài: Reorganize String, Task Scheduler, Maximum Performance of a Team.
heapify(arr) có complexity gì?Tại mỗi bước, lấy nhỏ nhất trong K đầu hiện tại, đẩy vào kết quả, đẩy đầu mới của nguồn đó vào heap.
Đề (LeetCode #23, Hard): Cho k linked list đã sort. Merge thành 1 list sort.
import heapq
def mergeKLists(lists):
h = []
# Push (val, idx, node) — idx làm tiebreaker để tránh "< not supported"
for i, head in enumerate(lists):
if head:
heapq.heappush(h, (head.val, i, head))
dummy = ListNode(0)
tail = dummy
while h:
val, i, node = heapq.heappop(h)
tail.next = node
tail = tail.next
if node.next:
heapq.heappush(h, (node.next.val, i, node.next))
return dummy.next
Time O(N log K) với N = tổng node, K = số list.
Heap tối đa K phần tử. Mỗi node được push 1 lần và pop 1 lần → tổng N × O(log K).
3 list: [1→4→5], [1→3→4], [2→6].
| Bước | Heap | Pop (val, idx) | Push tiếp | Output |
|---|---|---|---|---|
| 0 | [(1,0,...), (1,1,...), (2,2,...)] | — | — | — |
| 1 | — | (1, 0) | (4, 0, ...) | 1 |
| 2 | — | (1, 1) | (3, 1, ...) | 1, 1 |
| 3 | — | (2, 2) | (6, 2, ...) | 1, 1, 2 |
| 4 | — | (3, 1) | (4, 1, ...) | 1, 1, 2, 3 |
| 5 | — | (4, 0) | (5, 0, ...) | 1, 1, 2, 3, 4 |
| 6 | — | (4, 1) | — | 1, 1, 2, 3, 4, 4 |
| 7 | — | (5, 0) | — | 1, 1, 2, 3, 4, 4, 5 |
| 8 | — | (6, 2) | — | 1, 1, 2, 3, 4, 4, 5, 6 |
Hết heap → trả output. Heap không vượt 3 phần tử.
__lt__ → lỗi.
idx duy nhất đảm bảo so sánh không bao giờ chạm tới node.
Đề (LeetCode #373): Hai mảng sort nums1, nums2, số k.
Trả K cặp (u, v) có tổng nhỏ nhất.
Tổng số cặp = n × m, có thể rất lớn. Brute force: tạo mọi cặp + sort → O(nm log(nm)). Cần tốt hơn.
def kSmallestPairs(nums1, nums2, k):
if not nums1 or not nums2: return []
h = []
# Khởi tạo: ghép nums1[i] với nums2[0], i ∈ [0, min(k, len(nums1)))
for i in range(min(k, len(nums1))):
heapq.heappush(h, (nums1[i] + nums2[0], i, 0))
res = []
while h and len(res) < k:
s, i, j = heapq.heappop(h)
res.append([nums1[i], nums2[j]])
if j + 1 < len(nums2):
heapq.heappush(h, (nums1[i] + nums2[j+1], i, j+1))
return res
Time O(K log K), Space O(K).
|lo| = |hi| hoặc |lo| = |hi| + 1.
Pattern này áp dụng cho mọi bài "duy trì median động khi dữ liệu đến từng bước".
Đề (LeetCode #295, Hard): Cài data structure hỗ trợ:
addNum(num) — thêm số.findMedian() — trả median của các số đã thêm.import heapq
class MedianFinder:
def __init__(self):
self.lo = [] # max-heap (nhân -1)
self.hi = [] # min-heap
def addNum(self, num):
# Bước 1: đẩy vào lo, sau đó lấy top lo đẩy sang hi
heapq.heappush(self.lo, -num)
heapq.heappush(self.hi, -heapq.heappop(self.lo))
# Bước 2: nếu hi lớn hơn → cân bằng lại
if len(self.hi) > len(self.lo):
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def findMedian(self):
if len(self.lo) > len(self.hi):
return -self.lo[0]
return (-self.lo[0] + self.hi[0]) / 2
addNum O(log n), findMedian O(1).
Stream: 1, 2, 3, 4, 5
| Add | Bước 1: lo→hi qua đỉnh | Cân bằng | lo (max-heap) | hi (min-heap) | Median |
|---|---|---|---|---|---|
| 1 | push 1 vào lo, top lo (1) → hi | hi > lo → trả 1 về lo | [1] | [] | 1 |
| 2 | push 2 vào lo (top=2), 2 → hi | cân bằng (1=1) | [1] | [2] | (1+2)/2 = 1.5 |
| 3 | push 3 vào lo (top=3), 3 → hi | hi > lo → 2 về lo | [2, 1] | [3] | 2 |
| 4 | push 4 vào lo (top=4), 4 → hi | cân bằng (2=2) | [2, 1] | [3, 4] | (2+3)/2 = 2.5 |
| 5 | push 5 vào lo (top=5), 5 → hi | hi > lo → 3 về lo | [3, 1, 2] | [4, 5] | 3 |
| Operation | Average | Worst |
|---|---|---|
| Search | O(log n) | O(n) |
| Insert | O(log n) | O(n) |
| Delete | O(log n) | O(n) |
| Inorder traversal | O(n) | O(n) |
Worst case = O(n) khi cây bị skewed (mỗi node chỉ có 1 con). Balanced BST (AVL, Red-Black) đảm bảo O(log n) — nhưng LeetCode ít khi yêu cầu cài.
def search(root, target):
if not root or root.val == target: return root
if target < root.val:
return search(root.left, target)
return search(root.right, target)
Đệ quy theo nhánh duy nhất → O(h) với h = chiều cao cây.
Đề (LeetCode #230): Trả phần tử nhỏ thứ k trong BST.
Inorder của BST cho dãy sort tăng dần (Tuần 8). Phần tử thứ k của inorder = đáp án.
def kthSmallest(root, k):
res = []
def inorder(node):
if not node: return
inorder(node.left)
res.append(node.val)
inorder(node.right)
inorder(root)
return res[k - 1]
# Time O(n), Space O(n) — không tận dụng "dừng sớm"
def kthSmallest(root, k):
stack = []
cur = root
while cur or stack:
while cur:
stack.append(cur)
cur = cur.left
cur = stack.pop()
k -= 1
if k == 0: return cur.val
cur = cur.right
# Time O(h + k), Space O(h)
Đề (LeetCode #701): Thêm node giá trị val vào BST. Trả root cây mới.
def insertIntoBST(root, val):
if not root: return TreeNode(val) # đến vị trí trống → tạo node
if val < root.val:
root.left = insertIntoBST(root.left, val)
else:
root.right = insertIntoBST(root.right, val)
return root
Time O(h), Space O(h).
Tin rằng insertIntoBST(root.left, val) trả về cây con trái với val đã chèn. Gán lại
root.left = ... để cây cha "biết" thay đổi. Đây là pattern "trả về node đã sửa, gán lại" — xuất hiện ở Insert/Delete BST.
root.left = insertIntoBST(...) chứ không gọi không gán?Đề (LeetCode #450): Xoá node có key trong BST. Trả root cây mới.
def deleteNode(root, key):
if not root: return None
if key < root.val:
root.left = deleteNode(root.left, key)
elif key > root.val:
root.right = deleteNode(root.right, key)
else: # node cần xoá
if not root.left: return root.right # case 1+2
if not root.right: return root.left # case 1+2
# Case 3: tìm successor
succ = root.right
while succ.left: succ = succ.left
root.val = succ.val # copy giá trị
root.right = deleteNode(root.right, succ.val) # xoá successor
return root
Time O(h), Space O(h).
| Tín hiệu trong đề | Pattern |
|---|---|
| "Top K largest" với n lớn / stream | Min-heap size K |
| "Top K smallest" | Max-heap size K (idiom -x) |
| "K most frequent" | Counter + Heap (hoặc Bucket Sort O(n)) |
| "K closest points" | Max-heap size K theo khoảng cách bình phương |
| "Merge K sorted lists/arrays" | K-way Merge với min-heap chứa "đầu hiện tại" |
| "K smallest pairs/sums" | Min-heap với "expand frontier" |
| "Median in stream" | Two heaps (max + min) cân bằng |
| "Schedule with priority" | Priority queue |
| "Find Kth in BST" | Inorder iterative với early stop |
| "Kth largest in array (one-shot)" | Quick Select O(n) trung bình hoặc Heap O(n log K) |
Tự hỏi: "Mỗi bước có cần biết phần tử cực đoan của tập đã thấy không?" Có → Heap.
Graph — BFS, DFS, Connected Components, Topological Sort
Cấu trúc dữ liệu cuối cùng cần làm chủ. Học cách biểu diễn graph (adjacency list, matrix), duyệt graph qua BFS (queue đã học) và DFS (recursion đã học), tìm thành phần liên thông qua Union-Find, và Topological Sort cho graph có hướng không chu kỳ. Bài "boss": Course Schedule, Word Ladder, Number of Islands. Min-heap (Tuần 9) sẽ tái xuất hiện ở Dijkstra's Shortest Path.
Nhấn T hoặc Esc để đóng