1. 双端队列基础概念解析
双端队列(Deque,全称Double-Ended Queue)是一种允许在队列两端进行插入和删除操作的线性数据结构。与普通队列的FIFO(先进先出)特性不同,双端队列同时支持FIFO和LIFO(后进先出)两种操作模式。
1.1 双端队列的核心特性
双端队列的核心操作包含四个基本方法:
push_front(x):在队首插入元素xpush_back(x):在队尾插入元素xpop_front():删除并返回队首元素pop_back():删除并返回队尾元素
这种对称性的操作设计使得双端队列可以灵活地模拟栈和队列的行为:
- 如果只使用
push_back和pop_front,就是普通队列 - 如果只使用
push_back和pop_back,就是栈结构
1.2 双端队列的ADT表示
用抽象数据类型(ADT)描述双端队列:
code复制ADT Deque {
数据对象:D = {a₁, a₂, ..., aₙ | aᵢ ∈ ElemType, i=1,2,...,n}
数据关系:R = {<aᵢ₋₁, aᵢ> | aᵢ₋₁, aᵢ ∈ D, i=2,...,n}
基本操作:
InitDeque(&D): 初始化双端队列
IsEmpty(D): 判断队列是否为空
PushFront(&D, e): 队首插入元素e
PushBack(&D, e): 队尾插入元素e
PopFront(&D, &e): 删除队首元素并用e返回
PopBack(&D, &e): 删除队尾元素并用e返回
GetFront(D, &e): 获取队首元素
GetBack(D, &e): 获取队尾元素
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 双端队列的实现方式
2.1 基于数组的实现
使用循环数组实现双端队列时,需要维护两个指针front和rear:
c复制#define MAXSIZE 100
typedef struct {
ElemType data[MAXSIZE];
int front, rear; // 队首和队尾指针
} ArrayDeque;
关键操作实现细节:
PushFront:front = (front - 1 + MAXSIZE) % MAXSIZEPushBack:rear = (rear + 1) % MAXSIZE- 判断队列满的条件:(rear + 1) % MAXSIZE == front
- 判断队列空的条件:front == rear
注意:数组实现时需要特别注意队满和队空的判断条件,这是408考试中常见的考点。
2.2 基于链表的实现
双向链表天然适合实现双端队列:
c复制typedef struct DequeNode {
ElemType data;
struct DequeNode *prev, *next;
} DequeNode;
typedef struct {
DequeNode *front, *rear;
int size;
} LinkedDeque;
链表实现的优势:
- 动态扩容,没有容量限制
- 插入删除操作时间复杂度都是O(1)
- 实现相对直观,指针操作清晰
2.3 实现方式对比
| 实现方式 | 插入/删除时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 数组实现 | O(1) | O(n) | 内存连续,访问快 | 容量固定,扩容成本高 |
| 链表实现 | O(1) | O(n) | 动态扩容,灵活 | 内存不连续,访问慢 |
3. 2010年408真题解析
3.1 原题重现
题目描述:假设用不带头结点的单向循环链表表示队列,并且只设一个指针指向队尾元素结点(注意:不设头指针)。试设计相应的入队和出队算法。
3.2 解题思路分析
虽然题目描述的是普通队列,但解题思路可以借鉴双端队列的实现方式。关键点在于:
- 只维护尾指针,如何找到头节点
- 循环链表的特性利用
- 边界条件处理(空队列、单节点队列)
3.3 入队算法实现
c复制void EnQueue(LinkList &rear, ElemType x) {
// 创建新节点
LinkList p = (LinkList)malloc(sizeof(LNode));
p->data = x;
if (rear == NULL) { // 空队列情况
p->next = p; // 自己指向自己
rear = p;
} else {
p->next = rear->next; // 新节点指向头节点
rear->next = p; // 原尾节点指向新节点
rear = p; // 更新尾指针
}
}
3.4 出队算法实现
c复制void DeQueue(LinkList &rear, ElemType &x) {
if (rear == NULL) {
printf("Queue is empty\n");
return;
}
LinkList p = rear->next; // p指向头节点
x = p->data;
if (p == rear) { // 只有一个节点
free(p);
rear = NULL;
} else {
rear->next = p->next; // 绕过头节点
free(p);
}
}
3.5 算法复杂度分析
- 时间复杂度:
- 入队:O(1)
- 出队:O(1)
- 空间复杂度:O(n)
4. 双端队列的应用场景
4.1 滑动窗口问题
双端队列特别适合解决滑动窗口最大值/最小值问题。以LeetCode 239为例:
python复制def maxSlidingWindow(nums, k):
deque = collections.deque()
res = []
for i, num in enumerate(nums):
# 维护单调递减队列
while deque and nums[deque[-1]] <= num:
deque.pop()
deque.append(i)
# 移除超出窗口范围的索引
if deque[0] == i - k:
deque.popleft()
# 记录结果
if i >= k - 1:
res.append(nums[deque[0]])
return res
4.2 撤销操作实现
许多编辑器的撤销/重做功能使用双栈实现,这本质上就是双端队列的应用:
code复制操作序列:A B C D
撤销栈:[A, B, C, D]
重做栈:[]
撤销一次:
撤销栈:[A, B, C]
重做栈:[D]
重做一次:
撤销栈:[A, B, C, D]
重做栈:[]
4.3 工作窃取算法
在多线程编程中,工作窃取(Work Stealing)算法使用双端队列:
- 每个线程维护自己的双端队列
- 本地线程从队尾取任务(LIFO)
- 其他线程窃取时从队首取任务(FIFO)
5. 双端队列的扩展变种
5.1 输出受限双端队列
只允许在一端进行删除操作,但两端都可以插入。这种结构在某些特定场景下可以提高安全性。
5.2 输入受限双端队列
只允许在一端进行插入操作,但两端都可以删除。适用于生产者-消费者模型的变种。
5.3 单调队列
保持队列元素单调递增或递减的特殊双端队列,常用于优化动态规划问题:
python复制def monotonicQueue(nums):
deque = collections.deque()
for num in nums:
while deque and deque[-1] < num: # 维护单调递减
deque.pop()
deque.append(num)
return list(deque)
6. 常见问题与调试技巧
6.1 边界条件处理
双端队列实现中最容易出错的是边界条件:
- 空队列时的操作
- 只有一个元素时的操作
- 队列满时的处理
调试技巧:在实现时先专门编写测试用例覆盖这些边界情况。
6.2 内存管理问题
链表实现时常见的错误:
- 内存泄漏(删除节点未free)
- 野指针(删除后未置空)
- 重复释放
建议的防御性编程实践:
c复制void freeNode(DequeNode *p) {
if (p) {
p->prev = p->next = NULL; // 断开链接
free(p);
}
}
6.3 并发安全问题
多线程环境下使用双端队列需要考虑:
- 使用互斥锁保护共享队列
- 考虑无锁实现(CAS操作)
- 避免死锁情况
简单的线程安全实现示例:
java复制public class ConcurrentDeque<T> {
private final Deque<T> deque = new ArrayDeque<>();
private final Object lock = new Object();
public void pushFront(T item) {
synchronized(lock) {
deque.addFirst(item);
}
}
public T popFront() {
synchronized(lock) {
return deque.pollFirst();
}
}
}
7. 408考试备考建议
7.1 重点掌握内容
- 双端队列的ADT表示和基本操作
- 数组和链表两种实现方式的对比
- 循环队列的判空判满条件
- 双端队列的应用场景分析
7.2 典型题目练习
建议重点练习以下类型题目:
- 实现双端队列的基本操作
- 循环队列的判空判满条件推导
- 使用双端队列解决滑动窗口问题
- 双端队列与栈/队列的相互模拟
7.3 时间复杂度的分析
对于双端队列的各种操作,要能熟练分析其时间复杂度:
- 数组实现:所有操作都是O(1)
- 链表实现:所有操作都是O(1)
- 动态扩容情况:均摊分析仍然是O(1)
8. 扩展学习资源
- 《数据结构(C语言版)》- 严蔚敏:双端队列的基础理论
- 《算法导论》:双端队列在算法中的应用
- LeetCode题库:滑动窗口相关题目实践
- 王道考研数据结构课程:408考点精讲
在实际编程中,不同语言对双端队列的实现略有差异:
- C++:STL中的deque
- Java:java.util.ArrayDeque
- Python:collections.deque
- JavaScript:需要自己实现或使用第三方库
理解这些底层实现差异有助于在不同场景下做出合适的选择。例如,Python的deque是基于双向链表实现的,而Java的ArrayDeque使用循环数组实现,这些实现细节会影响它们在特定场景下的性能表现。
