CTDL & giải thuật

Hàng đợi Queue: FIFO, hàng đợi vòng và code C++, Python

Hàng đợi (queue) lấy phần tử theo thứ tự vào trước ra trước, gọi là FIFO. Trong C++ bạn dùng std::queue; trong Python bạn thường dùng collections.deque để thêm cuối và lấy đầu hiệu quả.

Ý chính
  • Queue giữ thứ tự FIFO: khách đến trước được xử lý trước.
  • Thêm ở cuối, lấy ở đầu; không nhầm với stack lấy ở cùng một đầu.
  • List Python có pop(0) nhưng chi phí tuyến tính; dùng deque.popleft() cho hàng đợi.

Queue là gì và khác Stack ở đâu?

Hãy hình dung ba yêu cầu được gửi đến theo thứ tự 10, 20, 30. Hệ thống đang xử lý yêu cầu lâu nhất còn chờ, nên lấy 10 trước, sau đó mới tới 20 và 30. Nếu yêu cầu 40 đến trong lúc đó, nó đứng cuối hàng. Nguyên tắc này là FIFO, viết đầy đủ là First In, First Out, tức vào trước ra trước.

Với ngăn xếp, phần tử đến sau lại được lấy trước. Vì vậy queue phù hợp cho xử lý theo lượt, còn stack phù hợp cho công việc lồng nhau hoặc quay lui. Cả hai là quy tắc truy cập dữ liệu, không bắt buộc phải có một cách lưu vật lý duy nhất. Bạn có thể xây queue bằng danh sách liên kết hoặc mảng vòng.

Thêm cuối 10 → 20 → 30 đầu: 10 Lấy đầu 20 → 30 lấy ra: 10 Thêm tiếp 20 → 30 → 40 đầu vẫn là 20
Thêm cuối 10 → 20 → 30 đầu: 10 Lấy đầu 20 → 30 lấy ra: 10 Thêm tiếp 20 → 30 → 40 đầu vẫn là 20
Ba trạng thái của hàng đợi: thêm 10, 20, 30; lấy 10; rồi thêm 40. Đầu hàng lúc cuối là 20.

Thao tác trong C++ và Python

Trong C++, khai báo queue<int> q với header <queue>. q.push(x) thêm cuối, q.front() xem đầu, q.back() xem cuối và q.pop() xóa đầu. Lưu ý pop() không trả về giá trị. Nếu cần dùng phần tử đầu, đọc front() trước rồi mới pop(). Luôn kiểm empty() trước hai thao tác này.

Trong Python, tạo q = deque() sau khi nhập deque từ collections. Thêm bằng append(x), lấy đầu bằng popleft(), xem đầu bằng q[0] và kiểm rỗng bằng if q. popleft() vừa xóa vừa trả phần tử nên cách viết khác C++. Đây là khác biệt cú pháp cần nhớ khi chuyển lời giải giữa hai ngôn ngữ.

Việc cần làm C++ Python
Thêm cuối q.push(x) q.append(x)
Xem đầu q.front() q[0]
Xóa đầu q.pop() q.popleft()
Đếm phần tử q.size() len(q)

Với các bộ chứa mặc định này và kiểu số đơn giản, thao tác ở hai đầu có chi phí hằng số theo mô hình thường dùng. Truy cập một chỉ số giữa deque Python không có bảo đảm O(1), vì vậy đừng dùng nó thay list cho bài cần đọc ngẫu nhiên khắp dãy.

Sinh các số nhị phân theo lượt

Bắt đầu hàng đợi chứa xâu 1. Mỗi lượt lấy xâu đầu s, ghi nó vào kết quả, rồi đưa s + "0" và s + "1" vào cuối. Sau lượt đầu, hàng chờ là 10, 11. Sau lượt thứ hai, nó là 11, 100, 101. Những xâu ngắn hơn được lấy trước, còn trong cùng độ dài thì nhánh 0 được xử lý trước nhánh 1.

#include <iostream>
#include <queue>
#include <string>
#include <vector>

using namespace std;

