1. 什么是 Deque?
Deque(发音通常同 "deck")是 "Double Ended Queue"(双端队列)的缩写。它是Java集合框架中的一个核心接口,定义了一种线性集合,允许在队列的两端进行元素的添加和移除操作。
Deque 的这个特性使它成为一个极其灵活的数据结构,可以同时扮演两种重要的角色:
- 栈 (Stack):当只在一端(例如,头部)进行添加和移除操作时,
Deque就表现为“后进先出”(LIFO)的栈。它是官方推荐的、用于替代老旧Stack类的现代化方案。 - 队列 (Queue):当在一端添加元素(队尾)、在另一端移除元素(队首)时,
Deque就表现为“先进先出”(FIFO)的队列。
Deque 是一个接口,因此在使用时需要创建它的具体实现类的实例,最常用的是 ArrayDeque 和 LinkedList。
2. Deque 的核心优势
- 一物两用,极其灵活:一个
Deque对象既能满足栈的需求,也能满足队列的需求,甚至可以完成更复杂的操作(如在队首入队)。 - 高性能:
Deque的主要实现类ArrayDeque是非同步的,基于可变数组,其性能远超于基于Vector的老旧Stack类。对于栈和队列操作,它通常能提供摊销常数时间(amortized constant time)的性能。 - 设计优良:遵循了Java集合框架“面向接口编程”的最佳实践,将接口定义与具体实现分离,使得代码更加健壮和可维护。
3. Deque 作为栈 (LIFO) 使用
这是替代 Stack 类的标准用法。Deque 接口特意提供了与 Stack 类同名的方法 (push, pop, peek),使得迁移和使用都非常方便。
| 栈操作 (Stack Operation) | Stack 方法 | Deque 等效方法 | 描述 |
|---|---|---|---|
| 入栈 | push(e) | push(e) 或 addFirst(e) | 将元素添加到栈顶(Deque的头部) |
| 出栈 | pop() | pop() 或 removeFirst() | 移除并返回栈顶元素 |
| 查看栈顶 | peek() | peek() 或 getFirst() | 查看栈顶元素但不移除 |
代码示例:
import java.util.Deque;
import java.util.ArrayDeque;
public class DequeAsStackDemo {
public static void main(String[] args) {
// 推荐使用 ArrayDeque 作为 Deque 的实现来模拟栈
Deque<String> stack = new ArrayDeque<>();
// 入栈
stack.push("Java");
stack.push("Python");
stack.push("C++");
System.out.println("当前栈顶元素: " + stack.peek()); // 输出: C++
// 出栈
while (!stack.isEmpty()) {
System.out.println("出栈: " + stack.pop());
}
// 输出顺序: C++, Python, Java
}
}
4. Deque 作为队列 (FIFO) 使用
Deque 也可以完美地实现一个标准的队列。
| 队列操作 (Queue Operation) | Queue 方法 | Deque 等效方法 | 描述 |
|---|---|---|---|
| 入队 | add(e) / offer(e) | addLast(e) / offerLast(e) | 将元素添加到队尾 |
| 出队 | remove() / poll() | removeFirst() / pollFirst() | 移除并返回队首元素 |
| 查看队首 | element() / peek() | getFirst() / peekFirst() | 查看队首元素但不移除 |
代码示例:
import java.util.Deque;
import java.util.ArrayDeque;
public class DequeAsQueueDemo {
public static void main(String[] args) {
Deque<String> queue = new ArrayDeque<>();
// 入队 (在尾部添加)
queue.offerLast("任务A");
queue.offerLast("任务B");
queue.offerLast("任务C");
System.out.println("当前队首任务: " + queue.peekFirst()); // 输出: 任务A
// 出队 (从头部移除)
while (!queue.isEmpty()) {
System.out.println("处理任务: " + queue.pollFirst());
}
// 输出顺序: 任务A, 任务B, 任务C
}
}
5. Deque 接口核心方法详解
Deque 的方法设计有一个重要特点:几乎每个操作都有两种形式。
- 抛出异常:如果操作失败(例如,从空队列中移除元素),会抛出
Exception。 - 返回特殊值:如果操作失败,会返回
null或false。
在需要严格控制程序流程时,推荐使用返回特殊值的方法。
| 操作 | 抛出异常的方法 | 返回特殊值的方法 | 描述 |
|---|---|---|---|
| 在头部插入 | addFirst(e) | offerFirst(e) | 将元素添加到 Deque 的头部。offerFirst在有容量限制时更安全。 |
| 在尾部插入 | addLast(e) | offerLast(e) | 将元素添加到 Deque 的尾部。 |
| 从头部移除 | removeFirst() | pollFirst() | 移除并返回头部元素。如果队列为空,removeFirst抛异常,pollFirst返回null。 |
| 从尾部移除 | removeLast() | pollLast() | 移除并返回尾部元素。如果队列为空,removeLast抛异常,pollLast返回null。 |
| 获取头部元素 | getFirst() | peekFirst() | 获取头部元素但不移除。如果队列为空,getFirst抛异常,peekFirst返回null。 |
| 获取尾部元素 | getLast() | peekLast() | 获取尾部元素但不移除。如果队列为空,getLast抛异常,peekLast返回null。 |
栈相关方法:
push(e): 内部调用addFirst(e)。pop(): 内部调用removeFirst()。peek(): 内部调用getFirst()。
6. Deque 的主要实现类
ArrayDeque
- 底层结构:可调整大小的数组。
- 特点:
- 不允许
null元素。 - 非线程安全。
- 性能极高,是实现栈和队列的首选。
- 不允许
- 最佳用途:当你需要一个纯粹的、高性能的栈或队列时。
LinkedList
- 底层结构:双向链表。
- 特点:
- 允许
null元素。 - 非线程安全。
- 除了
Deque接口,它还实现了List接口,功能更丰富。
- 允许
- 最佳用途:当你在同一个对象上既需要
List的随机访问/修改功能,又需要Deque的两端操作功能时。
怎么选?
- 通用场景:无脑选择
ArrayDeque。 - 特殊需求:需要存储
null,或者需要频繁地在集合中间进行增删操作时,可以考虑LinkedList。
7. 实际应用场景
Deque 的应用非常广泛,特别是在算法问题中:
- 括号匹配/表达式求值:经典的栈应用。
- 树的深度优先搜索 (DFS):可以使用
Deque作为栈来存储待访问的节点。 - 树的广度优先搜索 (BFS):可以使用
Deque作为队列来存储待访问的节点。 - 滑动窗口问题:例如“滑动窗口最大值”,
Deque可以用来维护一个单调队列,高效地找到当前窗口的最值。 - 任务调度器:可以利用
Deque实现一个工作窃取(Work-Stealing)算法,调度器可以从自己队列的头部获取任务,也可以从其他调度器队列的尾部“窃取”任务。
8. 总结
Deque 是Java中一个设计精良、功能强大的接口。它不仅是老旧 Stack 类的完美替代品,还提供了高效的队列实现。
核心要点:
Deque是一个双端队列接口。- 它可以作为栈 (LIFO) 或队列 (FIFO) 使用。
ArrayDeque是首选的实现类,性能高,不允许null。LinkedList在需要List和Deque双重功能时使用。- 在新的Java代码中,请始终优先选择
Deque(通常是ArrayDeque) 来满足你对栈或队列的需求。