文章目录
-
- [一、Java 队列基础回顾](#一、Java 队列基础回顾)
-
- [1. 队列的核心特性](#1. 队列的核心特性)
- [2. Java 中常用队列实现类(按场景分类)](#2. Java 中常用队列实现类(按场景分类))
- [3. 队列核心操作(以`Queue`接口为例)](#3. 队列核心操作(以
Queue
接口为例))
- [二、经典 OJ 题目实战(按难度梯度)](#二、经典 OJ 题目实战(按难度梯度))
-
- [题目 1:用栈实现队列(LeetCode 232)](#题目 1:用栈实现队列(LeetCode 232))
- [题目 2:二叉树的层次遍历(LeetCode 102)](#题目 2:二叉树的层次遍历(LeetCode 102))
- [题目 3:滑动窗口最大值(LeetCode 239)](#题目 3:滑动窗口最大值(LeetCode 239))
- [题目 4:用队列实现栈(LeetCode 225)](#题目 4:用队列实现栈(LeetCode 225))
- [题目 5:任务调度器(LeetCode 621)](#题目 5:任务调度器(LeetCode 621))
- [题目 6:约瑟夫环问题(LeetCode 1823)](#题目 6:约瑟夫环问题(LeetCode 1823))
- [三、队列 OJ 解题总结](#三、队列 OJ 解题总结)
-
- [1. 常见应用场景与队列选择](#1. 常见应用场景与队列选择)
- [2. 常见错误与避坑点](#2. 常见错误与避坑点)
- [3. 学习建议](#3. 学习建议)
- 总结
队列(Queue)是 Java 中核心的 线性数据结构 ,遵循 "先进先出(FIFO,First In First Out)" 规则(特殊队列如优先级队列除外),广泛用于 BFS(广度优先搜索)、任务调度、缓冲处理等场景。本文先梳理队列的基础理论,再通过 6 道经典 OJ 题讲解实战思路,帮助你将理论落地为解题能力。
一、Java 队列基础回顾
在进入 OJ 题之前,需先明确队列的核心概念、分类与 Java 实现类,这是解题的基础。
1. 队列的核心特性
-
FIFO 规则:默认情况下,最早入队的元素最早出队(如排队买票,先到先得);
-
操作受限:仅允许在 "队尾(Rear)" 入队,在 "队头(Front)" 出队,不允许随机访问中间元素;
-
无界 / 有界:
-
无界队列:默认容量可动态扩容(如
LinkedList
、PriorityQueue
); -
有界队列:容量固定,满时入队阻塞 / 失败(如
ArrayBlockingQueue
)。
-
2. Java 中常用队列实现类(按场景分类)
队列类型 | 实现类 | 底层结构 | 核心特点 | 适用场景 |
---|---|---|---|---|
普通队列(FIFO) | LinkedList |
双向链表 | 实现Queue 接口,支持队列基本操作,效率高 |
普通 FIFO 场景(如 BFS、模拟排队) |
双端队列 | ArrayDeque |
循环数组 | 实现Deque 接口,支持队头 / 队尾双向操作,效率优于LinkedList |
栈与队列互转、单调队列(滑动窗口最值) |
优先级队列 | PriorityQueue |
完全二叉树堆 | 非 FIFO,按优先级出队(默认小根堆) | TopK 问题、任务调度(按优先级执行) |
线程安全队列 | ConcurrentLinkedQueue |
链表 | 无界、线程安全,CAS 实现无锁操作 | 多线程下的普通队列场景 |
阻塞队列 | ArrayBlockingQueue |
数组 | 有界、线程安全,满时入队阻塞 / 空时出队阻塞 | 生产者 - 消费者模型(如线程池任务队列) |
3. 队列核心操作(以Queue
接口为例)
方法名 | 功能描述 | 异常情况 / 返回值 |
---|---|---|
boolean offer(E e) |
队尾入队,成功返回true |
无界队列永不失败;有界队列满时返回false |
E poll() |
队头出队,返回出队元素 | 队列为空时返回null |
E peek() |
查看队头元素,不删除 | 队列为空时返回null |
boolean isEmpty() |
判断队列是否为空 | 空返回true ,非空返回false |
int size() |
返回队列元素个数 | - |
注意:
Queue
还有
add()
/
remove()
方法,队满 / 队空时会抛异常(
IllegalStateException
/
NoSuchElementException
),实际开发中优先用
offer()
/
poll()
(返回值判断更安全)。
二、经典 OJ 题目实战(按难度梯度)
以下题目均来自 LeetCode,覆盖队列的核心应用场景,从基础模拟到高级单调队列,每道题包含 "题目描述→解题思路→Java 代码→复杂度分析"。
题目 1:用栈实现队列(LeetCode 232)
题目描述
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
-
void push(int x)
:将元素 x 推到队列的末尾; -
int pop()
:从队列的开头移除并返回元素; -
int peek()
:返回队列开头的元素; -
boolean empty()
:如果队列为空,返回true
;否则,返回false
。
解题思路
-
核心矛盾:栈是 "先进后出(LIFO)",队列是 "先进先出(FIFO)",需用两个栈的 "两次反转" 实现 FIFO;
-
设计两个栈:
-
inStack
:负责 "入队"(所有元素先压入inStack
); -
outStack
:负责 "出队"(当outStack
为空时,将inStack
的所有元素弹出并压入outStack
,此时顺序反转,符合 FIFO)。
-
Java 代码实现
import java.util.Stack;
class MyQueue {
private Stack\<Integer> inStack; // 入队栈
private Stack\<Integer> outStack; // 出队栈
public MyQueue() {
inStack = new Stack<>();
outStack = new Stack<>();
}
// 入队:直接压入inStack
public void push(int x) {
inStack.push(x);
}
// 出队:outStack空则倒入inStack元素,再弹出
public int pop() {
if (outStack.isEmpty()) {
transfer(); // 从inStack倒入outStack
}
return outStack.pop();
}
// 查看队头:逻辑与pop一致,只是不弹出
public int peek() {
if (outStack.isEmpty()) {
transfer();
}
return outStack.peek();
}
// 判断为空:两个栈都为空才是真的空
public boolean empty() {
return inStack.isEmpty() && outStack.isEmpty();
}
// 辅助方法:将inStack的元素全部倒入outStack
private void transfer() {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
}
复杂度分析
-
时间复杂度:
push
/empty
为 O (1);pop
/peek
为 amortized O (1)(每个元素最多被倒入outStack
一次,整体平均 O (1)); -
空间复杂度:O (n)(n 为元素个数,两个栈最多存储 n 个元素)。
题目 2:二叉树的层次遍历(LeetCode 102)
题目描述
给你二叉树的根节点 root
,返回其节点值的层序遍历(即逐层地,从左到右访问所有节点)。
解题思路
-
层序遍历是队列的经典 BFS 应用:队列的 FIFO 特性可保证 "按层访问";
-
步骤:
-
初始化队列,将根节点入队;
-
循环遍历队列,每次取出当前层的所有节点(通过
queue.size()
获取当前层节点数); -
对每个节点,记录其值,再将左、右子节点入队(下一层);
-
直到队列为空,返回所有层的节点值列表。
Java 代码实现
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public List\<List\<Integer>> levelOrder(TreeNode root) {
List\<List\<Integer>> result = new ArrayList<>();
if (root == null) return result; // 空树直接返回
Queue\<TreeNode> queue = new LinkedList<>();
queue.offer(root); // 根节点入队
while (!queue.isEmpty()) {
int levelSize = queue.size(); // 当前层的节点数
List\<Integer> levelList = new ArrayList<>();
// 遍历当前层所有节点
for (int i = 0; i < levelSize; i++) {
TreeNode curr = queue.poll(); // 出队当前节点
levelList.add(curr.val); // 记录节点值
// 左子节点入队(下一层)
if (curr.left != null) queue.offer(curr.left);
// 右子节点入队(下一层)
if (curr.right != null) queue.offer(curr.right);
}
result.add(levelList); // 加入当前层结果
}
return result;
}
}
复杂度分析
-
时间复杂度:O (n)(n 为二叉树节点数,每个节点入队、出队各一次);
-
空间复杂度:O (n)(队列最多存储一层的节点,最坏情况为满二叉树的最后一层,节点数约 n/2)。
题目 3:滑动窗口最大值(LeetCode 239)
题目描述
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。
解题思路
-
核心痛点:暴力法(每次窗口滑动后遍历 k 个元素找最大值)时间复杂度 O (nk),效率低;需用单调递减队列优化;
-
单调队列设计(用
Deque
实现):-
队列中存储的是元素的索引(而非元素值),便于判断元素是否超出当前窗口;
-
维护队列单调性:每次加入新元素前,移除队列中 "比当前元素小" 的所有元素(这些元素不可能成为后续窗口的最大值);
-
维护窗口边界:移除队列中 "索引 ≤ 当前窗口左边界" 的元素(超出窗口范围);
-
队列头部始终是 "当前窗口的最大值索引"。
-
Java 代码实现
import java.util.Deque;
import java.util.LinkedList;
import java.util.Arrays;
class Solution {
public int\[] maxSlidingWindow(int\[] nums, int k) {
if (nums == null || nums.length == 0 || k == 0) return new int\[0];
int n = nums.length;
int\[] result = new int\[n - k + 1]; // 结果数组长度:n - k + 1
Deque\<Integer> deque = new LinkedList<>(); // 单调递减队列(存储索引)
for (int i = 0; i < n; i++) {
// 1. 移除超出当前窗口左边界的元素(窗口左边界 = i - k)
while (!deque.isEmpty() && deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 2. 移除队列中比当前元素小的元素,维护单调递减
while (!deque.isEmpty() && nums\[deque.peekLast()] < nums\[i]) {
deque.pollLast();
}
// 3. 当前元素索引入队
deque.offerLast(i);
// 4. 当i >= k-1时,窗口已形成,记录最大值(队列头部)
if (i >= k - 1) {
result\[i - k + 1] = nums\[deque.peekFirst()];
}
}
return result;
}
}
复杂度分析
-
时间复杂度:O (n)(每个元素入队、出队各一次,队列操作均为 O (1));
-
空间复杂度:O (k)(队列最多存储 k 个元素,即窗口大小)。
题目 4:用队列实现栈(LeetCode 225)
题目描述
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、pop
、top
、empty
)。
解题思路
-
核心矛盾:队列是 FIFO,栈是 LIFO,需用两个队列的 "转移" 实现 LIFO;
-
设计两个队列(
q1
为主队列,q2
为辅助队列):-
push
:直接入队q1
; -
pop
:将q1
中除最后一个元素外的所有元素转移到q2
,弹出q1
的最后一个元素(即栈顶),再将q2
的元素转移回q1
; -
优化:可省略 "转移回
q1
" 的步骤,每次pop
后交换q1
和q2
的引用(q2
变为新的主队列)。
-
Java 代码实现(优化版)
import java.util.LinkedList;
import java.util.Queue;
class MyStack {
private Queue\<Integer> q1; // 主队列(存储元素)
private Queue\<Integer> q2; // 辅助队列(临时转移)
public MyStack() {
q1 = new LinkedList<>();
q2 = new LinkedList<>();
}
// 入栈:直接入队q1
public void push(int x) {
q1.offer(x);
}
// 出栈:将q1除最后一个元素外的元素转移到q2,弹出q1最后一个元素
public int pop() {
// 转移q1的元素到q2(保留最后一个)
while (q1.size() > 1) {
q2.offer(q1.poll());
}
int top = q1.poll(); // 弹出q1最后一个元素(栈顶)
// 交换q1和q2的引用,q2变为新的主队列
Queue\<Integer> temp = q1;
q1 = q2;
q2 = temp;
return top;
}
// 查看栈顶:逻辑与pop一致,只是不弹出,需将元素放回q1
public int top() {
while (q1.size() > 1) {
q2.offer(q1.poll());
}
int top = q1.peek(); // 查看栈顶
q2.offer(q1.poll()); // 将栈顶元素也转移到q2
// 交换引用
Queue\<Integer> temp = q1;
q1 = q2;
q2 = temp;
return top;
}
// 判断为空:主队列q1为空即栈空
public boolean empty() {
return q1.isEmpty();
}
}
复杂度分析
-
时间复杂度:
push
/empty
为 O (1);pop
/top
为 O (n)(需转移 n-1 个元素,n 为元素个数); -
空间复杂度:O (n)(两个队列最多存储 n 个元素)。
题目 5:任务调度器(LeetCode 621)
题目描述
给你一个用字符数组 tasks
表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。
然而,两个相同种类 的任务之间必须有长度为整数 n
的冷却时间,因此至少有连续 n
个单位时间内 CPU 在执行不同的任务,或者在待命状态。
返回完成所有任务所需要的最短时间。
解题思路
-
核心需求:最大化 CPU 利用率,减少待命时间,需优先安排 "出现次数最多的任务"(避免因冷却时间导致大量待命);
-
用优先级队列(大根堆) 存储任务的出现次数(优先处理次数最多的任务);
-
步骤:
-
统计每个任务的出现次数(如用数组计数);
-
将次数 > 0 的任务入大根堆(Java 中
PriorityQueue
默认小根堆,需自定义比较器改为大根堆); -
循环处理堆,每次取
n+1
个任务(一个 "冷却周期" 最多处理 n+1 个不同任务),减少每个任务的次数,若次数仍 > 0 则暂存,待下一轮处理; -
计算时间:若本轮处理了任务,时间增加
n+1
;若仅剩余少量任务(不足 n+1 个),时间增加实际处理的任务数。
Java 代码实现
import java.util.PriorityQueue;
import java.util.Arrays;
class Solution {
public int leastInterval(char\[] tasks, int n) {
// 1. 统计每个任务的出现次数(26个大写字母)
int\[] count = new int\[26];
for (char task : tasks) {
count\[task - 'A']++;
}
// 2. 初始化大根堆(存储任务次数,优先处理次数多的任务)
PriorityQueue\<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
for (int cnt : count) {
if (cnt > 0) {
maxHeap.offer(cnt);
}
}
int time = 0; // 总时间
// 3. 循环处理堆
while (!maxHeap.isEmpty()) {
int cycle = n + 1; // 一个冷却周期的长度
int processed = 0; // 本轮处理的任务数
// 暂存本轮处理后仍有剩余的任务次数
PriorityQueue\<Integer> temp = new PriorityQueue<>((a, b) -> b - a);
// 处理一个冷却周期(最多处理n+1个不同任务)
while (cycle > 0 && !maxHeap.isEmpty()) {
int cnt = maxHeap.poll();
if (cnt > 1) {
temp.offer(cnt - 1); // 次数减1,暂存
}
processed++;
cycle--;
time++;
}
// 将暂存的任务次数放回大根堆
maxHeap.addAll(temp);
// 4. 若堆不为空,说明还有任务未处理,需补全冷却周期的待命时间
if (!maxHeap.isEmpty()) {
time += cycle; // cycle为剩余的待命时间
}
}
return time;
}
}
复杂度分析
-
时间复杂度:O (m log 26) = O (m)(m 为任务总数,堆中最多 26 个元素,每次堆操作 O (log26)≈O (1));
-
空间复杂度:O (26) = O (1)(计数数组和堆的大小均为 26,与任务总数无关)。
题目 6:约瑟夫环问题(LeetCode 1823)
题目描述
共有 n
名小伙伴围坐成一个圈。顺时针从 1
到 n
编号。从第 1
号小伙伴开始顺时针报数,报到 k
的小伙伴出列,接着从下一个小伙伴开始继续报数。求最后一个留在圈里的小伙伴的编号。
解题思路
-
核心是 "模拟出圈过程":队列的 FIFO 特性可模拟 "环形报数"(出队后重新入队,实现循环);
-
步骤:
-
将 1~n 的编号入队;
-
循环报数:每次让前
k-1
个编号出队并重新入队(报数 1~k-1),第k
个编号出队(淘汰); -
重复步骤 2,直到队列中只剩 1 个编号,即为结果。
Java 代码实现
import java.util.LinkedList;
import java.util.Queue;
class Solution {
public int findTheWinner(int n, int k) {
Queue\<Integer> queue = new LinkedList<>();
// 1. 将1\~n的编号入队
for (int i = 1; i <= n; i++) {
queue.offer(i);
}
// 2. 模拟报数淘汰过程
while (queue.size() > 1) {
// 前k-1个编号出队并重新入队(报数1\~k-1)
for (int i = 0; i < k - 1; i++) {
queue.offer(queue.poll());
}
// 第k个编号出队(淘汰)
queue.poll();
}
// 3. 剩余的最后一个编号即为结果
return queue.peek();
}
}
复杂度分析
-
时间复杂度:O (nk)(n 个元素,每个元素需经过 k 次操作才淘汰,最后一个元素需 n-1 次淘汰过程);
-
空间复杂度:O (n)(队列存储 n 个编号)。
注:该题可通过数学公式优化为 O (n) 时间复杂度,但队列模拟更直观,适合初学者理解。
三、队列 OJ 解题总结
1. 常见应用场景与队列选择
解题场景 | 推荐队列类型 | 核心原因 |
---|---|---|
BFS(层次遍历、最短路径) | LinkedList (Queue) |
FIFO 特性,保证按层 / 按步骤访问 |
滑动窗口最值 | ArrayDeque (Deque) |
支持双端操作,可维护单调队列 |
TopK / 任务调度 | PriorityQueue |
按优先级排序,快速获取最值 |
栈与队列互转 | LinkedList (Queue) |
双队列转移,模拟对方的特性 |
环形模拟(约瑟夫环) | LinkedList (Queue) |
出队后重新入队,实现环形结构 |
2. 常见错误与避坑点
-
混淆队列类型 :如用
PriorityQueue
解决 FIFO 问题(如层次遍历),导致顺序错误; -
忘记维护队列边界:如滑动窗口最大值中,未移除超出窗口的元素,导致最大值错误;
-
忽略线程安全 :OJ 题多为单线程场景,无需考虑
ConcurrentLinkedQueue
,避免过度设计; -
效率浪费 :如用
LinkedList
实现双端队列(效率低于ArrayDeque
),或用暴力法替代单调队列。
3. 学习建议
-
先掌握基础队列 :熟悉
Queue
/Deque
接口的核心操作,明确不同实现类的区别; -
从 BFS 入手:层次遍历是队列最基础的 OJ 应用,先练熟该类题目,建立解题手感;
-
突破单调队列:滑动窗口最值是队列的难点,需理解 "单调维护" 的逻辑(为什么要移除小元素);
-
总结模板:如 BFS 模板(队列初始化→循环出队→入队子节点)、单调队列模板(移除超出范围→维护单调性→入队→记录结果),遇到类似题目可快速套用。
总结
Java 队列是 OJ 中的高频考点,核心在于理解不同队列的特性(FIFO、优先级、双端操作),并根据题目需求选择合适的队列类型。通过本文的基础梳理与 6 道经典题实战,你应能掌握队列的核心应用场景与解题思路。后续可进一步练习 "最短路径"(如 LeetCode 127. 单词接龙)、"阻塞队列"(多线程 OJ 题)等进阶题目,深化对队列的理解。