Hàng đợi vòng
Sau bài này bạn sẽ làm được
- Cài hàng đợi vòng bằng cặp front và size
- Phân biệt rỗng và đầy mà không cần hy sinh một ô
- Thay chia dư bằng phép và bit khi kích thước là lũy thừa của hai
- Nêu ứng dụng trong hệ nhúng
Bài 23.1 kết thúc với một hàng đợi mảng báo đầy dù mảng trống. Lời giải chỉ là một phép chia dư, nhưng đi kèm một câu hỏi tinh tế: làm sao phân biệt hàng đợi rỗng với hàng đợi đầy khi hai chỉ số trùng nhau.
#Chỉ số quay vòng
/* Không quay vòng: chỉ số chỉ tăng, chạm biên là hết */
q->data[q->rear++] = v;
/* Quay vòng: chỉ số quay lại 0 sau khi vượt quá cuối mảng */
q->data[(q->front + q->size) % QMAX] = v;| Thao tác | front | size | Ô được ghi hoặc đọc |
|---|---|---|---|
| Khởi tạo | 0 | 0 | - |
| enqueue 10 | 0 | 1 | (0 + 0) % 8 = 0 |
| enqueue 20 | 0 | 2 | (0 + 1) % 8 = 1 |
| dequeue -> 10 | 1 | 1 | 0 |
| dequeue -> 20 | 2 | 0 | 1 |
| ... sau nhiều lần, front = 6 | 6 | 0 | - |
| enqueue 30 | 6 | 1 | (6 + 0) % 8 = 6 |
| enqueue 40 | 6 | 2 | (6 + 1) % 8 = 7 |
| enqueue 50 | 6 | 3 | (6 + 2) % 8 = 0 quay vòng |
#Phân biệt rỗng với đầy
Nếu giữ hai chỉ số front và rear thì cả hàng đợi rỗng lẫn hàng đợi đầy đều cho front == rear. Có ba cách chữa.
| Cách | Rỗng khi | Đầy khi | Đánh giá |
|---|---|---|---|
| Hy sinh một ô, không bao giờ dùng hết mảng | front == rear | (rear + 1) % N == front | Đơn giản, mất một ô |
| Giữ thêm trường size | size == 0 | size == N | Rõ ràng nhất, tốn thêm một trường |
| Giữ thêm một cờ đầy | front == rear và cờ tắt | front == rear và cờ bật | Phải nhớ bật tắt cờ ở mọi chỗ, dễ sai |
typedef struct {
int data[QMAX];
size_t front, rear;
} CQTe;
/* front == rear ở CẢ HAI trạng thái, không phân biệt được */
int cq_rong(const CQTe *q) { return q->front == q->rear; }
int cq_day (const CQTe *q) { return q->front == q->rear; } /* vô nghĩa */typedef struct {
int data[QMAX];
size_t front; /* chỉ số phần tử đầu */
size_t size; /* số phần tử đang có */
} CQ;
int cq_rong(const CQ *q) { return q->size == 0; }
int cq_day (const CQ *q) { return q->size == QMAX; }#Cài đặt đầy đủ
#ifndef CQUEUE_H
#define CQUEUE_H
#include <stddef.h>
#define QMAX 8
/* Bất biến:
1. 0 <= front < QMAX
2. 0 <= size <= QMAX
3. Phần tử thứ i tính từ đầu nằm ở data[(front + i) % QMAX] */
typedef struct {
int data[QMAX];
size_t front;
size_t size;
} CQueue;
void cq_khoi_tao(CQueue *q);
int cq_rong(const CQueue *q);
int cq_day(const CQueue *q);
size_t cq_so_phan_tu(const CQueue *q);
int cq_enqueue(CQueue *q, int v);
int cq_dequeue(CQueue *q, int *out);
int cq_xem_dau(const CQueue *q, int *out);
void cq_in(const CQueue *q);
#endif#include <stdio.h>
#include "cqueue.h"
void cq_khoi_tao(CQueue *q)
{
q->front = 0;
q->size = 0;
}
int cq_rong(const CQueue *q) { return q->size == 0; }
int cq_day (const CQueue *q) { return q->size == QMAX; }
size_t cq_so_phan_tu(const CQueue *q) { return q->size; }
/* Thêm vào đuôi. O(1). Trả về 0 nếu ổn, -1 nếu đầy. */
int cq_enqueue(CQueue *q, int v)
{
if (q->size == QMAX) return -1;
q->data[(q->front + q->size) % QMAX] = v;
++q->size;
return 0;
}
/* Lấy ở đầu. O(1). Trả về 0 nếu ổn, -1 nếu rỗng. */
int cq_dequeue(CQueue *q, int *out)
{
if (q->size == 0) return -1;
if (out != NULL) *out = q->data[q->front];
q->front = (q->front + 1) % QMAX;
--q->size;
return 0;
}
int cq_xem_dau(const CQueue *q, int *out)
{
if (q->size == 0) return -1;
if (out != NULL) *out = q->data[q->front];
return 0;
}
/* In cả trạng thái bên trong để thấy chỉ số quay vòng. */
void cq_in(const CQueue *q)
{
printf("front=%zu size=%zu [", q->front, q->size);
for (size_t i = 0; i < q->size; ++i) {
printf("%d", q->data[(q->front + i) % QMAX]);
if (i + 1 < q->size) printf(", ");
}
printf("] o nho: ");
for (size_t k = 0; k < QMAX; ++k) {
int dang_dung = 0;
for (size_t i = 0; i < q->size; ++i)
if ((q->front + i) % QMAX == k) dang_dung = 1;
if (dang_dung) printf("%d ", q->data[k]);
else printf(". ");
}
printf("\n");
}#include <stdio.h>
#include "cqueue.h"
int main(void)
{
CQueue q;
int v;
cq_khoi_tao(&q);
for (int i = 1; i <= 6; ++i) cq_enqueue(&q, i * 10);
cq_in(&q);
for (int i = 0; i < 5; ++i) cq_dequeue(&q, &v);
cq_in(&q);
/* Thêm tiếp, chỉ số sẽ quay vòng qua đầu mảng */
for (int i = 1; i <= 6; ++i) cq_enqueue(&q, i * 100);
cq_in(&q);
printf("day chua : %d\n", cq_day(&q));
printf("them nua : %d\n", cq_enqueue(&q, 999));
printf("lay het : ");
while (cq_dequeue(&q, &v) == 0) printf("%d ", v);
printf("\n");
cq_in(&q);
return 0;
}front=0 size=6 [10, 20, 30, 40, 50, 60] o nho: 10 20 30 40 50 60 . . front=5 size=1 [60] o nho: . . . . . 60 . . front=5 size=7 [60, 100, 200, 300, 400, 500, 600] o nho: 300 400 500 . . 60 100 200 day chua : 0 them nua : 0 lay het : 60 100 200 300 400 500 600 999 front=0 size=0 [] o nho: . . . . . . . .
#Thay chia dư bằng phép và bit
Phép chia dư là một trong những lệnh chậm nhất của bộ xử lý, thường hai mươi tới bốn mươi chu kỳ. Khi kích thước mảng là lũy thừa của hai thì thay được bằng phép và bit, chỉ một chu kỳ.
/* Với N là lũy thừa của 2:
x % N == x & (N - 1) khi x không âm
Ví dụ N = 8, tức N - 1 = 7 = 0b111:
13 % 8 = 5 13 & 7 = 0b1101 & 0b0111 = 0b101 = 5
16 % 8 = 0 16 & 7 = 0b10000 & 0b00111 = 0 */
#define QMAX 8
#define QMASK (QMAX - 1)
_Static_assert((QMAX & QMASK) == 0, "QMAX phai la luy thua cua 2");
int cq_enqueue(CQueue *q, int v)
{
if (q->size == QMAX) return -1;
q->data[(q->front + q->size) & QMASK] = v;
++q->size;
return 0;
}chia du (%) : 0.412 s va bit (&) : 0.198 s nhanh hon : 2.1 lan
#Bộ đệm vòng trong hệ nhúng
Hàng đợi vòng là cấu trúc dữ liệu được dùng nhiều nhất trong lập trình nhúng, vì nó có ba tính chất mà hệ nhúng cần: kích thước cố định biết trước, không cấp phát động, và mọi thao tác là O(1) với thời gian không đổi.
| Nơi dùng | Ai ghi vào | Ai đọc ra |
|---|---|---|
| Bộ đệm nhận của cổng nối tiếp | Trình xử lý ngắt khi có byte tới | Vòng lặp chính khi rảnh |
| Bộ đệm âm thanh | Chương trình sinh mẫu | Bộ chuyển đổi số sang tương tự, theo ngắt định thời |
| Hàng đợi sự kiện | Nhiều nguồn ngắt | Vòng lặp xử lý sự kiện |
| Nhật ký vòng | Mọi nơi trong chương trình | Đọc ra khi gỡ lỗi, ghi đè bản ghi cũ nhất khi đầy |
#include <stdint.h>
#define RX_SIZE 256
#define RX_MASK (RX_SIZE - 1)
_Static_assert((RX_SIZE & RX_MASK) == 0, "RX_SIZE phai la luy thua cua 2");
/* Một bên ghi, một bên đọc. Với đúng một người ghi và đúng một người đọc,
cấu trúc này an toàn mà KHÔNG cần khóa, nếu hai chỉ số là nguyên tử. */
static volatile uint8_t rx_dem[RX_SIZE];
static volatile uint16_t rx_ghi; /* chỉ trình xử lý ngắt sửa */
static volatile uint16_t rx_doc; /* chỉ vòng lặp chính sửa */
/* Gọi từ trình xử lý ngắt. Trả về 0 nếu chứa được, -1 nếu đầy. */
int rx_them(uint8_t b)
{
uint16_t ke = (uint16_t)((rx_ghi + 1) & RX_MASK);
if (ke == rx_doc) return -1; /* đầy, hy sinh một ô để phân biệt */
rx_dem[rx_ghi] = b;
rx_ghi = ke; /* cập nhật SAU khi ghi dữ liệu */
return 0;
}
/* Gọi từ vòng lặp chính. Trả về 0 nếu lấy được, -1 nếu rỗng. */
int rx_lay(uint8_t *ra)
{
if (rx_doc == rx_ghi) return -1; /* rỗng */
*ra = rx_dem[rx_doc];
rx_doc = (uint16_t)((rx_doc + 1) & RX_MASK);
return 0;
}Nhật ký vòng: ghi đè bản ghi cũ nhất
/* Với nhật ký, đầy không nên là lỗi. Ghi đè bản ghi cũ nhất
hợp lý hơn, vì thông tin gần đây quan trọng hơn thông tin cũ. */
void log_them(CQueue *q, int v)
{
if (q->size == QMAX) {
q->data[q->front] = v; /* ghi đè ô cũ nhất */
q->front = (q->front + 1) % QMAX; /* đẩy front lên */
} else {
q->data[(q->front + q->size) % QMAX] = v;
++q->size;
}
}them 1..12 vao bo dem 8 o: [5, 6, 7, 8, 9, 10, 11, 12] bon ban ghi dau tien da bi ghi de
Tự làm thử
- Cài
CQueuevớifrontvàsize, kèm hàmcq_inhiện cả trạng thái bộ nhớ để thấy chỉ số quay vòng. - Cài bản hy sinh một ô, rồi so số phần tử tối đa chứa được của hai bản với cùng
QMAX. - Đổi
QMAXthành 100 trong bản dùng phép và bit, xác nhận_Static_assertchặn được lúc biên dịch. - Đo thời gian mười triệu lần enqueue và dequeue với
%và với&, kích thước truyền qua biến để trình biên dịch không tự tối ưu. - Cài nhật ký vòng ghi đè bản ghi cũ nhất, rồi thêm hàm đọc ra theo thứ tự từ cũ tới mới.
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 vòng lấy dư chỉ số cho kích thước mảng, nên không ô nào bị bỏ phí.
- Giữ
frontvàsizelà cách rõ ràng nhất để phân biệt rỗng với đầy, và không mất ô nào. - Duyệt phải theo chỉ số logic rồi ánh xạ sang vật lý bằng
(front + i) % N, không được duyệt thẳng theo chỉ số mảng. - Với kích thước là lũy thừa của hai thì
% Nthay được bằng& (N - 1). Đặt_Static_assertđể chặn kích thước sai. - Trong bối cảnh có ngắt, hai chỉ số độc lập an toàn hơn một trường
sizemà cả hai bên cùng sửa.