
Chào các "coder nhí" tương lai của thế giới số! Anh là Creyt đây, và hôm nay chúng ta sẽ "bóc tách" một khái niệm siêu "cool ngầu" mà lại cực kỳ hữu ích trong lập trình: priority_queue.
1. Priority Queue là gì? "Xếp hàng VIP" cho dữ liệu của bạn!
Các em hình dung thế này: Khi mình đi xem concert của idol, có phải ai đến trước thì được vào trước không? Đúng, đó là hàng đợi (queue) thông thường. Nhưng nếu có hàng "VIP" hoặc "Fast Pass" thì sao? Dù bạn đến sau, nhưng vì có "ưu tiên" (priority) cao hơn, bạn sẽ được vào trước, đúng không? priority_queue trong C++ chính là cái "hàng VIP" đó!
Nói một cách hàn lâm hơn nhưng vẫn dễ hiểu, priority_queue là một container adapter (một kiểu gói ghém các container khác như vector hoặc deque) mà nó luôn đảm bảo rằng phần tử có độ ưu tiên cao nhất sẽ luôn nằm ở đầu hàng đợi.
Để làm gì? Đơn giản là để các em luôn có thể lấy ra cái "quan trọng nhất", cái "khẩn cấp nhất" hoặc cái "lớn nhất/nhỏ nhất" một cách nhanh chóng mà không cần phải lục tung cả đống dữ liệu lên.
2. Cách hoạt động: "Heap" là ông trùm!
Đằng sau cái vẻ "ưu tiên" kia, priority_queue thường được triển khai bằng một cấu trúc dữ liệu gọi là Heap (cụ thể hơn là Max-Heap theo mặc định trong C++). Heap là một cây nhị phân gần hoàn chỉnh có một "tính chất" đặc biệt: giá trị của mỗi nút luôn lớn hơn hoặc bằng giá trị của các nút con của nó. Nhờ vậy, cái phần tử "to nhất" (ưu tiên cao nhất) luôn nằm ở gốc cây, tức là ở "đầu" priority_queue.
Các thao tác cơ bản:
push(element): Thêm một phần tử vào hàng đợi. Nó sẽ tự động sắp xếp lại để đảm bảo phần tử có ưu tiên cao nhất vẫn ở đầu. (Độ phức tạp: O(log N))pop(): Xóa phần tử có ưu tiên cao nhất ra khỏi hàng đợi. (Độ phức tạp: O(log N))top(): Xem phần tử có ưu tiên cao nhất mà không xóa nó. (Độ phức tạp: O(1))empty(): Kiểm tra xem hàng đợi có rỗng không. (Độ phức tạp: O(1))size(): Trả về số lượng phần tử. (Độ phức tạp: O(1))