vector<string> sinh_nhi_phan(int n) {
    vector<string> ket_qua;
    if (n == 0) return ket_qua;
    ket_qua.reserve(n);

    queue<string> q;
    q.push("1");

    for (int buoc = 0; buoc != n; ++buoc) {
        string hien_tai = q.front();
        q.pop();
        ket_qua.push_back(hien_tai);

        q.push(hien_tai + "0");
        q.push(hien_tai + "1");
    }
    return ket_qua;
}

int main() {
    vector<string> ds = sinh_nhi_phan(7);
    bool dau = true;
    for (const string& s : ds) {
        if (!dau) cout << ' ';
        dau = false;
        cout << s;
    }
    cout << '\n';
    return 0;
}

Chạy tệp ví dụ cho bảy số nhận được 1 10 11 100 101 110 111, là biểu diễn nhị phân của các số từ 1 tới 7. Dữ liệu được lưu dưới dạng xâu, nên phép nối tạo thêm ký tự chứ không phải cộng số nguyên. Khi yêu cầu n kết quả, tổng số ký tự cần tạo và lưu có cận O(n log(n+1)); không nên ghi đơn giản O(n) rồi bỏ qua độ dài xâu.

Ý tưởng xử lý từng lớp này còn xuất hiện trong BFS, tức duyệt theo chiều rộng. Trong bài lưới, hàng đợi thường lưu tọa độ hoặc chỉ số đỉnh thay vì xâu. Đánh dấu đỉnh đã đưa vào hàng chờ giúp tránh đưa lại cùng đỉnh nhiều lần.

Học cùng giảng viên

Chủ đề này nằm trong Lecture 06 - Stack & Queue của Khóa Blue 2.0. Xem đề cương

Hàng đợi vòng: tái sử dụng các ô đã lấy

Giả sử vùng chứa có đúng ba ô. Bạn thêm 10, 20, 30 rồi lấy 10. Nếu chỉ tăng mãi chỉ số cuối, vùng phía sau sớm hết dù ô đầu đã trống. Mảng vòng giải quyết bằng phép chia dư: chỉ số kế tiếp là (chi_so + 1) % suc_chua.

Code dùng ba biến: dau_hang chỉ phần tử sắp lấy, cuoi_hang chỉ ô sẽ ghi tiếp, và so_luong phân biệt trạng thái đầy với rỗng. Khi đầy, thao tác thêm trả false; khi rỗng, thao tác xóa cũng trả false. Sau khi bỏ 10 và thêm 40, mảng vật lý có thể là [40, 20, 30], nhưng thứ tự logic vẫn là 20, 30, 40.

#include <iostream>
#include <queue>
#include <stdexcept>
#include <vector>

using namespace std;

class HangDoiVong {
private:
    vector<int> a;
    int dau_hang;
    int cuoi_hang;
    int so_luong;
    int suc_chua;

public:
    explicit HangDoiVong(int k)
        : a(k, 0), dau_hang(0), cuoi_hang(0), so_luong(0), suc_chua(k) {}

    bool day_vao(int x) {
        if (so_luong == suc_chua) return false;
        a[cuoi_hang] = x;
        cuoi_hang = (cuoi_hang + 1) % suc_chua;
        ++so_luong;
        return true;
    }

    bool lay_ra() {
        if (so_luong == 0) return false;
        dau_hang = (dau_hang + 1) % suc_chua;
        --so_luong;
        return true;
    }

    int xem_dau() const {
        return so_luong == 0 ? -1 : a[dau_hang];
    }

    int xem_cuoi() const {
        if (so_luong == 0) return -1;
        int vi_tri_cuoi = (cuoi_hang - 1 + suc_chua) % suc_chua;
        return a[vi_tri_cuoi];
    }
};

int vong_tron_josephus(int n, int k) {
    if (n <= 0 || k <= 0) throw invalid_argument("n va k phai duong");
    queue<int> q;
    for (int i = 1; i != n + 1; ++i) {
        q.push(i);
    }
    while (q.size() != 1) {
        for (int dem = 1; dem != k; ++dem) {
            q.push(q.front());
            q.pop();
        }
        q.pop(); // Loại người thứ k
    }
    return q.front();
}

