C++ STL Containers

🧩 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.

unordered_mapKhóa → giá trị
Average O(1)Tra cứu trung bình
priority_queueLấy phần tử ưu tiên nhất
O(log n)push / pop heap

🎯 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 mapunordered_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 pair trong Priority Queue.
  • Mô phỏng chọn công việc theo độ ưu tiên.

1. Đặt vấn đề

Bài toán A – đếm tần suất.
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ừ.
Bài toán B – xử lý công việc.
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ầuCách chậm / bất tiệnCấu trúc phù hợp
Tìm dữ liệu theo khóaDuyệ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ấtMỗi lần duyệt tìm max O(n)Priority Queue – top O(1), push/pop O(log n)
Hash Table tối ưu tra cứu theo khóa; Priority Queue tối ưu truy cập phần tử ưu tiên nhất

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.

bucket = hash(key) mod bucket_count

Cú pháp C++

unordered_map frequency;

frequency["apple"]++;
frequency["banana"] += 2;

if (frequency.find("apple")
    != frequency.end()) {
    cout << frequency["apple"];
}
Thao tácCú phápTrung bìnhGhi chú
Thêm/cập nhậtmp[key]=valueO(1)operator[] có thể tạo khóa mới.
Tìmmp.find(key)O(1)Không tạo phần tử mới.
Kiểm tramp.count(key)O(1)Với unordered_map, kết quả là 0 hoặc 1.
Xóamp.erase(key)O(1)Trung bình.
Duyệtrange-based forO(n)Thứ tự không được đảm bảo.
Worst case: Nếu nhiều khóa rơi vào cùng bucket, thao tác có thể suy giảm tới O(n).

3. Mô phỏng Hash Table

key
hash(key)
bucket

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ử.

Ứng dụng phù hợp: đếm tần suất, kiểm tra tồn tại, ánh xạ ID → thông tin, lưu trạng thái đã thăm.

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.
Priority Queue không duyệt theo thứ tự đầy đủ. Muốn lấy toàn bộ theo thứ tự ưu tiên, phải lặp top rồi pop.

6. Mô phỏng Priority Queue

Mảng heap

7. Mô phỏng lịch xử lý công việc

Mỗi công việc 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ầuCấu trúcĐộ phức tạp chínhGhi chú
Tra cứu theo khóa, không cần thứ tựunordered_mapO(1) trung bìnhThứ tự duyệt không ổn định.
Tra cứu theo khóa và cần thứ tựmapO(log n)Duyệt tăng theo khóa.
Kiểm tra tồn tạiunordered_setO(1) trung bìnhKhông lưu value.
Luôn lấy max/minpriority_queuetop 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ácTùy bàiCó thể dùng set, indexed heap hoặc lazy deletion.
Priority Queue không phải hàng đợi FIFO. Phần tử vào trước chưa chắc ra trước; thứ tự do comparator quyết định.

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_map duyệ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ặc pop() 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_queue> như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âu 1. Tra cứu unordered_map có độ phức tạp trung bình nào?
Câu 2. Priority Queue mặc định của int là gì?
Câu 3. Muốn xem nhưng không xóa phần tử ưu tiên nhất dùng gì?
Câu 4. Khi chỉ muốn kiểm tra khóa có tồn tại mà không tạo khóa mới, nên dùng gì?

📘 Cơ bản

  1. Đếm tần suất số nguyên.
  2. Kiểm tra từ đã xuất hiện.
  3. In các phần tử Priority Queue theo thứ tự.

📗 Trung bình

  1. Top K phần tử lớn nhất.
  2. Tìm K số xuất hiện nhiều nhất.
  3. Mô phỏng lịch xử lý công việc.

📙 Nâng cao

  1. Ghép dây với chi phí nhỏ nhất.
  2. K-way merge.
  3. Sliding window với lazy deletion.

🐉 HSG

  1. Dijkstra với Priority Queue.
  2. Custom hash chống dữ liệu xấu.
  3. Indexed heap / mutable priority queue.