Bài 23.320 phút đọc
Deque
Sau bài này bạn sẽ làm được
- Cài bốn thao tác của deque
- So sánh cài bằng danh sách đôi và bằng mảng vòng
- Dùng deque giải bài cửa sổ trượt lớn nhất
- Thấy deque thay được cả ngăn xếp lẫn hàng đợi
Deque cho phép thêm và lấy ở cả hai đầu, tất cả trong O(1). Nó là cấu trúc tổng quát hơn cả ngăn xếp lẫn hàng đợi, và nó giải được một lớp bài toán mà hai cấu trúc kia bó tay: cửa sổ trượt.
#Bốn thao tác
Deque
Double-Ended Queue, hàng đợi hai đầu. Thêm và lấy được ở cả đầu lẫn cuối, mỗi thao tác O(1).
int deque_them_dau (Deque *d, int v);
int deque_them_cuoi(Deque *d, int v);
int deque_lay_dau (Deque *d, int *out);
int deque_lay_cuoi (Deque *d, int *out);
/* Cộng ba hàm hỏi trạng thái */
int deque_rong(const Deque *d);
size_t deque_so_phan_tu(const Deque *d);
int deque_xem_dau(const Deque *d, int *out);
int deque_xem_cuoi(const Deque *d, int *out);| Ngăn xếp | Hàng đợi | Deque | |
|---|---|---|---|
| Thêm đầu | Không | Không | O(1) |
| Thêm cuối | O(1) | O(1) | O(1) |
| Lấy đầu | Không | O(1) | O(1) |
| Lấy cuối | O(1) | Không | O(1) |
| Truy cập phần tử thứ i | Không | Không | O(1) nếu cài bằng mảng |
#Cài bằng mảng vòng
Bài 23.2 đã có gần hết. Chỉ cần thêm phép thêm ở đầu, tức lùi front một bước theo vòng.
deque.h
#ifndef DEQUE_H
#define DEQUE_H
#include <stddef.h>
#define DMAX 8
typedef struct {
int data[DMAX];
size_t front;
size_t size;
} Deque;
void dq_khoi_tao(Deque *d);
int dq_rong(const Deque *d);
int dq_day(const Deque *d);
size_t dq_so_phan_tu(const Deque *d);
int dq_them_dau(Deque *d, int v);
int dq_them_cuoi(Deque *d, int v);
int dq_lay_dau(Deque *d, int *out);
int dq_lay_cuoi(Deque *d, int *out);
int dq_tai(const Deque *d, size_t i, int *out);
#endifdeque.c
#include "deque.h"
void dq_khoi_tao(Deque *d) { d->front = 0; d->size = 0; }
int dq_rong(const Deque *d) { return d->size == 0; }
int dq_day (const Deque *d) { return d->size == DMAX; }
size_t dq_so_phan_tu(const Deque *d) { return d->size; }
/* Thêm vào cuối: y hệt enqueue của Bài 23.2. */
int dq_them_cuoi(Deque *d, int v)
{
if (d->size == DMAX) return -1;
d->data[(d->front + d->size) % DMAX] = v;
++d->size;
return 0;
}
/* Thêm vào đầu: LÙI front một bước theo vòng, rồi ghi vào đó. */
int dq_them_dau(Deque *d, int v)
{
if (d->size == DMAX) return -1;
d->front = (d->front + DMAX - 1) % DMAX; /* lùi một, không dùng số âm */
d->data[d->front] = v;
++d->size;
return 0;
}
/* Lấy ở đầu: y hệt dequeue. */
int dq_lay_dau(Deque *d, int *out)
{
if (d->size == 0) return -1;
if (out != NULL) *out = d->data[d->front];
d->front = (d->front + 1) % DMAX;
--d->size;
return 0;
}
/* Lấy ở cuối: chỉ cần giảm size, front không đổi. */
int dq_lay_cuoi(Deque *d, int *out)
{
if (d->size == 0) return -1;
if (out != NULL) *out = d->data[(d->front + d->size - 1) % DMAX];
--d->size;
return 0;
}
/* Truy cập phần tử thứ i tính từ đầu. O(1). */
int dq_tai(const Deque *d, size_t i, int *out)
{
if (i >= d->size) return -1;
*out = d->data[(d->front + i) % DMAX];
return 0;
}#Cài bằng danh sách đôi
Danh sách đôi ở Bài 21.7 vốn đã cho cả bốn thao tác trong O(1). Chỉ cần đặt tên lại cho hợp ngữ cảnh.
deque-lk.c
#include <stdlib.h>
typedef struct DNode {
int data;
struct DNode *prev, *next;
} DNode;
typedef struct {
DNode *head, *tail;
size_t size;
} DequeLK;
int dql_them_dau(DequeLK *d, int v)
{
DNode *n = malloc(sizeof *n);
if (n == NULL) return -1;
n->data = v;
n->prev = NULL;
n->next = d->head;
if (d->head != NULL) d->head->prev = n;
else d->tail = n;
d->head = n;
++d->size;
return 0;
}
int dql_lay_cuoi(DequeLK *d, int *out)
{
if (d->tail == NULL) return -1;
DNode *n = d->tail;
if (out != NULL) *out = n->data;
d->tail = n->prev;
if (d->tail != NULL) d->tail->next = NULL;
else d->head = NULL;
free(n);
--d->size;
return 0;
}| Mảng vòng | Danh sách đôi | |
|---|---|---|
| Bốn thao tác chính | O(1) | O(1) |
| Truy cập phần tử thứ i | O(1) | O(n) |
| Kích thước | Cố định, hoặc phải cấp lại | Không giới hạn |
| Bộ nhớ mỗi phần tử | 4 byte | 40 byte thực tế |
| Thân thiện bộ nhớ đệm | Rất tốt | Kém |
| Con trỏ tới phần tử có bền không | Không | Có |
| Số dòng cài đặt | Ít hơn | Nhiều hơn |
#Cửa sổ trượt lớn nhất
Cho mảng n số và cửa sổ rộng k. Tìm giá trị lớn nhất trong mỗi cửa sổ khi nó trượt từ trái sang phải. Cách ngây thơ tốn O(n nhân k). Deque cho O(n).
cua-so.c
#include <stdio.h>
#include <stdlib.h>
/* Deque chứa CHỈ SỐ, không chứa giá trị.
Bất biến: các giá trị tại những chỉ số trong deque giảm dần.
Nhờ vậy phần tử đầu deque luôn là chỉ số của giá trị lớn nhất
trong cửa sổ hiện tại. */
int cua_so_lon_nhat(const int *a, size_t n, size_t k, int *ra)
{
if (a == NULL || ra == NULL || k == 0 || k > n) return -1;
size_t *dq = malloc(n * sizeof *dq); /* deque chỉ số, đủ chỗ xấu nhất */
if (dq == NULL) return -1;
size_t dau = 0, cuoi = 0; /* [dau, cuoi) là vùng dùng */
size_t m = 0;
for (size_t i = 0; i < n; ++i) {
/* 1. Bỏ ở ĐẦU những chỉ số đã rơi ra ngoài cửa sổ */
while (dau < cuoi && dq[dau] + k <= i) ++dau;
/* 2. Bỏ ở CUỐI những chỉ số có giá trị nhỏ hơn hoặc bằng a[i].
Chúng không bao giờ còn là lớn nhất nữa, vì a[i] vào sau
và lớn hơn, nên sẽ ở lại lâu hơn. */
while (dau < cuoi && a[dq[cuoi - 1]] <= a[i]) --cuoi;
/* 3. Thêm i vào cuối */
dq[cuoi++] = i;
/* 4. Từ cửa sổ đầy đủ trở đi, ghi kết quả */
if (i + 1 >= k) ra[m++] = a[dq[dau]];
}
free(dq);
return 0;
}main.c
#include <stdio.h>
int cua_so_lon_nhat(const int *a, size_t n, size_t k, int *ra);
int main(void)
{
int a[] = { 1, 3, -1, -3, 5, 3, 6, 7 };
size_t n = sizeof a / sizeof a[0];
size_t k = 3;
int ra[8];
if (cua_so_lon_nhat(a, n, k, ra) != 0) return 1;
printf("mang : ");
for (size_t i = 0; i < n; ++i) printf("%d ", a[i]);
printf("\nk = %zu\n", k);
for (size_t i = 0; i + k <= n; ++i) {
printf("cua so [");
for (size_t j = i; j < i + k; ++j) printf("%d%s", a[j], j + 1 < i + k ? " " : "");
printf("] -> %d\n", ra[i]);
}
return 0;
}terminal
gcc -std=c17 -Wall -Wextra -g cua-so.c main.c -o t && ./t
mang : 1 3 -1 -3 5 3 6 7 k = 3 cua so [1 3 -1] -> 3 cua so [3 -1 -3] -> 3 cua so [-1 -3 5] -> 5 cua so [-3 5 3] -> 5 cua so [5 3 6] -> 6 cua so [3 6 7] -> 7
# So với cách ngây thơ trên mảng một triệu phần tử, k = 1000
./do-cua-so 1000000 1000
ngay tho : 4.812 s deque : 0.011 s nhanh hon: 437.5 lan
#Deque thay được cả ngăn xếp lẫn hàng đợi
Dùng deque làm ngăn xếp và hàng đợi
Deque d;
dq_khoi_tao(&d);
/* Dùng như NGĂN XẾP: chỉ đụng vào cuối */
dq_them_cuoi(&d, 10);
dq_them_cuoi(&d, 20);
dq_lay_cuoi(&d, &v); /* 20, LIFO */
/* Dùng như HÀNG ĐỢI: thêm cuối, lấy đầu */
dq_them_cuoi(&d, 10);
dq_them_cuoi(&d, 20);
dq_lay_dau(&d, &v); /* 10, FIFO */Ứng dụng thật của deque
| Ứng dụng | Vì sao cần cả hai đầu |
|---|---|
| Cửa sổ trượt lớn nhất và nhỏ nhất | Bỏ ở đầu khi hết hạn, bỏ ở cuối khi bị lấn át |
| Lịch sử duyệt web có giới hạn | Thêm ở cuối, và bỏ mục cũ nhất ở đầu khi vượt giới hạn |
| Hoàn tác có giới hạn số bước | Cùng lý do trên |
| Ăn cắp việc trong lập lịch song song | Luồng chủ lấy ở một đầu, luồng khác cướp việc ở đầu kia |
| Thuật toán 0-1 BFS | Cạnh trọng số 0 đẩy vào đầu, cạnh trọng số 1 đẩy vào cuối |
Tự làm thử
- Cài
Dequebằng mảng vòng với đủ bốn thao tác cộngdq_tai. - Viết bản
dq_them_daudùng(front - 1) % DMAX, đổiDMAXthành 10 và xác nhận nó sai. - Cài
cua_so_lon_nhatvà kiểm với mảng trong bài, rồi vớikbằng 1 vàkbằngn. - Sửa hàm đó thành
cua_so_nho_nhatchỉ bằng cách đổi một dấu so sánh. - Đo thời gian của cách ngây thơ và cách deque với n bằng một triệu và k bằng 10, 100, 1000. Giải thích vì sao tỷ lệ tăng theo k.
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
- Deque cho thêm và lấy ở cả hai đầu trong O(1). Ngăn xếp và hàng đợi đều là deque bị bịt bớt một phía.
- Lùi chỉ số theo vòng phải viết
(front + N - 1) % N, không được viết(front - 1) % Nvới kiểu không dấu. - Cài bằng mảng vòng cho truy cập theo chỉ số O(1) và tiết kiệm bộ nhớ. Cài bằng danh sách đôi cho kích thước không giới hạn và con trỏ bền.
- Bài cửa sổ trượt cần bỏ ở cả hai đầu, nên nó là bài toán mà chỉ deque giải được trong O(n).
- Có deque rồi vẫn nên dùng kiểu chuyên biệt khi bài toán chỉ cần một đầu, vì mã nói được ý định và trình biên dịch bắt lỗi giúp.