双端队列(Deque)是数据结构家族中的一位多面手,它融合了队列与栈的特性,在算法设计与系统开发中扮演着重要角色。本文将深入探讨其核心原理、底层实现,并通过Java、Python等语言示例,助你掌握这一高效工具。

1️⃣ 什么是双端队列?
双端队列,简称Deque(Double-Ended Queue),是一种允许在两端进行插入和删除操作的抽象数据类型。与普通队列(FIFO)仅能在队尾添加、队头移除不同,Deque打破了这一限制,提供了更灵活的操作方式。

你可以将Deque想象成一座“双向桥梁”——元素既可以从左端进入/离开,也可以从右端进入/离开。这种设计使其同时具备了队列(先进先出)和栈(后进先出)的能力,是算法竞赛和工业级开发中的常客。在Python中,collections.deque是高性能的默认选择;在Java中,ArrayDeque是官方推荐实现;而在JavaScript和Go中,开发者常基于双向链表或环形数组自行构建。
2️⃣ 双端队列 vs 普通队列 vs 栈
为了更直观地理解Deque的优势,我们将其与普通队列和栈进行对比:
| 数据结构 | 插入位置 | 删除位置 | 操作原则 | 灵活性 |
|---|---|---|---|---|
| 栈(Stack) | 顶部 | 顶部 | LIFO(后进先出) | ⭐⭐ |
| 队列(Queue) | 队尾 | 队首 | FIFO(先进先出) | ⭐⭐ |
| 双端队列(Deque) | 两端任意 | 两端任意 | 灵活组合 | ⭐⭐⭐⭐⭐ |
从上表可以看出:Deque在操作灵活性上远超队列和栈。普通队列严格遵循FIFO,栈严格遵循LIFO,而Deque可以自由切换两种模式。这种特性使得Deque在滑动窗口、回文检测、任务调度等场景中表现出色。
3️⃣ 为什么需要Deque?—— 现有方案的局限性
在Java等主流语言中,开发者通常面临以下选择,但每种方案都存在明显短板:
- 栈(Stack类 / Deque模拟栈):Java的
Stack类(或用Deque模拟栈)仅支持从栈顶执行push(入栈)、pop(出栈)、peek(查看栈顶)操作。无法直接访问栈底元素,也不能随机访问栈中任意位置的元素,操作灵活性极低。 - 队列(Queue接口):Java的
Queue接口(典型实现如LinkedList)严格遵循先进先出(FIFO)规则,仅能从队尾添加元素、队头移除元素,无法灵活地在队头/队尾之外的位置操作,也不支持反向遍历或修改。 - 静态数组(如int[]、String[]):长度固定,在数组中间位置插入/删除元素时,需手动遍历并移动后续所有元素,时间复杂度为O(n),效率极低。
- 链表(LinkedList底层实现):Java中
LinkedList底层是双向链表结构,其随机访问(通过索引get(int index))需从头节点或尾节点遍历到目标位置,时间复杂度O(n),远不如数组的O(1)随机访问性能。 - 动态数组(ArrayList):Java的
ArrayList是最常用的动态数组实现,其底层基于可变长度的连续内存块(会自动扩容),核心操作的性能特征直接受限于连续内存布局:- 尾部操作:
add(E e)(尾部添加)、remove(size()-1)(尾部删除)的时间复杂度为O(1)(均摊)。因为ArrayList会预分配额外的内存空间(默认扩容因子1.5倍),尾部操作无需移动其他元素。 - 头部操作:
add(0, E e)(头部插入)、remove(0)(头部删除)的时间复杂度为O(n)。为了保持内存的连续性,头部插入时需要将数组中所有元素向右移动一位;头部删除时需要将所有元素向左移动一位,元素数量越多,移动耗时越长,这是ArrayList最关键的性能瓶颈。
- 尾部操作:

Deque的设计初衷正是为了解决这些痛点。它通过非连续的内存布局(如块状双向链表或环形数组),保证了在两端的添加和删除操作都具有O(1)的平均时间复杂度。无论是Python的deque、Java的ArrayDeque,还是C++标准库中的std::deque,其核心性能优势都源于此。
4️⃣ Deque的底层实现原理
要理解Deque为何如此高效,我们需要探究其内部结构。一种经典且高效的实现方式是采用块状双向链表。

