Bài 23.122 phút đọc
Hàng đợi cơ bản
Sau bài này bạn sẽ làm được
- Cài enqueue và dequeue trong O(1)
- Xử lý đúng lúc hàng đợi trở thành rỗng
- Giải thích vì sao thêm ở đuôi và lấy ở đầu
- Hủy hàng đợi không rò rỉ
Hàng đợi đảo ngược quy tắc của ngăn xếp: vào trước ra trước. Nó cần hai đầu, nên cần hai con trỏ, và cả hai đều có một trường hợp biên riêng khi hàng đợi trở nên rỗng.
#FIFO và hai đầu
FIFO
First In, First Out. Phần tử vào trước là phần tử ra trước. Như xếp hàng mua vé: ai đến trước được phục vụ trước.
| Ngăn xếp | Hàng đợi | |
|---|---|---|
| Quy tắc | LIFO | FIFO |
| Số đầu phải giữ | 1 | 2 |
| Thêm ở | Đỉnh | Đuôi |
| Lấy ở | Đỉnh | Đầu |
| Dùng cho | Cấu trúc lồng nhau | Xử lý theo thứ tự đến |
| Duyệt cây | Theo chiều sâu | Theo tầng |
| Duyệt đồ thị | DFS | BFS |
#Cài đặt bằng danh sách liên kết
queue.h
#ifndef QUEUE_H
#define QUEUE_H
#include <stddef.h>
typedef struct QNode {
int data;
struct QNode *next;
} QNode;
/* Bất biến:
1. front == NULL khi và chỉ khi size == 0
2. rear == NULL khi và chỉ khi front == NULL
3. rear != NULL thì rear->next == NULL
4. size == số nút đi được từ front theo next */
typedef struct {
QNode *front; /* nút sẽ được lấy ra tiếp theo */
QNode *rear; /* nút vừa được thêm vào */
size_t size;
} Queue;
void queue_khoi_tao(Queue *q);
void queue_huy(Queue *q);
int queue_rong(const Queue *q);
size_t queue_so_phan_tu(const Queue *q);
int queue_enqueue(Queue *q, int v);
int queue_dequeue(Queue *q, int *out);
int queue_xem_dau(const Queue *q, int *out);
#endifqueue.c
#include <stdlib.h>
#include "queue.h"
void queue_khoi_tao(Queue *q)
{
q->front = NULL;
q->rear = NULL;
q->size = 0;
}
int queue_rong(const Queue *q)
{
return q->front == NULL;
}
size_t queue_so_phan_tu(const Queue *q)
{
return q->size;
}
/* Thêm vào ĐUÔI. O(1). Trả về 0 nếu ổn, -1 nếu hết bộ nhớ. */
int queue_enqueue(Queue *q, int v)
{
QNode *n = malloc(sizeof *n);
if (n == NULL) return -1;
n->data = v;
n->next = NULL;
if (q->rear != NULL) q->rear->next = n; /* nối vào sau nút cuối */
else q->front = n; /* hàng đợi đang rỗng */
q->rear = n;
++q->size;
return 0;
}
/* Lấy ở ĐẦU. O(1). Trả về 0 nếu ổn, -1 nếu rỗng. */
int queue_dequeue(Queue *q, int *out)
{
if (q->front == NULL) return -1;
QNode *n = q->front;
if (out != NULL) *out = n->data;
q->front = n->next;
if (q->front == NULL) q->rear = NULL; /* vừa lấy nút cuối cùng */
free(n);
--q->size;
return 0;
}
int queue_xem_dau(const Queue *q, int *out)
{
if (q->front == NULL) return -1;
if (out != NULL) *out = q->front->data;
return 0;
}
void queue_huy(Queue *q)
{
while (queue_dequeue(q, NULL) == 0) { }
}#Hai trường hợp biên
| Trường hợp | Trong hàm nào | Phải làm gì | Quên thì sao |
|---|---|---|---|
| Thêm vào hàng đợi đang rỗng | enqueue | Gán cả front lẫn rear | front vẫn NULL, hàng đợi mãi rỗng dù size tăng |
| Lấy nút cuối cùng ra | dequeue | Đặt rear về NULL | rear thành con trỏ treo, lần enqueue sau ghi vào vùng đã giải phóng |
Quên đặt rear về NULL
int queue_dequeue(Queue *q, int *out)
{
if (q->front == NULL) return -1;
QNode *n = q->front;
if (out != NULL) *out = n->data;
q->front = n->next;
/* quên: nếu front vừa thành NULL thì rear vẫn trỏ vào n */
free(n);
--q->size;
return 0;
}Đầy đủ
q->front = n->next;
if (q->front == NULL) q->rear = NULL;
free(n);terminal
# Thêm một, lấy một, rồi thêm một nữa
gcc -std=c17 -g -fsanitize=address queue-quen.c -o t-asan && ./t-asan
ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000018
WRITE of size 8 at 0x602000000018 thread T0
#0 in queue_enqueue queue-quen.c:24
0x602000000018 is located 8 bytes inside of 16-byte region
freed by thread T0 here:
#0 in free
#1 in queue_dequeue queue-quen.c:41Chương trình thử
main.c
#include <stdio.h>
#include "queue.h"
int main(void)
{
Queue q;
int v;
queue_khoi_tao(&q);
printf("dequeue tren rong: %d\n", queue_dequeue(&q, &v));
/* Trường hợp biên: thêm một, lấy một, thêm một */
queue_enqueue(&q, 1);
queue_dequeue(&q, &v);
printf("them 1 roi lay ra: %d, size = %zu\n", v, queue_so_phan_tu(&q));
queue_enqueue(&q, 2); /* đây là chỗ rear treo nếu quên đặt NULL */
queue_dequeue(&q, &v);
printf("them 2 roi lay ra: %d\n", v);
for (int i = 1; i <= 5; ++i)
if (queue_enqueue(&q, i * 10) != 0) { queue_huy(&q); return 1; }
queue_xem_dau(&q, &v);
printf("dau hang : %d\n", v);
printf("so phan tu : %zu\n", queue_so_phan_tu(&q));
printf("lay ra : ");
while (queue_dequeue(&q, &v) == 0) printf("%d ", v);
printf("\n");
queue_huy(&q);
return 0;
}terminal
gcc -std=c17 -Wall -Wextra -g queue.c main.c -o t && ./t
dequeue tren rong: -1 them 1 roi lay ra: 1, size = 0 them 2 roi lay ra: 2 dau hang : 10 so phan tu : 5 lay ra : 10 20 30 40 50
# Valgrind chạy trên bản không bật sanitizer
valgrind --leak-check=full ./t
All heap blocks were freed -- no leaks are possible ERROR SUMMARY: 0 errors from 0 contexts
Thứ tự lấy ra đúng bằng thứ tự đưa vào. Đó là toàn bộ nội dung của FIFO, và nó khác hẳn Bài 22.1 nơi thứ tự bị đảo.
#Vì sao mảng ngây thơ không dùng được
Thử cài hàng đợi bằng mảng theo cách trực tiếp nhất: giữ hai chỉ số, thêm ở rear và lấy ở front. Vấn đề lộ ra rất nhanh.
queue-mang-te.c
#define QMAX 100
typedef struct {
int data[QMAX];
size_t front; /* chỉ số phần tử đầu */
size_t rear; /* chỉ số ô trống kế tiếp */
} QueueTe;
int qt_enqueue(QueueTe *q, int v)
{
if (q->rear == QMAX) return -1; /* "đầy" */
q->data[q->rear++] = v;
return 0;
}
int qt_dequeue(QueueTe *q, int *out)
{
if (q->front == q->rear) return -1; /* rỗng */
*out = q->data[q->front++];
return 0;
}terminal
./queue-mang-te
them 100 phan tu, lay ra 100 phan tu front = 100, rear = 100 so phan tu thuc te = 0 nhung them tiep: -1 (bao DAY du mang trong khong!)
| Cách chữa | Chi phí | Đánh giá |
|---|---|---|
| Dồn mọi phần tử về đầu mảng khi rear chạm biên | O(n) mỗi lần dồn | Chạy được, nhưng có đỉnh nhọn về thời gian |
| Dịch cả mảng mỗi lần dequeue | O(n) mỗi lần lấy | Tệ, biến O(1) thành O(n) |
| Cho chỉ số quay vòng bằng phép chia dư | O(1) mọi lúc | Đúng cách, xem Bài 23.2 |
#Ứng dụng
| Ứng dụng | Cái gì xếp hàng | Vì sao FIFO là đúng |
|---|---|---|
| Duyệt cây theo tầng | Nút chờ được thăm | Nút tầng trên phải được thăm trước tầng dưới |
| BFS trên đồ thị | Đỉnh chờ được thăm | Bảo đảm tìm được đường đi ngắn nhất theo số cạnh |
| Bộ lập lịch tiến trình | Tiến trình sẵn sàng chạy | Công bằng, ai chờ lâu được chạy trước |
| Hàng đợi in ấn | Tài liệu chờ in | Người gửi trước được in trước |
| Bộ đệm giữa hai luồng | Dữ liệu bên sản xuất tạo ra | Bên tiêu thụ xử lý đúng thứ tự |
| Hàng đợi sự kiện giao diện | Sự kiện chuột và bàn phím | Sự kiện phải xử lý theo đúng thứ tự xảy ra |
Duyệt cây theo tầng, xem trước Bài 24.2
theo-tang.c
#include <stdio.h>
#include "queue-ptr.h" /* hàng đợi chứa TNode *, cùng mẫu ở trên */
typedef struct TNode {
int data;
struct TNode *left, *right;
} TNode;
/* In cây theo từng tầng từ trên xuống, trái sang phải. */
void theo_tang(TNode *goc)
{
if (goc == NULL) return;
QueueP q;
queue_khoi_tao(&q);
if (queue_enqueue(&q, goc) != 0) return;
while (!queue_rong(&q)) {
TNode *n;
queue_dequeue(&q, &n);
printf("%d ", n->data);
if (n->left != NULL) queue_enqueue(&q, n->left);
if (n->right != NULL) queue_enqueue(&q, n->right);
}
printf("\n");
queue_huy(&q);
}terminal
./theo-tang
cay:
50
/ \
30 70
/ \ / \
20 40 60 80
theo tang: 50 30 70 20 40 60 80Tự làm thử
- Cài
Queueđầy đủ và chạy dãy thao tác thêm một, lấy một, thêm một dưới-fsanitize=addressđể kiểm bẫyreartreo. - Bỏ dòng đặt
rearvề NULL, rồi tìm dãy thao tác ngắn nhất làm chương trình sai. - Cài bản có nút canh và đếm số nhánh
ifcủa hai bản. - Cài hàng đợi mảng ngây thơ, thêm và lấy đủ
QMAXlần, rồi xác nhận nó báo đầy dù mảng trống. - Cài duyệt cây theo tầng có xuống dòng giữa các tầng, dùng mẹo chốt số nút của tầng trước vòng lặp trong.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Hàng đợi là FIFO, cần hai con trỏ:
frontđể lấy vàrearđể thêm. - Thêm phải ở đuôi và lấy phải ở đầu, vì với danh sách đơn thì xóa ở cuối là O(n).
- Hai trường hợp biên: thêm vào hàng đợi rỗng phải gán cả
front, và lấy nút cuối cùng phải đặtrearvề NULL. - Hàng đợi mảng với hai chỉ số chỉ đi tới sẽ lãng phí toàn bộ phần đầu mảng. Lời giải là cho chỉ số quay vòng.
- Đổi hàng đợi thành ngăn xếp trong một thuật toán duyệt là đổi từ duyệt theo tầng sang duyệt theo chiều sâu.