"人生就像双端队列,有时候得从前面冲,有时候得从后面撤!" 😎
📖 一、什么是双端队列?让我们从排队说起
1.1 生活中的普通队列 vs 双端队列
想象一下这个场景:
普通队列(Queue) 就像你去奶茶店排队:
- 🚶♂️ 新来的人从队尾进入
- 🏃♀️ 买到奶茶的人从队头离开
- ❌ 你不能从队伍中间插队或离开
双端队列(Deque,读作"deck") 就像VIP通道:
- ✅ 可以从队头进入,也可以从队尾进入
- ✅ 可以从队头离开,也可以从队尾离开
- 🎯 两端都开放,灵活得很!
1.2 专业定义
双端队列(Double-Ended Queue,简称 Deque) 是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。
简单来说:
普通队列: 只能从一头进,一头出 → FIFO(先进先出)
栈: 只能从一头进出 → LIFO(后进先出)
双端队列: 两头都能进出 → 自由灵活!
🎨 二、双端队列的结构图解
2.1 形象化理解
队头(Front/Head) 队尾(Rear/Tail)
↓ ↓
┌────────┬────────┬────────┬────────┬────────┐
←─── │ A │ B │ C │ D │ E │ ←───
───→ │ │ │ │ │ │ ───→
└────────┴────────┴────────┴────────┴────────┘
↑ ↑
可以进出 可以进出
2.2 操作演示
从队头添加元素(addFirst):
添加X到队头:
┌───┬───┬───┬───┬───┐
原来: │ A │ B │ C │ D │ E │
└───┴───┴───┴───┴───┘
↓
┌───┬───┬───┬───┬───┬───┐
现在: │ X │ A │ B │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘
从队尾添加元素(addLast):
添加Y到队尾:
┌───┬───┬───┬───┬───┐
原来: │ A │ B │ C │ D │ E │
└───┴───┴───┴───┴───┘
↓
┌───┬───┬───┬───┬───┬───┐
现在: │ A │ B │ C │ D │ E │ Y │
└───┴───┴───┴───┴───┴───┘
🔧 三、双端队列的核心操作
3.1 八大金刚操作
| 操作类型 | 队头操作 | 队尾操作 | 说明 |
|---|---|---|---|
| 添加 | addFirst(e) | addLast(e) | 添加元素,满了会抛异常 |
| 添加 | offerFirst(e) | offerLast(e) | 添加元素,满了返回false |
| 删除 | removeFirst() | removeLast() | 删除元素,空了会抛异常 |
| 删除 | pollFirst() | pollLast() | 删除元素,空了返回null |
| 查看 | getFirst() | getLast() | 查看元素,空了会抛异常 |
| 查看 | peekFirst() | peekLast() | 查看元素,空了返回null |
3.2 记忆小技巧 🧠
- add/remove/get → 失败会抛异常(激进派)💥
- offer/poll/peek → 失败返回特殊值(温和派)😊
- First → 队头操作 🔵
- Last → 队尾操作 🔴
💻 四、Java中的Deque实现
4.1 实现类对比
Java中有两种主要的Deque实现:
🔹 ArrayDeque(数组实现)
Deque<Integer> deque = new ArrayDeque<>();
特点:
- ✅ 底层用可变数组实现
- ✅ 不支持null元素
- ✅ 非线程安全
- ✅ 默认初始容量16,扩容为2倍
- ⚡ 两端操作都是 O(1) 时间复杂度
- 🎯 推荐使用(比LinkedList快)
内部结构:
循环数组实现(类似循环队列)
tail
↓
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ │ │ A │ B │ C │ D │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
↑
head
🔹 LinkedList(链表实现)
Deque<Integer> deque = new LinkedList<>();
特点:
- ✅ 底层用双向链表实现
- ✅ 支持null元素
- ✅ 非线程安全
- ✅ 没有容量限制
- ⚡ 两端操作都是 O(1) 时间复杂度
- 💾 但有额外的节点内存开销
内部结构:
双向链表
null ← [A] ⇄ [B] ⇄ [C] ⇄ [D] → null
↑ ↑
first last
4.2 代码实战 🚀
import java.util.*;
public class DequeDemo {
public static void main(String[] args) {
// 创建双端队列
Deque<String> deque = new ArrayDeque<>();
System.out.println("=== 1. 从两端添加元素 ===");
deque.addFirst("B"); // 队头添加
deque.addLast("C"); // 队尾添加
deque.addFirst("A"); // 队头添加
deque.addLast("D"); // 队尾添加
// 现在deque:[A, B, C, D]
System.out.println("Deque: " + deque);
System.out.println("\n=== 2. 查看两端元素 ===");
System.out.println("队头元素: " + deque.peekFirst()); // A
System.out.println("队尾元素: " + deque.peekLast()); // D
System.out.println("Deque: " + deque);
System.out.println("\n=== 3. 从两端删除元素 ===");
String first = deque.removeFirst(); // 删除A
String last = deque.removeLast(); // 删除D
System.out.println("删除的队头: " + first);
System.out.println("删除的队尾: " + last);
System.out.println("Deque: " + deque); // [B, C]
System.out.println("\n=== 4. 当作栈使用(LIFO)===");
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 等同于addFirst
stack.push(2);
stack.push(3);
System.out.println("栈: " + stack); // [3, 2, 1]
System.out.println("弹出: " + stack.pop()); // 3,等同于removeFirst
System.out.println("\n=== 5. 当作队列使用(FIFO)===");
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(10); // 等同于offerLast
queue.offer(20);
queue.offer(30);
System.out.println("队列: " + queue); // [10, 20, 30]
System.out.println("出队: " + queue.poll()); // 10,等同于pollFirst
System.out.println("\n=== 6. 遍历Deque ===");
Deque<String> words = new ArrayDeque<>(Arrays.asList("Hello", "World", "Java"));
// 正向遍历
System.out.print("正向: ");
for (String word : words) {
System.out.print(word + " ");
}
// 反向遍历
System.out.print("\n反向: ");
Iterator<String> descendingIterator = words.descendingIterator();
while (descendingIterator.hasNext()) {
System.out.print(descendingIterator.next() + " ");
}
}
}
输出结果:
=== 1. 从两端添加元素 ===
Deque: [A, B, C, D]
=== 2. 查看两端元素 ===
队头元素: A
队尾元素: D
Deque: [A, B, C, D]
=== 3. 从两端删除元素 ===
删除的队头: A
删除的队尾: D
Deque: [B, C]
=== 4. 当作栈使用(LIFO)===
栈: [3, 2, 1]
弹出: 3
=== 5. 当作队列使用(FIFO)===
队列: [10, 20, 30]
出队: 10
=== 6. 遍历Deque ===
正向: Hello World Java
反向: Java World Hello
🎯 五、应用场景:双端队列的十八般武艺
5.1 浏览器的前进后退功能 🌐
你在浏览网页时:
- 后退 → 从历史记录的队尾取出
- 前进 → 从历史记录的队头取出
class BrowserHistory {
private Deque<String> backStack = new ArrayDeque<>();
private Deque<String> forwardStack = new ArrayDeque<>();
private String current;
public BrowserHistory(String homepage) {
this.current = homepage;
}
public void visit(String url) {
backStack.push(current);
current = url;
forwardStack.clear(); // 访问新页面,清空前进历史
}
public String back() {
if (!backStack.isEmpty()) {
forwardStack.push(current);
current = backStack.pop();
}
return current;
}
public String forward() {
if (!forwardStack.isEmpty()) {
backStack.push(current);
current = forwardStack.pop();
}
return current;
}
}
5.2 工作窃取算法(Work Stealing)⚙️
在Java的Fork/Join框架中:
- 每个线程有自己的双端队列
- 从队头取自己的任务
- 从其他线程的队尾"偷"任务
- 减少竞争,提高效率!
线程A的队列:
自己从头取 → [任务1][任务2][任务3][任务4] ← 线程B从尾偷
5.3 滑动窗口最大值 📊
经典算法题:找出数组中每个窗口的最大值
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) return new int[0];
Deque<Integer> deque = new ArrayDeque<>(); // 存储索引
int[] result = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i++) {
// 移除队头过期的元素
while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 移除队尾比当前元素小的元素
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offerLast(i);
// 记录窗口最大值
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
5.4 LRU缓存淘汰 🗑️
双端队列可用于实现LRU(最近最少使用)缓存:
- 新访问的放队头
- 淘汰时从队尾移除
5.5 回文判断 🔄
public boolean isPalindrome(String s) {
Deque<Character> deque = new ArrayDeque<>();
for (char c : s.toCharArray()) {
deque.addLast(c);
}
while (deque.size() > 1) {
if (deque.pollFirst() != deque.pollLast()) {
return false;
}
}
return true;
}
🆚 六、ArrayDeque vs LinkedList:选谁?
6.1 性能对比表
| 特性 | ArrayDeque | LinkedList |
|---|---|---|
| 底层实现 | 循环数组 | 双向链表 |
| 内存占用 | 紧凑,连续内存 | 节点对象,额外指针开销 |
| 缓存友好 | ✅ 是 | ❌ 否 |
| 两端操作 | O(1) | O(1) |
| 随机访问 | O(n) | O(n) |
| 支持null | ❌ 否 | ✅ 是 |
| 扩容 | 需要复制数组 | 不需要 |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
6.2 性能实测 🔬
public class PerformanceTest {
public static void main(String[] args) {
int n = 1000000;
// ArrayDeque测试
Deque<Integer> arrayDeque = new ArrayDeque<>();
long start = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
arrayDeque.addLast(i);
}
for (int i = 0; i < n; i++) {
arrayDeque.pollFirst();
}
long arrayTime = System.currentTimeMillis() - start;
// LinkedList测试
Deque<Integer> linkedList = new LinkedList<>();
start = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
linkedList.addLast(i);
}
for (int i = 0; i < n; i++) {
linkedList.pollFirst();
}
long linkedTime = System.currentTimeMillis() - start;
System.out.println("ArrayDeque: " + arrayTime + "ms");
System.out.println("LinkedList: " + linkedTime + "ms");
}
}
结果:ArrayDeque 通常快 2-3 倍! 🚀
6.3 选择建议
✅ 选择 ArrayDeque 的场景:
- 不需要存储null
- 需要高性能
- 当作栈或队列使用
- 绝大多数情况(推荐)
✅ 选择 LinkedList 的场景:
- 需要存储null元素
- 需要频繁在中间位置插入/删除
- 内存使用波动大,避免数组扩容
🎓 七、经典面试题
面试题1:Deque如何实现栈?
答案:
// Stack接口方法 → Deque等价方法
push(e) → addFirst(e)
pop() → removeFirst()
peek() → peekFirst()
// 示例
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 实际调用addFirst(1)
stack.push(2);
int top = stack.pop(); // 实际调用removeFirst()
面试题2:Deque如何实现队列?
答案:
// Queue接口方法 → Deque等价方法
offer(e) → offerLast(e)
poll() → pollFirst()
peek() → peekFirst()
// 示例
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(1); // 实际调用offerLast(1)
queue.offer(2);
int front = queue.poll(); // 实际调用pollFirst()
面试题3:为什么ArrayDeque不允许null?
答案:
因为ArrayDeque的poll()和peek()等方法用null表示队列为空。如果允许存null,就无法区分"队列空"还是"取出的元素是null"了!
Deque<String> deque = new ArrayDeque<>();
String s = deque.poll(); // 返回null表示队列为空
// 如果允许存null,就分不清了
面试题4:ArrayDeque的扩容机制?
答案:
- 初始容量:16(必须是2的幂)
- 扩容时机:元素数量达到容量
- 扩容大小:2倍
- 为什么是2的幂:方便用位运算取模
// 源码简化
private void doubleCapacity() {
int oldCapacity = elements.length;
int newCapacity = oldCapacity << 1; // 2倍扩容
// ... 复制元素
}
🎪 八、趣味小故事
故事:双端队列的自述
大家好,我是双端队列Deque,江湖人称"两头蛇" 🐍
我爹是队列Queue,我娘是栈Stack,所以我继承了他们俩的优点:
- 👨 从我爹那里学会了FIFO(先进先出)
- 👩 从我娘那里学会了LIFO(后进先出)
- 😎 但我比他们都灵活,两头都能进出!
小时候,我有两个兄弟:
- 大哥 ArrayDeque(数组实现):跑得快,但房子固定大小
- 二哥 LinkedList(链表实现):房子能无限扩,但跑得慢点
长大后,我们仨在Java帝国找到了工作:
- 在并发框架里帮忙分配任务(Work Stealing)
- 在算法竞赛中解决滑动窗口问题
- 在浏览器里记录访问历史
- 在游戏开发中管理技能冷却队列
虽然我的普通队列表亲和栈表妹更出名,但我才是真正的全能选手!💪
📚 九、知识点总结
核心要点 ✨
- 定义:双端队列(Deque)是两端都能进出的队列
- 操作:支持First/Last两个方向的add、remove、peek等操作
- 实现:ArrayDeque(推荐)和 LinkedList
- 时间复杂度:两端操作都是 O(1)
- 特点:既能当栈用(LIFO),又能当队列用(FIFO)
记忆口诀 🎵
双端队列Deque好,
两头进出真灵巧。
ArrayDeque跑得快,
LinkedList能存null。
First操作在队头,
Last操作在队尾。
栈和队列都能当,
面试必考要记牢!
对比表格 📊
| 数据结构 | 队头操作 | 队尾操作 | 典型应用 |
|---|---|---|---|
| Stack | ✅ 进出 | ❌ | 函数调用栈 |
| Queue | ✅ 出 | ✅ 进 | 任务队列 |
| Deque | ✅ 进出 | ✅ 进出 | 滑动窗口、LRU |
🎯 十、练习题
练习1:用Deque实现一个简单的撤销功能
class UndoManager {
private Deque<String> history = new ArrayDeque<>();
private Deque<String> redoStack = new ArrayDeque<>();
public void execute(String action) {
history.push(action);
redoStack.clear(); // 执行新操作,清空重做栈
}
public String undo() {
if (history.isEmpty()) return null;
String action = history.pop();
redoStack.push(action);
return action;
}
public String redo() {
if (redoStack.isEmpty()) return null;
String action = redoStack.pop();
history.push(action);
return action;
}
}
练习2:实现一个最大值最小值队列
class MinMaxQueue {
private Deque<Integer> data = new ArrayDeque<>();
private Deque<Integer> maxDeque = new ArrayDeque<>(); // 单调递减
private Deque<Integer> minDeque = new ArrayDeque<>(); // 单调递增
public void offer(int val) {
data.offer(val);
// 维护最大值队列
while (!maxDeque.isEmpty() && maxDeque.peekLast() < val) {
maxDeque.pollLast();
}
maxDeque.offer(val);
// 维护最小值队列
while (!minDeque.isEmpty() && minDeque.peekLast() > val) {
minDeque.pollLast();
}
minDeque.offer(val);
}
public int poll() {
int val = data.poll();
if (maxDeque.peekFirst() == val) maxDeque.pollFirst();
if (minDeque.peekFirst() == val) minDeque.pollFirst();
return val;
}
public int max() {
return maxDeque.peekFirst();
}
public int min() {
return minDeque.peekFirst();
}
}
🌟 十一、总结彩蛋
恭喜你!🎉 你已经掌握了双端队列这个强大的数据结构!
还记得吗?
- 🎯 Deque = Double-Ended Queue(两端都能操作)
- 🚀 ArrayDeque 是首选(快!)
- 💪 既能当栈,又能当队列
- 🔥 面试高频考点
最后送你一张图
队头 ← Deque → 队尾
↖ ↑ ↗
╲ | ╱
╲ | ╱
╲|╱
🎁
超级灵活!
下次见,继续学习下一个知识点吧! 😄
📖 参考资料
- Java官方文档:Deque Interface
- 《算法导论》第10章 - 基本数据结构
- 《Java核心技术卷I》- 集合框架
- LeetCode题单:Deque相关题目
作者: AI算法导师
最后更新: 2025年11月
难度等级: ⭐⭐⭐ (中等)
预计学习时间: 2-3小时
💡 温馨提示:光看不练假把式,一定要手写代码,才能真正掌握哦!