Hàng đợi (queue)
Cập nhật lần cuối
Hàng đợi (queue) có hai đầu cùng hoạt động. Giá trị mới vào ở cuối, còn giá trị rời đi từ đầu, nên thứ nào chờ lâu nhất sẽ được phục vụ trước. Đó là FIFO, và cũng chính là cách một hàng người xếp trước quầy vận hành: vào ở cuối và được phục vụ từ đầu là điều khiến việc chờ đợi công bằng. Nhấn Phát ở trên và xem các giá trị đi vào ở một phía và rời đi ở phía kia.
Vì mỗi đầu được theo dõi bằng chỉ số hoặc con trỏ riêng, cả hai thao tác đều tốn O(1) và không thao tác nào phải dời phần dữ liệu còn lại. Đó là lý do hàng đợi nằm bên dưới mọi thứ xử lý công việc theo thứ tự đến: lệnh in, hàng đợi tác vụ và tin nhắn, bộ đệm request, và tìm kiếm theo chiều rộng, thuật toán duyệt đồ thị theo từng tầng chính vì nó giữ biên duyệt trong một hàng đợi. Chuyển đầu lấy ra về phía cuối và bạn có một ngăn xếp.
Độ phức tạp thời gian và không gian
Với hàng đợi dùng bộ đệm vòng (ring buffer) hoặc danh sách liên kết, hai cách cài đặt tiêu chuẩn:
| Thao tác | Độ phức tạp | Ghi chú |
|---|---|---|
| Enqueue | O(1) | Ghi vào cuối và tăng chỉ số cuối. |
| Dequeue | O(1) | Đọc ở đầu và tăng chỉ số đầu, không phải dời phần tử. |
| Peek (đầu) | O(1) | Đọc giá trị ở đầu mà không lấy nó ra. |
| Tìm kiếm | O(n) | Không phải việc của hàng đợi: bạn phải lấy hết ra mới xem được bên trong. |
| Không gian | O(n) | Mỗi giá trị đang chờ chiếm một ô. |
Từng bước
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Hàng đợi bắt đầu rỗng, đầu và cuối cùng trỏ vào một ô. |
| 2 | Enqueue ghi giá trị vào cuối, rồi tăng chỉ số cuối thêm một. |
| 3 | Mỗi lần enqueue tiếp theo đặt giá trị phía sau các giá trị đang chờ. |
| 4 | Dequeue đọc giá trị ở đầu, rồi tăng chỉ số đầu thêm một. |
| 5 | Giá trị được trả về luôn là giá trị đã chờ lâu nhất. |
| 6 | Khi đầu gặp cuối, hàng đợi lại rỗng, và dequeue thêm nữa sẽ gây lỗi. |
Ví dụ minh họa
Enqueue 3, 7, 5 rồi lấy hết hàng đợi ra:
| Thao tác | Hàng đợi (từ đầu đến cuối) | Trả về |
|---|---|---|
enqueue(3) | [3] | không có gì |
enqueue(7) | [3, 7] | không có gì |
enqueue(5) | [3, 7, 5] | không có gì |
dequeue() | [7, 5] | 3, giá trị cũ nhất |
dequeue() | [5] | 7 |
dequeue() | [] | 5, giá trị mới nhất, ra sau cùng |
Khi nào nên dùng hàng đợi
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Công việc phải được xử lý theo thứ tự đến: hàng đợi tác vụ, bộ đệm request, bộ đệm lệnh in | Bạn cần phần tử mới nhất trước, đó là việc của ngăn xếp |
| Bạn duyệt theo từng tầng, như tìm kiếm theo chiều rộng | Các phần tử phải được phục vụ theo độ ưu tiên thay vì thứ tự đến, khi đó heap phù hợp hơn |
| Bên sản xuất và bên tiêu thụ chạy ở tốc độ khác nhau và cần một bộ đệm ở giữa | Bạn cần tìm kiếm hoặc truy cập theo chỉ số vào giữa dữ liệu |
Bạn muốn chèn và lấy ra trong O(1) mà không phải dời phần tử | Bạn định cài đặt nó bằng cách dời mảng mỗi lần dequeue, khiến nó thành O(n) |
Code Queue
Một cách cài đặt Queue gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Queue bằng Python
1from collections import deque2
3queue = deque()4
5# Enqueue three values at the rear6for value in [3, 7, 5]:7 queue.append(value)8 print(f"enqueue {value} -> {list(queue)}")9
10# Dequeue them from the front: first in, first out11while queue:12 value = queue.popleft()13 print(f"dequeue {value} -> {list(queue)}")14
15print("empty:", len(queue) == 0)Code Queue bằng JavaScript
1// A plain array makes dequeue O(n): shift() moves every element left.2// Track a head index instead, the fix the queue article describes.3const queue = { items: [], head: 0 };4
5function enqueue(value) {6 queue.items.push(value);7}8
9function dequeue() {10 const value = queue.items[queue.head];11 queue.items[queue.head] = undefined; // free the slot12 queue.head += 1;13 // Reclaim space once the consumed prefix dominates.14 if (queue.head * 2 >= queue.items.length) {15 queue.items = queue.items.slice(queue.head);16 queue.head = 0;17 }18 return value;19}20
21const size = () => queue.items.length - queue.head;22
23for (const value of [3, 7, 5]) {24 enqueue(value);25 console.log(`enqueue ${value} -> size ${size()}`);26}27
28// Dequeue from the front: first in, first out, amortized O(1)29while (size() > 0) {30 console.log(`dequeue ${dequeue()} -> size ${size()}`);31}32
33console.log('empty:', size() === 0);Code Queue bằng Java
1import java.util.ArrayDeque;2import java.util.Queue;3
4public class Main {5 public static void main(String[] args) {6 Queue<Integer> queue = new ArrayDeque<>();7
8 // Enqueue three values at the rear9 for (int value : new int[] {3, 7, 5}) {10 queue.add(value);11 System.out.println("enqueue " + value + " -> " + queue);12 }13
14 // Dequeue them from the front: first in, first out15 while (!queue.isEmpty()) {16 int value = queue.remove();17 System.out.println("dequeue " + value + " -> " + queue);18 }19
20 System.out.println("empty: " + queue.isEmpty());21 }22}Code Queue bằng C++
1#include <iostream>2#include <queue>3
4int main() {5 std::queue<int> queue;6
7 // Enqueue three values at the rear8 for (int value : {3, 7, 5}) {9 queue.push(value);10 std::cout << "enqueue " << value << " -> size " << queue.size() << "\n";11 }12
13 // Dequeue them from the front: first in, first out14 while (!queue.empty()) {15 int value = queue.front();16 queue.pop();17 std::cout << "dequeue " << value << " -> size " << queue.size() << "\n";18 }19
20 std::cout << "empty: " << std::boolalpha << queue.empty() << "\n";21 return 0;22}Code Queue bằng C
1#include <stdio.h>2
3#define CAP 164
5int queue[CAP];6int front = 0;7int rear = 0; /* index of the next free slot */8
9int main(void) {10 int values[3] = {3, 7, 5};11
12 /* Enqueue three values at the rear */13 for (int i = 0; i < 3; i++) {14 queue[rear++] = values[i];15 printf("enqueue %d -> size %d\n", values[i], rear - front);16 }17
18 /* Dequeue them from the front: first in, first out */19 while (front < rear) {20 int value = queue[front++];21 printf("dequeue %d -> size %d\n", value, rear - front);22 }23
24 printf("empty: %d\n", front == rear);25 return 0;26}Câu hỏi thường gặp về hàng đợi
FIFO nghĩa là gì?
Hàng đợi và ngăn xếp khác nhau như thế nào?
O(1); hàng đợi lấy ra từ đầu (FIFO), ngăn xếp lấy ra từ chính đầu đã thêm vào (LIFO). Ngoài điểm đó, bảng độ phức tạp của chúng giống hệt nhau.Các thao tác chính trên hàng đợi là gì?
enqueue thêm một giá trị vào cuối, dequeue lấy ra và trả về giá trị ở đầu, peek (hoặc front) đọc giá trị ở đầu mà không lấy ra, và is_empty cho biết còn gì đang chờ không. Cả bốn đều tốn O(1).Tại sao dequeue chậm nếu tôi dùng mảng thường?
O(n). Các cài đặt thực tế tránh điều này bằng bộ đệm vòng có chỉ số đầu tăng dần, hoặc danh sách liên kết có con trỏ head. collections.deque của Python và ArrayDeque của Java làm việc này cho bạn, còn list.pop(0) thì không.Hàng đợi vòng là gì?
n có thể chạy mãi thay vì đi ra ngoài cuối mảng.