int main() {
    HangDoiVong cq(3);
    cq.day_vao(10);
    cq.day_vao(20);
    cq.day_vao(30);
    cq.lay_ra();
    cq.day_vao(40); // Tái sử dụng ô nhớ chỉ số 0 nhờ phép chia dư % 3

    cout << cq.xem_dau() << ' ' << cq.xem_cuoi() << ' '
         << vong_tron_josephus(7, 3) << '\n';
    return 0;
}

Đầu ra ví dụ là 20 40 4. Hai số đầu là đầu và cuối hàng sau các thao tác trên. Số cuối là người còn lại trong Josephus với n = 7, k = 3. Lớp minh họa dùng -1 để báo hàng rỗng khi xem đầu/cuối; với hệ thống cho phép lưu -1, nên trả thêm cờ thành công hoặc kiểu tùy chọn để phân biệt.

Josephus: xoay hàng đợi để đếm vòng

Xếp người 1 tới n vào hàng. Mỗi lượt chuyển k−1 người ở đầu xuống cuối, rồi loại người đầu tiếp theo. Với n = 7 và k = 3, thứ tự bị loại là 3, 6, 2, 7, 5, 1; người 4 còn lại. Phần kiểm đi kèm đối chiếu cách mô phỏng với công thức truy hồi Josephus trên nhiều đầu vào nhỏ.

Cách mô phỏng này tốn O(nk) thao tác trong trường hợp k được dùng nguyên vẹn mỗi lượt, bộ nhớ O(n). Nó dễ hiểu nhưng không phù hợp khi k rất lớn. Nếu đề chỉ hỏi người cuối, dùng truy hồi f(i) = (f(i−1) + k) % i với chỉ số từ 0 để đạt O(n) thời gian và O(1) bộ nhớ. Hàm ví dụ yêu cầu n và k dương.

Lỗi thường gặp và cách tự kiểm

Cẩn thận
  • Dùng list.pop(0) lặp nhiều lần làm tổng chi phí có thể thành O(n²).
  • Với mảng vòng, quên kiểm đầy/rỗng trước khi tính hoặc truy cập chỉ số.
  • Dùng chung một chỉ số đầu/cuối mà không có quy ước phân biệt đầy và rỗng.
  • Đánh giá thời gian chỉ theo số lần lấy xâu, bỏ qua chi phí sao chép xâu.

Hãy thử hàng rỗng, một phần tử, đầy sức chứa, rồi lặp thêm–lấy qua nhiều vòng. Với Josephus, k = 1 phải để người n sống sót. Với sinh nhị phân, n = 0 phải cho danh sách rỗng. Những ca nhỏ này thường phát hiện nhầm thứ tự trước khi bạn thử dữ liệu lớn.

Luyện ngay trên Big-O Coding

Hai bài phục vụ khách hàng luyện FIFO trực tiếp. Bài ma trận khoảng cách miễn phí mở rộng sang BFS: mỗi ô mới được đưa vào hàng để xử lý theo lớp.

Câu hỏi thường gặp

Queue có tự chọn việc quan trọng nhất không?

Không. Queue thường chọn việc đến trước. Muốn chọn theo độ ưu tiên, dùng hàng đợi ưu tiên; quy tắc lấy phần tử đã thay đổi.

Queue có bắt buộc giới hạn kích thước không?

Không. Mảng vòng trong bài có sức chứa cố định để minh họa. Bộ chứa thư viện có thể tăng theo nhu cầu và giới hạn bộ nhớ.

Khi nào dùng queue trong bài đồ thị?

Khi xử lý đỉnh theo từng lớp như BFS, hoặc duy trì các công việc sẵn sàng trong thuật toán Kahn. Điều kiện đưa phần tử vào hàng vẫn do từng thuật toán quyết định.

Muốn học bài bản cùng giảng viên?

Luyện phỏng vấn Google, Meta, Amazon theo Interview Patterns: 16 buổi online live, lớp 7–15 người, C++/Python/Java. Học thử buổi 1 miễn phí.

Xem Khóa Blue 2.0

Nguồn tham khảo

Chia sẻ: Facebook LinkedIn
Nhắn Zalo tư vấnZalo Gọi 0937 401 483