这种结构是如何工作的?
- 数据块:Deque内部维护了一系列固定大小的数组(例如,每个数组容量为64),我们称之为“数据块”。
- 双向链表:这些数据块通过指针连接成一个双向链表。每个块都知道它的前一个块和后一个块是谁。
- 头尾指针:Deque对象本身持有指向最左边块(头部)和最右边块(尾部)的指针,以及块内具体元素的偏移量。
当在Deque的一端添加元素时:
- 如果该端的块还有空间,直接在块内的数组中添加元素即可。这是一个O(1)操作。
- 如果该端的块已满,只需创建一个新的空块,通过链表指针将其连接到当前端块的旁边,然后将新元素放入新块中。这个过程只涉及指针操作和内存分配,与Deque中已有元素的数量无关,因此也是O(1)操作。
删除操作的逻辑完全对称,同样高效。这种设计巧妙地规避了动态数组那样大规模的数据迁移问题。
为什么不用纯粹的双向链表? 纯粹的双向链表(每个节点存一个元素)虽然也能实现O(1)的两端操作,但有两个主要缺点:1) 内存开销大:每个元素都需要额外存储两个指针。2) 缓存性能差:元素在内存中是零散分布的,CPU无法有效利用缓存进行预读,导致访问速度慢。而Deque的“块状”结构,使得一个块内的元素是连续存储的,大大提高了缓存命中率,实现了空间和时间的双重优化。
⚠️ 随机访问的代价:这种设计的唯一缺点是随机访问deque[i]的性能。对于靠近两端的元素访问很快,通常是O(1)操作,但越靠近中间,速度越慢,是O(n)操作,这点就不如List了。
5️⃣ 双端队列的实现
5.1 Java的Deque接口
优先使用性能更优的ArrayDeque实现:
package duilie;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
/**
* @Author Stringzhua
* @Date 2025/12/18 17:51
* description:
*/
public class DequeExample {
// deque的rotate操作 右旋转n位
public static void rotate(Deque deque, int n) {
if (deque.isEmpty() || n == 0) {
return;
}
// 处理旋转步数超过队列长度的情况(取模),兼容负数左旋转
int rotateSteps = n % deque.size();
if (rotateSteps < 0) {
rotateSteps += deque.size();
}
// 右旋转逻辑:每次将最后一个元素移到头部,循环n次
for (int i = 0; i < rotateSteps; i++) {
T lastElement = deque.removeLast();
deque.addFirst(lastElement);
}
}
// 自定义有限长度Deque
static class LimitedDeque extends ArrayDeque {
private final int maxLen;
public LimitedDeque(int maxLen) {
super(maxLen);
this.maxLen = maxLen;
}
@Override
public void addLast(T t) {
if (size() == maxLen) {
this.removeFirst(); // 超过最大长度,移除头部元素
}
super.addLast(t); // 调用父类的addLast(返回void)
}
// 兼容offerLast(Java 8中offerLast返回boolean)
@Override
public boolean offerLast(T t) {
if (size() == maxLen) {
this.removeFirst();
}
super.addLast(t);
return true;
}
// 重写toString
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("deque([");
if (!isEmpty()) {
Object[] elements = this.toArray();
for (int i = 0; i < elements.length; i++) {
sb.append(elements[i]);
if (i < elements.length - 1) {
sb.append(", ");
}
}
}
sb.append("])");
sb.append(", maxlen=").append(maxLen);
return sb.toString();
}
}
// 辅助方法:格式化普通Deque的输出
public static String formatDeque(Deque deque) {
StringBuilder sb = new StringBuilder();
sb.append("deque([");
if (!deque.isEmpty()) {
Object[] elements = deque.toArray();
for (int i = 0; i < elements.length; i++) {
sb.append(elements[i]);
if (i < elements.length - 1) {
sb.append(", ");
}
}
}
sb.append("])");
return sb.toString();
}
public static void main(String[] args) {
// 1. 创建一个 deque
Deque d = new ArrayDeque<>(Arrays.asList("c", "d", "e"));
System.out.printf("初始 deque: %s%n", formatDeque(d));
// 2. 在右侧添加元素
d.addLast("f");
System.out.printf("append('f'): %s%n", formatDeque(d));
// 3. 在左侧添加元素
d.addFirst("b");
System.out.printf("appendleft('b'): %s%n", formatDeque(d));
// 4. 从右侧移除元素
String rightItem = d.removeLast();
System.out.printf("pop() -> '%s', deque: %s%n", rightItem, formatDeque(d));
// 5. 从左侧移除元素
String leftItem = d.removeFirst();
System.out.printf("popleft() -> '%s', deque: %s%n", leftItem, formatDeque(d));
// 6. 旋转元素
rotate(d, 1);
System.out.printf("rotate(1): %s%n", formatDeque(d));
// 7. 创建一个有最大长度限制的 deque
LimitedDeque dLimited = new LimitedDeque<>(3);
dLimited.addLast(1);
dLimited.addLast(2);
dLimited.addLast(3);
System.out.printf("有限长度 deque (满): %s%n", dLimited);
dLimited.addLast(4);
System.out.printf("添加 4 后: %s%n", dLimited);
}
}
运行结果:
初始 deque: deque([c, d, e])
append('f'): deque([c, d, e, f])
appendleft('b'): deque([b, c, d, e, f])
pop() -> 'f', deque: deque([b, c, d, e])
popleft() -> 'b', deque: deque([c, d, e])
rotate(1): deque([e, c, d])
有限长度 deque (满): deque([1, 2, 3]), maxlen=3
添加 4 后: deque([2, 3, 4]), maxlen=3
5.2 基于双向链表的实现
package duilie;
/**
* 基于双向链表的双端队列 Java 实现
* @author Stringzhua
*/
public class DequeDemo {
// 双向链表节点类
static class Node {
Object data;
Node prev; // 前驱指针
Node next; // 后继指针
public Node(Object data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
// 基于双向链表的双端队列实现
static class Deque {
private Node head; // 哨兵头节点
private Node tail; // 哨兵尾节点
private int size; // 队列元素个数
public Deque() {
// 初始化哑头/哑尾节点,简化边界处理
head = new Node(null);
tail = new Node(null);
head.next = tail;
tail.prev = head;
size = 0;
}
/**
* 在两个节点之间插入新节点
* @param data 插入的元素
* @param predecessor 前驱节点
* @param successor 后继节点
*/
private void addBetween(Object data, Node predecessor, Node successor) {
Node newNode = new Node(data);
newNode.prev = predecessor;
newNode.next = successor;
predecessor.next = newNode;
successor.prev = newNode;
size++;
}
/**
* 删除指定节点
* @param node 要删除的节点
* @return 被删除节点的元素
*/
private Object removeNode(Node node) {
if (size == 0) {
throw new IndexOutOfBoundsException("双端队列为空");
}
Object data = node.data;
Node predecessor = node.prev;
Node successor = node.next;
predecessor.next = successor;
successor.prev = predecessor;
size--;
return data;
}
/**
* 在左端添加元素
* @param data 要添加的元素
*/
public void appendLeft(Object data) {
addBetween(data, head, head.next);
}
/**
* 在右端添加元素
* @param data 要添加的元素
*/
public void appendRight(Object data) {
addBetween(data, tail.prev, tail);
}
/**
* 从左端删除并返回元素
* @return 左端的元素
*/
public Object popLeft() {
return removeNode(head.next);
}
/**
* 从右端删除并返回元素
* @return 右端的元素
*/
public Object popRight() {
return removeNode(tail.prev);
}
/**
* 查看左端元素
* @return 左端的元素
*/
public Object peekLeft() {
if (size == 0) {
throw new IndexOutOfBoundsException("双端队列为空");
}
return head.next.data;
}
/**
* 查看右端元素
* @return 右端的元素
*/
public Object peekRight() {
if (size == 0) {
throw new IndexOutOfBoundsException("双端队列为空");
}
return tail.prev.data;
}
/**
* 检查队列是否为空
* @return 空返回true,否则返回false
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 获取队列长度
* @return 元素个数
*/
public int size() {
return size;
}
/**
* 重写toString
* @return 队列的字符串表示
*/
@Override
public String toString() {
if (isEmpty()) {
return "Deque([])";
}
StringBuilder elements = new StringBuilder();
Node current = head.next;
while (current != tail) {
elements.append(current.data);
if (current.next != tail) {
elements.append(" ↔ ");
}
current = current.next;
}
return String.format("Deque([%s])", elements.toString());
}
}
public static void main(String[] args) {
// 创建双端队列实例
Deque dq = new Deque();
// 1. 基本操作演示
System.out.println("=== 双端队列基本操作演示 ===");
System.out.printf("初始状态: %s%n", dq); // Deque([])
// 左端添加元素
dq.appendLeft("A");
dq.appendLeft("B");
System.out.printf("左端添加A,B后: %s%n", dq); // Deque([B ↔ A])
// 右端添加元素
dq.appendRight("C");
dq.appendRight("D");
System.out.printf("右端添加C,D后: %s%n", dq); // Deque([B ↔ A ↔ C ↔ D])
// 2. 查看操作
System.out.println("\n=== 查看操作 ===");
System.out.printf("左端元素: %s%n", dq.peekLeft()); // B
System.out.printf("右端元素: %s%n", dq.peekRight()); // D
System.out.printf("队列长度: %d%n", dq.size()); // 4
// 3. 删除操作
System.out.println("\n=== 删除操作 ===");
Object leftItem = dq.popLeft();
System.out.printf("从左端删除: %s%n", leftItem); // B
System.out.printf("删除后状态: %s%n", dq); // Deque([A ↔ C ↔ D])
Object rightItem = dq.popRight();
System.out.printf("从右端删除: %s%n", rightItem); // D
System.out.printf("删除后状态: %s%n", dq); // Deque([A ↔ C])
// 4. 作为栈使用(LIFO)
System.out.println("\n=== 作为栈使用 ===");
Deque stackDq = new Deque();
// 入栈(右端进入)
for (int i = 1; i <= 3; i++) {
stackDq.appendRight(i);
System.out.printf("入栈 %d: %s%n", i, stackDq);
}
// 出栈(右端出来)
while (!stackDq.isEmpty()) {
Object item = stackDq.popRight();
System.out.printf("出栈 %s: %s%n", item, stackDq);
}
// 5. 作为队列使用(FIFO)
System.out.println("\n=== 作为队列使用 ===");
Deque queueDq = new Deque();
// 入队(右端进入)
String[] queueItems = {"first", "second", "third"};
for (String item : queueItems) {
queueDq.appendRight(item);
System.out.printf("入队 %s: %s%n", item, queueDq);
}
// 出队(左端出来)
while (!queueDq.isEmpty()) {
Object item = queueDq.popLeft();
System.out.printf("出队 %s: %s%n", item, queueDq);
}
}
}
运行结果:
=== 双端队列基本操作演示 ===
初始状态: Deque([])
左端添加A,B后: Deque([B ↔ A])
右端添加C,D后: Deque([B ↔ A ↔ C ↔ D])
=== 查看操作 ===
左端元素: B
右端元素: D
队列长度: 4
=== 删除操作 ===
从左端删除: B
删除后状态: Deque([A ↔ C ↔ D])
从右端删除: D
删除后状态: Deque([A ↔ C])
=== 作为栈使用 ===
入栈 1: Deque([1])
入栈 2: Deque([1 ↔ 2])
入栈 3: Deque([1 ↔ 2 ↔ 3])
出栈 3: Deque([1 ↔ 2])
出栈 2: Deque([1])
出栈 1: Deque([])
=== 作为队列使用 ===
入队 first: Deque([first])
入队 second: Deque([first ↔ second])
入队 third: Deque([first ↔ second ↔ third])
出队 first: Deque([second ↔ third])
出队 second: Deque([third])
出队 third: Deque([])
5.3 基于环形数组实现的队列
环形数组能够做到所有操作都是O(1)操作,其核心本质:
- 数学原理:模运算:(front - 1 + capacity) % capacity(左端位置计算),(front + size) % capacity(右端位置计算)。模运算是CPU原生支持的操作,时间复杂度O(1)。
- 内存原理:随机访问:数组元素在内存中连续存储,地址计算公式:address = base_address + index × element_size。无论访问哪个索引,计算时间都是常数。
- 算法原理:避免数据移动:传统数组插入需要移动后续所有元素,环形数组通过指针重定向避免数据移动,空间复用,逻辑连续但物理不连续。
package duilie; /** * @Author Stringzhua * @Date 2026/1/5 14:28 * description: * 基于循环数组的双端队列实现 ** 核心设计原理: * 1. 使用固定大小的数组作为底层存储 * 2. 通过front指针标记第一个元素的位置 * 3. 通过size记录当前元素个数 * 4. 通过模运算实现环形访问 *
* 时间复杂度分析:所有操作都是O(1),因为只需要简单的指针计算 */ public class CircularArrayDeque
{ private T[] data; // 底层数组 private int capacity; // 数组容量 private int front; // 第一个元素的索引 private int size; // 当前元素个数 /** * 初始化 - 时间复杂度: O(1) * * @param capacity 初始容量 */ @SuppressWarnings("unchecked") public CircularArrayDeque(int capacity) { this.capacity = capacity; // Java不支持直接创建泛型数组,需要类型转换 this.data = (T[]) new Object[capacity]; this.front = 0; this.size = 0; } /** * 无参构造函数,默认容量8 */ public CircularArrayDeque() { this(8); } /** * 左端添加 - O(1)摊销复杂度 * * 为什么是O(1)? * 1. 指针计算: (front-1+capacity)%capacity - O(1) * 2. 数组赋值: data[index] = value - O(1) * 3. 计数器: size += 1 - O(1) * * @param element 要添加的元素 */ public void appendLeft(T element) { if (size == capacity) { resize(); // 扩容,摊销后O(1) } // 关键:模运算实现环形左端插入 front = (front - 1 + capacity) % capacity; data[front] = element; size++; } /** * 右端添加 - O(1)摊销复杂度 * * @param element 要添加的元素 */ public void appendRight(T element) { if (size == capacity) { resize(); } int rear = (front + size) % capacity; // O(1)计算 data[rear] = element; size++; } /** * 左端删除 - 严格的O(1) * * @return 删除的元素 * @throws IllegalStateException 队列为空时抛出 */ public T popLeft() { if (size == 0) { throw new IllegalStateException("双端队列为空"); } T element = data[front]; // O(1)数组访问 data[front] = null; // 清理引用,帮助GC front = (front + 1) % capacity; // O(1)指针移动 size--; return element; } /** * 右端删除 - 严格的O(1) *
* 为什么是O(1)? * 1. 计算尾部位置: (front+size-1)%capacity - O(1) * 2. 数组访问: data[rear] - O(1) * 3. 清理和计数: O(1) * * @return 删除的元素 * @throws IllegalStateException 队列为空时抛出 */ public T popRight() { if (size == 0) { throw new IllegalStateException("双端队列为空"); } int rear = (front + size - 1) % capacity; // O(1)计算尾部 T element = data[rear]; // O(1)数组访问 data[rear] = null; // 清理引用,帮助GC size--; return element; } /** * 查看左端元素 - O(1) * * @return 左端元素 * @throws IllegalStateException 队列为空时抛出 */ public T peekLeft() { if (size == 0) { throw new IllegalStateException("双端队列为空"); } return data[front]; // 直接访问,O(1) } /** * 查看右端元素 - O(1) * * @return 右端元素 * @throws IllegalStateException 队列为空时抛出 */ public T peekRight() { if (size == 0) { throw new IllegalStateException("双端队列为空"); } int rear = (front + size - 1) % capacity; return data[rear]; } /** * 扩容操作 - 摊销O(1) *
* 为什么扩容是摊销O(1)? * 1. 每次扩容都将容量翻倍 * 2. 触发扩容需要插入capacity个元素 * 3. 复制n个元素的成本被分摊到n次插入中 * 4. 均摊下来每次插入的扩容成本是O(1) */ @SuppressWarnings("unchecked") private void resize() { T[] oldData = data; int oldCapacity = capacity; // 新容量翻倍 capacity = oldCapacity * 2; data = (T[]) new Object[capacity]; // 重新排列数据,消除环形结构 for (int i = 0; i < size; i++) { int oldIndex = (front + i) % oldCapacity; data[i] = oldData[oldIndex]; } front = 0; // 重置front指针 } /** * 检查是否为空 - O(1) * * @return 空返回true,否则false */ public boolean isEmpty() { return size == 0; } /** * 返回元素个数 - O(1) * * @return 元素个数 */ public int size() { return size; } /** * 获取逻辑顺序(用于调试和可视化) * * @return 逻辑顺序的数组 */ public Object[] getLogicalOrder() { Object[] result = new Object[size]; for (int i = 0; i < size; i++) { int index = (front + i) % capacity; result[i] = data[index]; } return result; } /** * 字符串表示 * * @return 格式化的字符串 */ @Override public String toString() { if (isEmpty()) { return "CircularArrayDeque([])"; } StringBuilder sb = new StringBuilder(); sb.append("CircularArrayDeque(["); Object[] logicalOrder = getLogicalOrder(); for (int i = 0; i < logicalOrder.length; i++) { sb.append(logicalOrder[i]); if (i != logicalOrder.length - 1) { sb.append(", "); } } sb.append("])"); return sb.toString(); } // 主方法:功能演示 public static void main(String[] args) { // 创建循环数组双端队列实例,初始容量为4 CircularArrayDequecircularDq = new CircularArrayDeque<>(4); // 1. 基本操作演示 - 每个操作都是O(1) System.out.println("=== 循环数组双端队列演示 ==="); System.out.printf("初始容量: %d, 当前大小: %d%n", 4, circularDq.size()); System.out.println("初始数组状态: " + java.util.Arrays.toString(circularDq.data)); // 演示左端插入的O(1)过程 System.out.println("\n--- 左端插入O(1)演示 ---"); System.out.printf("插入前: front=%d, size=%d%n", 0, circularDq.size()); circularDq.appendLeft("A"); // O(1): 只需(front-1+capacity)%capacity计算 System.out.printf("插入A后: front=%d, size=%d%n", circularDq.front, circularDq.size()); System.out.println("数组状态: " + java.util.Arrays.toString(circularDq.data)); System.out.println("逻辑顺序: " + java.util.Arrays.toString(circularDq.getLogicalOrder())); circularDq.appendLeft("B"); // 再次O(1)插入 System.out.printf("插入B后: front=%d, 逻辑顺序: %s%n", circularDq.front, java.util.Arrays.toString(circularDq.getLogicalOrder())); // 演示右端插入的O(1)过程 System.out.println("\n--- 右端插入O(1)演示 ---"); circularDq.appendRight("C"); // O(1): 只需(front+size)%capacity计算 System.out.println("插入C后: 逻辑顺序: " + java.util.Arrays.toString(circularDq.getLogicalOrder())); System.out.println("数组状态: " + java.util.Arrays.toString(circularDq.data)); circularDq.appendRight("D"); System.out.println("插入D后: 逻辑顺序: " + java.util.Arrays.toString(circularDq.getLogicalOrder())); System.out.printf("当前容量已满: %d/%d%n", circularDq.size(), 4); // 2. 自动扩容演示 - 摊销O(1) System.out.println("\n--- 自动扩容演示 ---"); System.out.printf("扩容前容量: %d, 数据: %s%n", 4, java.util.Arrays.toString(circularDq.data)); circularDq.appendRight("E"); // 这会触发扩容 System.out.printf("扩容后容量: %d, 新front: %d%n", circularDq.capacity, circularDq.front); // 打印扩容后有效数据 Object[] validData = new Object[circularDq.size()]; System.arraycopy(circularDq.data, 0, validData, 0, circularDq.size()); System.out.println("扩容后数据: " + java.util.Arrays.toString(validData)); System.out.println("逻辑顺序保持: " + java.util.Arrays.toString(circularDq.getLogicalOrder())); // 3. 删除操作演示 - 每个都是O(1) System.out.println("\n--- 删除操作演示 ---"); while (circularDq.size() > 0) { if (circularDq.size() % 2 == 1) { // 左端删除 - O(1) String item = circularDq.popLeft(); System.out.printf("左端删除: %s, 剩余: %s, front=%d%n", item, java.util.Arrays.toString(circularDq.getLogicalOrder()), circularDq.front); } else { // 右端删除 - O(1) String item = circularDq.popRight(); System.out.printf("右端删除: %s, 剩余: %s%n", item, java.util.Arrays.toString(circularDq.getLogicalOrder())); } } } }
运行结果:
=== 循环数组双端队列演示 ===
初始容量: 4, 当前大小: 0
初始数组状态: [null, null, null, null]
--- 左端插入O(1)演示 ---
插入前: front=0, size=0
插入A后: front=3, size=1
数组状态: [null, null, null, A]
逻辑顺序: [A]
插入B后: front=2, 逻辑顺序: [B, A]
--- 右端插入O(1)演示 ---
插入C后: 逻辑顺序: [B, A, C]
数组状态: [C, null, B, A]
插入D后: 逻辑顺序: [B, A, C, D]
当前容量已满: 4/4
--- 自动扩容演示 ---
扩容前容量: 4, 数据: [C, D, B, A]
扩容后容量: 8, 新front: 0
扩容后数据: [B, A, C, D, E]
逻辑顺序保持: [B, A, C, D, E]
--- 删除操作演示 ---
左端删除: B, 剩余: [A, C, D, E], front=1
右端删除: E, 剩余: [A, C, D]
左端删除: A, 剩余: [C, D], front=2
右端删除: D, 剩余: [C]
左端删除: C, 剩余: [], front=3
双端队列示例1:集成Java 8+ Stream API + 迭代器(Iterable接口)
import java.util.Iterator;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* 支持Stream和迭代的双端队列(体现Java 8+ Stream API、Iterable接口特性)
*/
public class IterableCircularDeque implements Iterable {
private T[] data;
private int capacity;
private int front;
private int size;
@SuppressWarnings("unchecked")
public IterableCircularDeque(int capacity) {
this.capacity = capacity;
this.data = (T[]) new Object[capacity];
this.front = 0;
this.size = 0;
}
public IterableCircularDeque() {
this(8);
}
// 核心操作:左端添加(保留原逻辑)
public void appendLeft(T element) {
if (size == capacity) resize();
front = (front - 1 + capacity) % capacity;
data[front] = element;
size++;
}
// 核心操作:右端添加(保留原逻辑)
public void appendRight(T element) {
if (size == capacity) resize();
int rear = (front + size) % capacity;
data[rear] = element;
size++;
}
// 核心操作:左端删除(保留原逻辑)
public T popLeft() {
if (size == 0) throw new IllegalStateException("队列为空");
T element = data[front];
data[front] = null;
front = (front + 1) % capacity;
size--;
return element;
}
// 核心操作:右端删除(保留原逻辑)
public T popRight() {
if (size == 0) throw new IllegalStateException("队列为空");
int rear = (front + size - 1) % capacity;
T element = data[rear];
data[rear] = null;
size--;
return element;
}
// 扩容逻辑(保留原逻辑)
@SuppressWarnings("unchecked")
private void resize() {
T[] oldData = data;
capacity *= 2;
data = (T[]) new Object[capacity];
for (int i = 0; i < size; i++) {
int oldIndex = (front + i) % oldData.length;
data[i] = oldData[oldIndex];
}
front = 0;
}
// ========== Java特性:实现Iterable接口,支持foreach遍历 ==========
@Override
public Iterator iterator() {
return new Iterator<>() {
private int current = 0; // 遍历计数器
@Override
public boolean hasNext() {
return current < size;
}
@Override
public T next() {
int index = (front + current) % capacity;
current++;
return data[index];
}
};
}
// ========== Java特性:支持Stream流操作 ==========
public Stream stream() {
// 将迭代器转换为Spliterator,再包装为Stream
return StreamSupport.stream(
Spliterators.spliterator(
iterator(), size, Spliterator.ORDERED),
false // 非并行流
);
}
// ========== Java特性:forEach默认方法(Iterable接口) ==========
@Override
public void forEach(Consumer action) {
// 自定义高效实现,避免迭代器开销
for (int i = 0; i < size; i++) {
int index = (front + i) % capacity;
action.accept(data[index]);
}
}
// 获取大小
public int size() {
return size;
}
// 测试示例
public static void main(String[] args) {
IterableCircularDeque deque = new IterableCircularDeque<>(4);
deque.appendLeft("A");
deque.appendLeft("B");
deque.appendRight("C");
deque.appendRight("D");
// 特性1:foreach遍历(Iterable接口)
System.out.println("=== foreach遍历 ===");
for (String s : deque) {
System.out.print(s + " "); // 输出:B A C D
}
System.out.println();
// 特性2:Stream API操作
System.out.println("\n=== Stream流操作 ===");
deque.stream()
.filter(s -> s.compareTo("B") > 0) // 过滤大于"B"的元素
.map(String::toLowerCase) // 转小写
.forEach(s -> System.out.print(s + " ")); // 输出:a c d
System.out.println();
// 特性3:forEach方法(Lambda)
System.out.println("\n=== forEach Lambda ===");
deque.forEach(s -> System.out.println("元素:" + s));
}
}
双端队列示例2:泛型边界 + 自动资源释放(AutoCloseable)
/**
* 带资源管理的双端队列(体现Java泛型边界、AutoCloseable特性)
* @param 元素类型,必须实现AutoCloseable(支持资源释放)
*/
public class ResourceDeque implements AutoCloseable {
private T[] data;
private int capacity;
private int front;
private int size;
@SuppressWarnings("unchecked")
public ResourceDeque(int capacity) {
this.capacity = capacity;
this.data = (T[]) new AutoCloseable[capacity]; // 泛型边界限定
this.front = 0;
this.size = 0;
}
// 核心操作:右端添加
public void append(T element) {
if (size == capacity) resize();
int rear = (front + size) % capacity;
data[rear] = element;
size++;
}
// 核心操作:左端删除
public T take() {
if (size == 0) throw new IllegalStateException("队列为空");
T element = data[front];
data[front] = null;
front = (front + 1) % capacity;
size--;
return element;
}
// 扩容逻辑
@SuppressWarnings("unchecked")
private void resize() {
T[] oldData = data;
capacity *= 2;
data = (T[]) new AutoCloseable[capacity];
for (int i = 0; i < size; i++) {
int oldIndex = (front + i) % oldData.length;
data[i] = oldData[oldIndex];
}
front = 0;
}
// ========== Java特性:AutoCloseable,自动释放所有资源 ==========
@Override
public void close() throws Exception {
System.out.println("开始释放队列中所有资源...");
// 遍历并关闭所有元素的资源
for (int i = 0; i < size; i++) {
int index = (front + i) % capacity;
if (data[index] != null) {
data[index].close(); // 调用元素的close方法
data[index] = null;
}
}
size = 0;
System.out.println("所有资源释放完成");
}
// 测试示例:模拟管理可关闭的资源(如文件)
static class MockResource implements AutoCloseable {
private final String name;
public MockResource(String name) {
this.name = name;
System.out.println("创建资源:" + name);
}
@Override
public void close() {
System.out.println("释放资源:" + name);
}
@Override
public String toString() {
return name;
}
}
// 测试main方法
public static void main(String[] args) {
// 特性1:try-with-resources自动释放资源
try (ResourceDeque deque = new ResourceDeque<>(2)) {
deque.append(new MockResource("文件1.txt"));
deque.append(new MockResource("数据库连接1"));
MockResource res = deque.take();
System.out.println("取出资源:" + res);
} catch (Exception e) {
e.printStackTrace();
}
// 执行完try块后,会自动调用deque.close(),释放剩余资源
}
}
双端队列示例3:接口抽象 + 默认方法(Java 8+)
/**
* 双端队列接口(体现Java接口抽象、默认方法特性)
*/
interface Deque {
// 抽象方法:核心操作
void appendLeft(T element);
void appendRight(T element);
T popLeft();
T popRight();
int size();
boolean isEmpty();
// ========== Java特性:默认方法(接口自带实现) ==========
// 默认方法:批量添加元素
default void addAll(Iterable elements) {
for (T e : elements) {
appendRight(e); // 复用已有方法
}
}
// 默认方法:清空队列
default void clear() {
while (!isEmpty()) {
popLeft(); // 复用已有方法
}
}
// 默认方法:反转队列(通用逻辑)
default void reverse() {
if (size() <= 1) return;
int n = size();
for (int i = 0; i < n / 2; i++) {
T left = popLeft();
T right = popRight();
appendLeft(right);
appendRight(left);
}
}
}
/**
* 基于循环数组的Deque实现(实现接口,复用默认方法)
*/
class ArrayDequeImpl implements Deque {
private T[] data;
private int capacity;
private int front;
private int size;
@SuppressWarnings("unchecked")
public ArrayDequeImpl(int capacity) {
this.capacity = capacity;
this.data = (T[]) new Object[capacity];
this.front = 0;
this.size = 0;
}
// 实现接口的抽象方法
@Override
public void appendLeft(T element) {
if (size == capacity) resize();
front = (front - 1 + capacity) % capacity;
data[front] = element;
size++;
}
@Override
public void appendRight(T element) {
if (size == capacity) resize();
int rear = (front + size) % capacity;
data[rear] = element;
size++;
}
@Override
public T popLeft() {
if (isEmpty()) throw new IllegalStateException("队列为空");
T element = data[front];
data[front] = null;
front = (front + 1) % capacity;
size--;
return element;
}
@Override
public T popRight() {
if (isEmpty()) throw new IllegalStateException("队列为空");
int rear = (front + size - 1) % capacity;
T element = data[rear];
data[rear] = null;
size--;
return element;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
// 扩容逻辑
@SuppressWarnings("unchecked")
private void resize() {
T[] oldData = data;
capacity *= 2;
data = (T[]) new Object[capacity];
for (int i = 0; i < size; i++) {
int oldIndex = (front + i) % oldData.length;
data[i] = oldData[oldIndex];
}
front = 0;
}
// 辅助方法:打印队列
public void print() {
for (int i = 0; i < size; i++) {
int index = (front + i) % capacity;
System.out.print(data[index] + " ");
}
System.out.println();
}
// 测试示例
public static void main(String[] args) {
Deque deque = new ArrayDequeImpl<>(4);
// 1. 调用接口默认方法:addAll
deque.addAll(java.util.Arrays.asList(1, 2, 3, 4));
System.out.println("初始队列:");
((ArrayDequeImpl) deque).print(); // 输出:1 2 3 4
// 2. 调用接口默认方法:reverse
deque.reverse();
System.out.println("反转后队列:");
((ArrayDequeImpl) deque).print(); // 输出:4 3 2 1
// 3. 调用接口默认方法:clear
deque.clear();
System.out.println("清空后大小:" + deque.size()); // 输出:0
}
}
6️⃣ 时间复杂度分析
| 操作 | 双向链表(Java对应结构) | 动态数组(Java对应结构) | 循环数组(Java对应结构) | Python deque(Java替代方案) |
|---|---|---|---|---|
| 左端插入 | O(1) (Java用,直接操作头尾节点) | O(n) (Java用,需整体后移元素) | O(1) (Java用,模运算定位) | O(1) (Java用替代) |
| 右端插入 | O(1) (直接操作尾节点) | O(1)摊销 (满容量时扩容复制) | O(1) (模运算定位) | O(1) (支持) |
| 左端删除 | O(1) (直接操作头节点) | O(n) (需整体前移元素) | O(1) (仅移动front指针) | O(1) (支持) |
| 右端删除 | O(1) (直接操作尾节点) | O(1) (仅修改size) | O(1) (仅修改size) | O(1) (支持) |
| 随机访问 | O(n) (需遍历查找) | O(1) (下标直接访问) | O(1) (通过计算) | O(n) (对应此特性) |
| 空间开销 | 高 (每个节点存prev/next指针) | 中 (扩容预留冗余空间) | 低 (无额外指针,仅用数组) | 中 (分块平衡开销) |
以下是Java的双端队列API对比表,清晰展示不同实现类的方法、复杂度和适用场景:
Java双端队列(Deque)核心API对比表
| 功能场景 | ArrayDeque(循环数组) | LinkedList(双向链表) | 时间复杂度 | 核心特点 |
|---|---|---|---|---|
| 左端插入 | addFirst(E e) / offerFirst(E e) | addFirst(E e) / offerFirst(E e) | O(1) | offerFirst返回boolean,addFirst抛异常 |
| 右端插入 | addLast(E e) / offerLast(E e) | addLast(E e) / offerLast(E e) | O(1)(ArrayDeque摊销) | ArrayDeque扩容时批量复制,摊销后O(1) |
| 左端删除 | removeFirst() / pollFirst() | removeFirst() / pollFirst() | O(1) | pollFirst为空返回null,removeFirst抛异常 |
| 右端删除 | removeLast() / pollLast() | removeLast() / pollLast() | O(1) | LinkedList需定位尾节点前驱,逻辑O(1) |
| 查看左端元素 | getFirst() / peekFirst() | getFirst() / peekFirst() | O(1) | peekFirst为空返回null,getFirst抛异常 |
| 查看右端元素 | getLast() / peekLast() | getLast() / peekLast() | O(1) | 直接访问头尾指针,无遍历开销 |
| 判断是否为空 | isEmpty() | isEmpty() | O(1) | 均通过size字段直接判断 |
| 获取元素个数 | size() | size() | O(1) | 维护独立size字段,无需计算 |
| 清空队列 | clear() | clear() | O(1) / O(n) | ArrayDeque重置指针;LinkedList需遍历置空节点 |
| 随机访问(第n个元素) | 无原生方法(需手动计算:(front + n) % capacity) | get(int index) | O(1) / O(n) | ArrayDeque手动计算下标O(1);LinkedList遍历O(n) |
| 批量添加 | addAll(Collection<? extends E> c) | addAll(Collection<? extends E> c) | O(k)(k为元素数) | 均从右端批量添加 |
| 包含元素检查 | contains(Object o) | contains(Object o) | O(n) / O(n) | 均需遍历所有元素 |
| 删除指定元素 | remove(Object o) | remove(Object o) | O(n) / O(n) | 找到元素后删除操作O(1),查找O(n) |
补充特性对比(非API)
| 特性维度 | ArrayDeque(循环数组) | LinkedList(双向链表) |
|---|---|---|
| 空间开销 | 低(仅数组+少量指针,扩容预留冗余) | 高(每个节点含prev/next指针,额外内存占用) |
| 缓存友好性 | 高(数组连续内存,CPU缓存命中率高) | 低(节点分散存储,缓存命中率低) |
| 适用场景 | 高频双端操作、随机访问(手动计算下标) | 频繁中间插入/删除、需序列化存储 |
| 线程安全性 | 非线程安全 | 非线程安全 |
| 空元素支持 | 不支持(存储null会抛出异常) | 支持(可存储null元素) |
总结:
- API层面:
ArrayDeque和LinkedList实现了相同的Deque接口,核心双端操作方法完全一致,仅异常处理和返回值规则相同。 - 性能层面:
ArrayDeque在绝大多数场景下性能更优(缓存友好、空间开销低),是Java双端队列的首选。 - 场景选择:仅当需要在队列中间频繁插入/删除,或需存储null元素时,才选择
LinkedList。
7️⃣ 双端队列的典型应用场景
Deque的灵活性使其在多种场景中成为首选数据结构:
- 广度优先搜索 (BFS):性能远超
list.pop(0)。在Python中,collections.deque是BFS的标准配套。 - 滑动窗口:高效地从两端添加和删除元素来维护窗口。这是LeetCode高频题型的核心解法。
- 任务调度(如轮转调度):完美匹配任务队列的需求。
- 撤销/重做功能:使用两个Deque分别模拟历史和未来操作。
- 历史记录:可以非常方便地实现自动淘汰旧记录的容器。
总结
双端队列是一种非常灵活和高效的数据结构,它的核心价值在于:
- 灵活性:可以在两端进行O(1)操作,适应多种算法需求。
- 实用性:在滑动窗口、回文检测、历史记录等场景中发挥重要作用。
- 高效性:相比数组和普通队列,提供了更好的操作效率。
无论你使用Java、Python、C++还是Go,掌握Deque的原理与实现都将显著提升你的代码质量与算法能力。希望本文能帮助你更好地理解并使用这一强大工具!
LinkedListArrayListArrayDequeArrayDequeLinkedListArrayListArrayDequeArrayDequeLinkedListArrayListArrayDequeArrayDequeLinkedListArrayListArrayDequeArrayDequeLinkedListArrayListArrayDeque(front+index)%capacityLinkedListLinkedListArrayListArrayDequeArrayDeque
浙公网安备 33010602011771号