🎭 双端队列(Deque):两头都能进出的神奇队伍!

66 阅读9分钟

"人生就像双端队列,有时候得从前面冲,有时候得从后面撤!" 😎


📖 一、什么是双端队列?让我们从排队说起

1.1 生活中的普通队列 vs 双端队列

想象一下这个场景:

普通队列(Queue) 就像你去奶茶店排队:

  • 🚶‍♂️ 新来的人从队尾进入
  • 🏃‍♀️ 买到奶茶的人从队头离开
  • ❌ 你不能从队伍中间插队或离开

双端队列(Deque,读作"deck") 就像VIP通道:

  • ✅ 可以从队头进入,也可以从队尾进入
  • ✅ 可以从队头离开,也可以从队尾离开
  • 🎯 两端都开放,灵活得很!

1.2 专业定义

双端队列(Double-Ended Queue,简称 Deque) 是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。

简单来说:

普通队列:  只能从一头进,一头出  →  FIFO(先进先出)
栈:        只能从一头进出        →  LIFO(后进先出)
双端队列:  两头都能进出          →  自由灵活!

🎨 二、双端队列的结构图解

2.1 形象化理解

        队头(Front/Head)                    队尾(Rear/Tail)
              ↓                                   ↓
         ┌────────┬────────┬────────┬────────┬────────┐
    ←─── │   AB    │   C    │   D    │   E    │ ←───
    ───→ │        │        │        │        │        │ ───→
         └────────┴────────┴────────┴────────┴────────┘
              ↑                                   ↑
         可以进出                              可以进出

2.2 操作演示

从队头添加元素(addFirst):

添加X到队头:
         ┌───┬───┬───┬───┬───┐
原来:   │ AB │ C │ D │ E │
         └───┴───┴───┴───┴───┘
              ↓
         ┌───┬───┬───┬───┬───┬───┐
现在:   │ X │ AB │ C │ D │ E │
         └───┴───┴───┴───┴───┴───┘

从队尾添加元素(addLast):

添加Y到队尾:
         ┌───┬───┬───┬───┬───┐
原来:   │ AB │ C │ D │ E │
         └───┴───┴───┴───┴───┘
              ↓
         ┌───┬───┬───┬───┬───┬───┐
现在:   │ AB │ 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 性能对比表

特性ArrayDequeLinkedList
底层实现循环数组双向链表
内存占用紧凑,连续内存节点对象,额外指针开销
缓存友好✅ 是❌ 否
两端操作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)
  • 算法竞赛中解决滑动窗口问题
  • 浏览器里记录访问历史
  • 游戏开发中管理技能冷却队列

虽然我的普通队列表亲和栈表妹更出名,但我才是真正的全能选手!💪


📚 九、知识点总结

核心要点 ✨

  1. 定义:双端队列(Deque)是两端都能进出的队列
  2. 操作:支持First/Last两个方向的add、remove、peek等操作
  3. 实现:ArrayDeque(推荐)和 LinkedList
  4. 时间复杂度:两端操作都是 O(1)
  5. 特点:既能当栈用(LIFO),又能当队列用(FIFO)

记忆口诀 🎵

双端队列Deque好,
两头进出真灵巧。
ArrayDeque跑得快,
LinkedList能存nullFirst操作在队头,
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 → 队尾
           ↖    ↑    ↗
             ╲  |  ╱
              ╲ | ╱
               ╲|╱
                🎁
           超级灵活!

下次见,继续学习下一个知识点吧! 😄


📖 参考资料

  1. Java官方文档:Deque Interface
  2. 《算法导论》第10章 - 基本数据结构
  3. 《Java核心技术卷I》- 集合框架
  4. LeetCode题单:Deque相关题目

作者: AI算法导师
最后更新: 2025年11月
难度等级: ⭐⭐⭐ (中等)
预计学习时间: 2-3小时

💡 温馨提示:光看不练假把式,一定要手写代码,才能真正掌握哦!