队列(Queue 接口)
概念
队列是一种特殊的线性表,只允许在一端进行插入数据操作,在另一端进行删除数据操作。
队列中的数据遵循先进先出 的顺序,FIFO (First In First Out)。

- 队头:进行删除操作的一端称为队头
- 队尾:进行插入操作的一端称为队尾
- 入队:将元素放进队尾,即为入队操作
- 出队:将队列中的首元素进行删除(或移出)
常用方法
| 方法 | 说明 |
|---|---|
boolean offer(E e) |
入队列 |
E poll() |
出队列 |
E peek() |
获取队头元素 |
int size() |
获取队列中有效元素个数 |
boolean isEmpty() |
检测队列是否为空 |
队列的模拟实现
链式结构

1. 单链表
用单链表实现队列,核心思路是维护一个尾节点引用,尾插法入队、头删法出队,做到 O(1) 的入队和出队。
- 添加一个尾节点的引用
- 入队采用尾插法
- 出队采用删除头节点
入队图解:
出队图解:

需要注意:哪怕有尾节点的标记,也不能从头节点入队。因为如果从头入队、从尾出队,删除时需要找尾节点的前一个节点,单链表的节点只有 next 指针,做不到 O(1) 删尾(除非维护的是尾节点的前一个节点的引用,但这样明显增加复杂度,没必要)。
所以单链表实现队列,尾插头删是唯一合理的方案------头删不需要前驱节点,尾插只需要尾节点引用。
2. 双链表
双链表每个节点有 prev 和 next,灵活很多。有两种方式:
- 从头进,从尾出
- 从尾进,从头出
两种方法都可以,这里采用从尾进,从头出 (即尾插法入队、头删法出队)。利用 head 和 last 两个引用,入队和出队都能做到 O(1)。
代码实现如下:
java
public class MyQueue { // 尾出 头进
// 链表节点结构
public static class ListNode {
public ListNode prev;
public ListNode next;
public int val;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head = null; // 队头
public ListNode last = null; // 队尾
public int usedSize = 0; // 有效元素个数
// 入队 ------ 链表尾插法
public void offer(int val) {
ListNode listNode = new ListNode(val);
if (isEmpty()) {
last = head = listNode;
} else {
last.next = listNode;
listNode.prev = last;
last = last.next;
}
this.usedSize++;
}
// 出队 ------ 链表删除头节点
public int poll() {
if (isEmpty()) {
return -1;
}
int tmpVal = head.val;
head = head.next;
if (head != null) {
// 如果只有一个元素,head 会走向 null,后面操作 head.prev 就会报空指针
head.prev = null;
}
this.usedSize--;
return tmpVal;
}
// 获取队头
public int peek() {
if (isEmpty()) {
return -1;
}
return head.val;
}
// 判空
public boolean isEmpty() {
return head == null;
}
// 获取有效元素个数
public int getUsedSize() {
return this.usedSize;
}
}
通过 head 和 last 双标记,入队和出队都是 O(1)。
顺序结构
1. 数组
定义一个 front 与 rear 引用头与尾:
- 入队:从 rear 处放入,
array[rear] = val; rear++ - 出队:从 front 处移除,
front++

但很快会发现一个问题。假设依次入队 1 2 3 4 5:

出队 1 2 3 后:

此刻 front 指向 4(下标 3),rear 指向 5 后面的空位(下标 5)。
此时如果再入队,rear++ 就越界了,虽然对数组进行扩容即可解决。
但前面的位置明明空着,却不用他,会造成资源的浪费------这就是假溢出问题。
解决方案:把数组"魔改"成循环数组 ,让下标能绕回去。
循环数组可以将其抽象看成一个圆环

2. 循环队列
循环队列底层依然是数组,但对下标做了改动:让下标的值一直在 [0, 数组长度) 区间循环波动。
下标计算分两种情况:
情况一:从队尾绕回队头
如图所示:

可以看出从 index → destination,需要向后移动两步:
那么可以发现,通过下面的公式即可计算出destination的下标
index = (index + 2) % 9 = 1
抽象公式:(当前下标 + 待移动步数) % 数组长度
情况二:从队头绕回队尾
如图所示:

从 index → destination,需要向前移动 3 步:
index = ((index + 9) - 3) % 9 = 6
抽象公式:((当前下标 + 数组长度) - 待移动步数) % 数组长度
3. 判空与判满
front == rear 既可能是空,也可能是队列绕了一圈回来了。
如图为循环队列 为空 和 为满 的情况

有三种方式来进行判断:
- 判空 :
front == rear - 判满 :
- 添加 boolean 标记记录是否为满
- 定义 size,
size == 数组长度即为满 - 浪费一个空间,
(rear + 1) % len == front即为满(如上图所示)
4. 数组模拟实现循环队列
有了以上知识作为补充,用数组实现队列就很简单了。
核心思路:维护 front(队头下标)和 rear(队尾下标,指向下一个入队位置)两个变量,入队时在 rear 处放入元素,出队时移动 front。下标通过取模运算循环。
这里使用浪费一个空间的方式来区分空和满(即始终保留一个空位),从尾进、从头出,入队和出队都是 O(1)。
代码实现如下:
java
public class MyCircularQueue {
private int[] array;
private int front; // 队头下标
private int rear; // 队尾下标(指向下一个入队位置)
// 构造时多开一个空间用于区分空/满
public MyCircularQueue(int k) {
array = new int[k + 1];
}
// 入队
public boolean enQueue(int value) {
if (isFull()) {
grow();
return enQueue(value);
}
array[rear] = value;
rear = (rear + 1) % array.length;
return true;
}
// 出队
public boolean deQueue() {
if (isEmpty()) {
return false;
}
front = (front + 1) % array.length;
return true;
}
// 获取队头
public int Front() {
if (isEmpty()) {
return -1;
}
return array[front];
}
// 获取队尾
public int Rear() {
if (isEmpty()) {
return -1;
}
// rear 指向下一个入队位置,队尾是 rear 的前一个
return array[(rear - 1 + array.length) % array.length];
}
// 判空
public boolean isEmpty() {
return front == rear;
}
// 判满
public boolean isFull() {
return (rear + 1) % array.length == front;
}
// 扩容
private void grow() {
int newLen = array.length * 2;
int[] newArray = new int[newLen];
// 按逻辑顺序搬运,重新排列成连续存放
int size = (rear - front + array.length) % array.length;
for (int i = 0; i < size; i++) {
newArray[i] = array[(front + i) % array.length];
}
array = newArray;
front = 0;
rear = size;
}
}
为什么不用Arrays.copyOf 扩容?
你可能会想,扩容直接用 Arrays.copyOf 不就行了?不行 ,这里有个坑。
举个例子就很直观了↓

通过上图,可以分析出以下内容:
扩容前
读取: (4)%7=4→1 (5)%7=5→2 (6)%7=6→3 (7)%7=0→4 ✓
结果: 1, 2, 3, 4 ✓
再分析下扩容后的:
读取: (4)%14=4→1 (5)%14=5→2 (6)%14=6→3 (7)%14=7→0 ✗
结果: 1, 2, 3, 0 ← 元素4丢了,读到的是空位置的默认值0
为什么元素 4 丢了?

直接 Arrays.copyOf 的话,元素 4 就成了孤儿 ,后面的 offer/poll 全都会错位。
这就是为什么扩容时必须手动按逻辑顺序搬运,重新排列成连续存放再重置 front=0, rear=size。
链式 vs 顺序------哪一个更好?
| 对比维度 | 链式队列(双链表) | 顺序队列(循环数组) |
|---|---|---|
| 入队/出队 | O(1),节点指针操作 | O(1),仅移动下标 |
| 内存分配 | 动态分配,入队才 new 节点 | 预分配一整块连续内存 |
| 空间利用率 | 每个节点有 prev/next 额外开销(≈ 16 字节) | 只有数据本身,无额外开销 |
| 扩容代价 | 无需扩容,来一个 new 一个 | 需搬移 + 重建,O(n) |
| CPU 缓存友好 | 节点分散,缓存不友好 | 连续存储,缓存友好 |
| 代码复杂度 | 指针操作多,边界情况多 | 核心是模运算,相对简单 |
实际开发中,LinkedList(双链表)和 ArrayDeque(循环数组)都实现了 Queue。JDK 官方推荐优先用 ArrayDeque,缓存更友好。
用队列实现栈
LeetCode 225. 用队列实现栈
核心思想
队列是 FIFO,栈是 LIFO,顺序是反的。所以每次出栈时,需要把前面的元素全部搬走,才能拿到最后入队的那个元素。
1. 定义两个队列
- q1:入栈队列
- q2:辅助搬运用的空队列
2. 模拟入栈 push()
直接向当前非空的队列中 offer 即可,与队列入队完全一致。
3. 模拟出栈 pop()
思路:将不为空的队列中的 n-1 个元素搬移到另一个队列,剩下的最后一个就是栈顶。
两种实现方案:
方案一(for + size)
java
public int pop() {
int size = q1.size();
for (int i = 0; i < size - 1; i++) {
q2.offer(q1.poll());
}
int top = q1.poll();
// 交换 q1 和 q2
Queue<Integer> tmp = q1;
q1 = q2;
q2 = tmp;
return top;
}
缺点:size 是快照值,逻辑分成「搬 n-1 个」和「取最后 1 个」两步,略显割裂。
方案二(while + tmpVal)
java
public int pop() {
int tmpVal = q1.poll();
while (!q1.isEmpty()) {
q2.offer(tmpVal);
tmpVal = q1.poll();
}
Queue<Integer> tmp = q1;
q1 = q2;
q2 = tmp;
return tmpVal;
}
优点:不依赖 size 快照,逻辑统一,更健壮。
4. 模拟获取栈顶 peek()
思路和 pop 一样,但拿到最后一个元素后,要把它放回去,保证数据不丢失。
java
public int peek() {
int tmpVal = q1.poll();
while (!q1.isEmpty()) {
q2.offer(tmpVal);
tmpVal = q1.poll();
}
q2.offer(tmpVal); // 放回去
Queue<Integer> tmp = q1;
q1 = q2;
q2 = tmp;
return tmpVal;
}
用栈实现队列
完整实现已在 【Java数据结构------栈(Stack)详解】 中写过,包括 push/pop/peek/empty 和均摊 O(1) 分析,不再重复。
核心思路:两个栈 stackIn 和 stackOut,入队压 stackIn,出队时 stackOut 为空就把 stackIn 全部倒入 stackOut(顺序反过来了),再从 stackOut 弹出。
双端队列(Deque)
概念
Deque(Double Ended Queue),两端都可以入队和出队的队列。可以当栈用(LIFO),也能当队列用(FIFO)。
Java Deque 接口
| 队头操作 | 队尾操作 | |
|---|---|---|
| 插入 | addFirst(e) / offerFirst(e) |
addLast(e) / offerLast(e) |
| 删除 | removeFirst() / pollFirst() |
removeLast() / pollLast() |
| 查看 | getFirst() / peekFirst() |
getLast() / peekLast() |
add/remove 系列失败抛异常,offer/poll/peek 系列返回特殊值。
当栈用:
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
System.out.println(stack.pop()); // 2
push 对应 addFirst,pop 对应 removeFirst,队头就是栈顶。
当队列用:
java
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(1); // addLast
queue.offer(2);
System.out.println(queue.poll()); // 1
ArrayDeque vs LinkedList
| ArrayDeque | LinkedList | |
|---|---|---|
| 底层结构 | 循环数组 | 双链表 |
| 栈/队列操作 | O(1) | O(1) |
| 内存 | 连续存储,缓存友好 | 节点分散,不友好 |
| 扩容 | 需搬移 | 无需 |
| 中间操作 | 不支持 | 支持(List 接口) |
| null 元素 | 不允许 | 允许 |
所以写栈时不太推荐使用 Stack 类(Vector 的子类,线程安全但性能差),梗推荐使用:
java
Deque<Integer> stack = new ArrayDeque<>();