3. Code Ví Dụ Minh Họa: "Thực chiến" thôi!
Ví dụ 1: Xếp hàng ưu tiên cho số nguyên (Max-Heap mặc định)
#include <iostream>
#include <queue> // Thư viện chứa priority_queue
#include <vector> // Mặc định dùng vector làm container
int main() {
// Khai báo một priority_queue kiểu int (mặc định là Max-Heap)
std::priority_queue<int> pq;
// Thêm các phần tử vào hàng đợi
pq.push(10);
pq.push(30);
pq.push(20);
pq.push(5);
pq.push(15);
std::cout << "Cac phan tu trong priority_queue (tu lon nhat den nho nhat):\n";
while (!pq.empty()) {
std::cout << pq.top() << " "; // Xem phan tu lon nhat
pq.pop(); // Xoa phan tu lon nhat
}
std::cout << std::endl;
// Output: 30 20 15 10 5
return 0;
}
Ví dụ 2: Tạo Min-Heap (ưu tiên số nhỏ nhất)
Để có một Min-Heap (tức là phần tử nhỏ nhất được ưu tiên), chúng ta cần chỉ định một comparator (bộ so sánh) khác. std::greater<int> sẽ làm điều đó.
#include <iostream>
#include <queue>
#include <vector>
#include <functional> // Can thiet cho std::greater
int main() {
// Khai bao priority_queue kieu int, su dung std::greater<int> de lam Min-Heap
std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;
min_pq.push(10);
min_pq.push(30);
min_pq.push(20);
min_pq.push(5);
min_pq.push(15);
std::cout << "Cac phan tu trong min_priority_queue (tu nho nhat den lon nhat):\n";
while (!min_pq.empty()) {
std::cout << min_pq.top() << " "; // Xem phan tu nho nhat
min_pq.pop(); // Xoa phan tu nho nhat
}
std::cout << std::endl;
// Output: 5 10 15 20 30
return 0;
}
Ví dụ 3: Ưu tiên với đối tượng tùy chỉnh (Custom Object)
Giả sử bạn muốn ưu tiên các sinh viên dựa trên điểm số của họ.
#include <iostream>
#include <queue>
#include <vector>
#include <string>
// Định nghĩa một struct SinhVien
struct SinhVien {
std::string ten;
int diem;
// Constructor
SinhVien(std::string t, int d) : ten(t), diem(d) {}
// Hàm so sánh cho priority_queue (để tạo Max-Heap theo điểm)
// Nếu muốn sinh viên điểm cao hơn được ưu tiên, thì operator< sẽ trả về true
// khi 'this' có điểm THẤP hơn 'other'. Nghe hơi ngược đời nhưng nó là vậy đó!
// priority_queue dùng operator< để quyết định phần tử nào 'nhỏ hơn'
// và phần tử 'nhỏ hơn' sẽ có ưu tiên THẤP hơn.
// Để điểm cao được ưu tiên, ta cần đảo ngược logic so sánh mặc định.
// HOẶC đơn giản hơn: viết một struct comparator riêng.
// Cách 1: Overload operator< (phổ biến hơn)
bool operator<(const SinhVien& other) const {
return diem < other.diem; // Sinh vien co diem THAP HON se bi coi la 'nho hon' -> uu tien THAP HON
// => Nghia la sinh vien diem CAO HON se duoc uu tien CAO HON (Max-Heap theo diem)
}
};
// Cách 2: Định nghĩa một struct comparator riêng (minh bạch hơn cho một số trường hợp)
// struct CompareSinhVien {
// bool operator()(const SinhVien& a, const SinhVien& b) {
// return a.diem < b.diem; // Max-Heap theo diem
// }
// };
int main() {
std::priority_queue<SinhVien> danhSachThiDua;
// Hoac: std::priority_queue<SinhVien, std::vector<SinhVien>, CompareSinhVien> danhSachThiDua;
danhSachThiDua.push(SinhVien("An", 85));
danhSachThiDua.push(SinhVien("Binh", 92));
danhSachThiDua.push(SinhVien("Cuong", 78));
danhSachThiDua.push(SinhVien("Dung", 95));
std::cout << "Danh sach sinh vien theo thu tu uu tien (diem cao nhat):\n";
while (!danhSachThiDua.empty()) {
SinhVien sv = danhSachThiDua.top();
std::cout << "Ten: " << sv.ten << ", Diem: " << sv.diem << std::endl;
danhSachThiDua.pop();
}
// Output:
// Ten: Dung, Diem: 95
// Ten: Binh, Diem: 92
// Ten: An, Diem: 85
// Ten: Cuong, Diem: 78
return 0;
}
4. Mẹo hay & Best Practices từ "Giáo sư Creyt"
- Nhớ kỹ mặc định là Max-Heap: Cứ
std::priority_queue<int>là nó sẽ ưu tiên số lớn nhất. Muốn số nhỏ nhất thì phải thêmstd::greater<int>vào nhé! - Custom Object? Overload
operator<: Khi làm việc vớistructhoặcclasscủa riêng mình, hãy overloadoperator<đểpriority_queuebiết cách so sánh và xác định độ ưu tiên. Nhớ làoperator<trả vềtruekhithiscó ưu tiên thấp hơnother(để tạo Max-Heap). Hoặc viết mộtcomparatorriêng cho nó minh bạch. - Không phải lúc nào cũng là giải pháp:
priority_queuerất mạnh mẽ nhưng không phải là "thuốc tiên". Nếu bạn cần truy cập ngẫu nhiên (random access) vào các phần tử, hoặc cần duyệt qua tất cả các phần tử theo thứ tự không ưu tiên, thìstd::vectorhoặcstd::listcó thể là lựa chọn tốt hơn. - Độ phức tạp là bạn: Nhớ
O(log N)chopush/popvàO(1)chotop. Điều này cực kỳ quan trọng khi các em đối phó với dữ liệu lớn.
5. Ứng dụng thực tế: "Priority Queue" có ở đâu?
priority_queue không chỉ là lý thuyết suông đâu, nó là "ngôi sao" thầm lặng đằng sau rất nhiều ứng dụng mà các em dùng hàng ngày đó:
- Hệ điều hành (Operating Systems): Khi máy tính của em chạy nhiều chương trình cùng lúc, CPU cần quyết định chương trình nào sẽ được chạy tiếp theo. Các tác vụ quan trọng hơn (như xử lý sự kiện chuột) sẽ có ưu tiên cao hơn các tác vụ nền (như cập nhật phần mềm). Đó chính là
priority_queuegiúp quản lý hàng đợi các tiến trình. - Thuật toán tìm đường (Pathfinding Algorithms): Các thuật toán như Dijkstra's hoặc A* (dùng trong game, Google Maps) để tìm đường đi ngắn nhất đều sử dụng
priority_queueđể luôn chọn điểm đến tiếp theo có "chi phí" (quãng đường) thấp nhất. - Mạng máy tính (Networking): Các router dùng
priority_queueđể ưu tiên các gói dữ liệu quan trọng hơn (ví dụ: dữ liệu thoại/video cần độ trễ thấp) so với các gói dữ liệu khác. - Mô phỏng sự kiện (Event Simulation): Trong các hệ thống mô phỏng,
priority_queuegiúp sắp xếp các sự kiện theo thời gian xảy ra, đảm bảo sự kiện nào đến trước (hoặc có ưu tiên cao hơn) sẽ được xử lý trước. - Y tế (Healthcare): Trong phòng cấp cứu, bệnh nhân sẽ được ưu tiên điều trị dựa trên mức độ nghiêm trọng của tình trạng, chứ không phải ai đến trước.
priority_queuecó thể mô phỏng hệ thống ưu tiên này.
6. Thử nghiệm và Hướng dẫn sử dụng
Khi nào nên dùng priority_queue?
- Khi bạn luôn cần truy cập hoặc xóa phần tử "tốt nhất" (best) hoặc "tồi tệ nhất" (worst) (dựa trên một tiêu chí ưu tiên nào đó) trong một tập hợp dữ liệu thay đổi liên tục.
- Khi bạn cần triển khai các thuật toán như Dijkstra's (tìm đường ngắn nhất), Prim's (tìm cây bao trùm tối thiểu), Huffman Coding (nén dữ liệu).
- Khi bạn muốn quản lý các tác vụ hoặc sự kiện theo mức độ khẩn cấp hoặc thời gian.
Khi nào KHÔNG nên dùng priority_queue?
- Khi bạn cần một hàng đợi FIFO (First-In, First-Out) truyền thống. Hãy dùng
std::queue. - Khi bạn cần một hàng đợi LIFO (Last-In, First-Out) (stack). Hãy dùng
std::stack. - Khi bạn cần truy cập ngẫu nhiên vào các phần tử hoặc duyệt qua tất cả các phần tử theo thứ tự không ưu tiên.
std::vectorhoặcstd::listcó thể phù hợp hơn. - Khi bạn cần một cấu trúc dữ liệu mà bạn có thể xóa một phần tử bất kỳ không phải là phần tử ưu tiên cao nhất một cách hiệu quả (thường thì các cấu trúc cây cân bằng như
std::sethoặcstd::mapsẽ tốt hơn cho việc này).
Thử nghiệm "nhẹ" cho các em:
Hãy thử viết một chương trình mô phỏng việc xếp hàng mua vé xem phim. Có hàng thường và hàng VIP. Hàng VIP được ưu tiên hơn hàng thường, nhưng trong mỗi hàng thì ai đến trước được phục vụ trước. Các em có thể dùng 2 priority_queue hoặc kết hợp priority_queue với std::pair để lưu cả ưu tiên và thời gian đến. Đây là một bài tập nhỏ để các em "vận động não" và áp dụng kiến thức vừa học đó!
Vậy là chúng ta đã cùng nhau khám phá "thế giới VIP" của priority_queue. Hy vọng các em đã nắm vững khái niệm và sẵn sàng áp dụng nó vào các dự án của mình. Nhớ nhé, lập trình là phải "thực chiến"!
Giáo sư Creyt out!
Thuộc Series: C++
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!