🧩 Hash Table & Priority Queue
Hash Table giúp truy cập theo khóa rất nhanh trong trung bình; Priority Queue luôn đưa phần tử có độ ưu tiên cao nhất ra trước. Hai cấu trúc này xuất hiện dày đặc trong đếm tần suất, tra cứu, mô phỏng lịch xử lý và các thuật toán tham lam.
🎯 Mục tiêu bài học
Cần hiểu
- Hash Table ánh xạ khóa vào bucket như thế nào.
- Va chạm hash và cơ chế chaining.
- Khác nhau giữa
mapvàunordered_map. - Priority Queue dựa trên heap.
- Max-heap, min-heap và comparator.
Cần làm được
- Đếm tần suất bằng
unordered_map. - Dùng
find,count,erase. - Dùng
priority_queueđể lấy max hoặc min. - Lưu
pairtrong Priority Queue. - Mô phỏng chọn công việc theo độ ưu tiên.
1. Đặt vấn đề
Có n từ. Với mỗi từ, cần biết xuất hiện bao nhiêu lần. Sau đó có q truy vấn hỏi tần suất của một từ.
Nhiều công việc đang chờ, mỗi công việc có mức ưu tiên. Mỗi lần phải lấy công việc có ưu tiên cao nhất.
1.1. Dùng cấu trúc nào?
| Nhu cầu | Cách chậm / bất tiện | Cấu trúc phù hợp |
|---|---|---|
| Tìm dữ liệu theo khóa | Duyệt toàn bộ danh sách O(n) | Hash Table – trung bình O(1) |
| Luôn lấy phần tử lớn nhất | Mỗi lần duyệt tìm max O(n) | Priority Queue – top O(1), push/pop O(log n) |
2. Hash Table và unordered_map
Ý tưởng
Một hàm hash biến khóa thành một số, sau đó ánh xạ số đó vào bucket.
Cú pháp C++
unordered_map frequency;
frequency["apple"]++;
frequency["banana"] += 2;
if (frequency.find("apple")
!= frequency.end()) {
cout << frequency["apple"];
}
| Thao tác | Cú pháp | Trung bình | Ghi chú |
|---|---|---|---|
| Thêm/cập nhật | mp[key]=value | O(1) | operator[] có thể tạo khóa mới. |
| Tìm | mp.find(key) | O(1) | Không tạo phần tử mới. |
| Kiểm tra | mp.count(key) | O(1) | Với unordered_map, kết quả là 0 hoặc 1. |
| Xóa | mp.erase(key) | O(1) | Trung bình. |
| Duyệt | range-based for | O(n) | Thứ tự không được đảm bảo. |
3. Mô phỏng Hash Table
4. Va chạm Hash
Hai khóa khác nhau có thể được ánh xạ vào cùng một bucket. Đây gọi là collision.
Separate chaining
Mỗi bucket lưu một danh sách các phần tử. Đây là cách mô phỏng trong bài học.
Load factor
size / bucket_count. Load factor cao thường làm tăng va chạm.
Rehash
Khi cần, container tăng số bucket và phân bố lại phần tử.
5. Priority Queue
Max-heap mặc định
priority_queue pq;
pq.push(5);
pq.push(10);
pq.push(3);
cout << pq.top(); // 10
Min-heap
priority_queue<
int,
vector,
greater
> pq;
| Thao tác | Độ phức tạp | Ý nghĩa |
|---|---|---|
top() | O(1) | Xem phần tử ưu tiên nhất. |
push(x) | O(log n) | Thêm phần tử và khôi phục heap. |
pop() | O(log n) | Xóa phần tử ưu tiên nhất. |
empty(), size() | O(1) | Kiểm tra trạng thái. |
6. Mô phỏng Priority Queue
Mảng heap
7. Mô phỏng lịch xử lý công việc
(priority, id). Chọn ưu tiên lớn hơn trước; nếu hòa, chọn id nhỏ hơn trước.
8. Phân tích và lựa chọn cấu trúc
| Nhu cầu | Cấu trúc | Độ phức tạp chính | Ghi chú |
|---|---|---|---|
| Tra cứu theo khóa, không cần thứ tự | unordered_map | O(1) trung bình | Thứ tự duyệt không ổn định. |
| Tra cứu theo khóa và cần thứ tự | map | O(log n) | Duyệt tăng theo khóa. |
| Kiểm tra tồn tại | unordered_set | O(1) trung bình | Không lưu value. |
| Luôn lấy max/min | priority_queue | top O(1), push/pop O(log n) | Không hỗ trợ tìm/xóa tùy ý hiệu quả. |
| Cần cập nhật/xóa phần tử bất kỳ | Cấu trúc khác | Tùy bài | Có thể dùng set, indexed heap hoặc lazy deletion. |
9. Code mẫu
#include
using namespace std;
int main() {
int n;
cin >> n;
unordered_map frequency;
for (int i = 0; i < n; i++) {
string word;
cin >> word;
frequency[word]++;
}
int q;
cin >> q;
while (q--) {
string word;
cin >> word;
auto iterator = frequency.find(word);
if (iterator == frequency.end()) {
cout << 0 << '
';
} else {
cout << iterator->second << '
';
}
}
} unordered_map nameById;
nameById[10] = "An";
nameById.insert({20, "Binh"});
nameById.emplace(30, "Chi");
if (nameById.count(20)) {
cout << nameById.at(20);
}
auto iterator = nameById.find(30);
if (iterator != nameById.end()) {
nameById.erase(iterator);
}
for (const auto& [id, name] : nameById) {
cout << id << ' ' << name << '
';
} #include
using namespace std;
int main() {
priority_queue pq;
pq.push(5);
pq.push(10);
pq.push(3);
pq.push(8);
while (!pq.empty()) {
cout << pq.top() << ' ';
pq.pop();
}
}
// Output: 10 8 5 3 #include
using namespace std;
int main() {
priority_queue<
long long,
vector,
greater
> minHeap;
minHeap.push(5);
minHeap.push(10);
minHeap.push(3);
minHeap.push(8);
while (!minHeap.empty()) {
cout << minHeap.top() << ' ';
minHeap.pop();
}
}
// Output: 3 5 8 10 #include
using namespace std;
struct Job {
int id;
int priority;
};
struct CompareJob {
bool operator()(
const Job& a,
const Job& b
) const {
if (a.priority != b.priority) {
return a.priority < b.priority;
}
return a.id > b.id;
}
};
int main() {
priority_queue<
Job,
vector,
CompareJob
> jobs;
jobs.push({3, 5});
jobs.push({1, 5});
jobs.push({2, 7});
while (!jobs.empty()) {
Job current = jobs.top();
jobs.pop();
cout << current.id
<< ' '
<< current.priority
<< '
';
}
} from collections import Counter
import heapq
words = ["apple", "banana", "apple"]
frequency = Counter(words)
min_heap = []
heapq.heappush(min_heap, 5)
heapq.heappush(min_heap, 10)
heapq.heappush(min_heap, 3)
while min_heap:
print(heapq.heappop(min_heap))10. Giải thích code
10.1. Vì sao frequency[word]++ hoạt động?
Nếu khóa chưa tồn tại, operator[] tạo một phần tử mới với giá trị mặc định 0, sau đó tăng lên 1.
10.2. Khi nào nên dùng find thay vì operator[]?
Khi chỉ muốn kiểm tra hoặc đọc mà không muốn tạo khóa mới. mp[key] có thể làm thay đổi map.
10.3. Vì sao unordered_map không có thứ tự tăng dần?
Phần tử được tổ chức theo bucket của hash, không theo thứ tự khóa.
10.4. Vì sao priority_queue mặc định là max-heap?
Comparator mặc định làm phần tử lớn nhất có độ ưu tiên cao nhất và nằm ở top().
10.5. Comparator Priority Queue có vẻ “ngược” vì sao?
Comparator trả về true khi a có độ ưu tiên thấp hơn b. Với max-heap số nguyên, less làm số lớn nhất ở top.
10.6. Vì sao top() không xóa phần tử?
top() chỉ xem phần tử ưu tiên nhất. Muốn xóa, phải gọi pop() riêng.
10.7. Có thể sửa trực tiếp pq.top() không?
Không nên và giao diện chuẩn trả về tham chiếu hằng. Sửa trực tiếp có thể phá tính chất heap.
10.8. Khi nào dùng lazy deletion?
Khi cần “xóa” hoặc cập nhật phần tử không nằm ở top. Ta đánh dấu dữ liệu cũ là không còn hợp lệ và bỏ qua khi nó lên top.
11. Lỗi thường gặp
- Dùng
mp[key]chỉ để kiểm tra và vô tình tạo khóa. - Cho rằng
unordered_mapduyệt theo thứ tự chèn hoặc thứ tự khóa. - Giả sử mọi thao tác hash luôn O(1), bỏ qua worst case.
- Quên rằng rehash có thể làm iterator bị vô hiệu hóa.
- Gọi
top()hoặcpop()khi Priority Queue rỗng. - Tưởng
pop()trả về phần tử. - Dùng comparator sai chiều trong Priority Queue.
- Tưởng Priority Queue hỗ trợ xóa phần tử bất kỳ hiệu quả.
- Dùng
priority_queuenhưng quên quy tắc so sánh từ điển.> - Dùng min-heap nhưng quên thêm
greater.
12. Quiz và bài tập
📘 Cơ bản
- Đếm tần suất số nguyên.
- Kiểm tra từ đã xuất hiện.
- In các phần tử Priority Queue theo thứ tự.
📗 Trung bình
- Top K phần tử lớn nhất.
- Tìm K số xuất hiện nhiều nhất.
- Mô phỏng lịch xử lý công việc.
📙 Nâng cao
- Ghép dây với chi phí nhỏ nhất.
- K-way merge.
- Sliding window với lazy deletion.
🐉 HSG
- Dijkstra với Priority Queue.
- Custom hash chống dữ liệu xấu.
- Indexed heap / mutable priority queue.
💳 Quét mã ủng hộ tuỳ tâm nhé!