【Java数据结构】队列详解与经典 OJ 题目实战

文章目录

    • [一、Java 队列基础回顾](#一、Java 队列基础回顾)
      • [1. 队列的核心特性](#1. 队列的核心特性)
      • [2. Java 中常用队列实现类(按场景分类)](#2. Java 中常用队列实现类(按场景分类))
      • [3. 队列核心操作(以`Queue`接口为例)](#3. 队列核心操作(以Queue接口为例))
    • [二、经典 OJ 题目实战(按难度梯度)](#二、经典 OJ 题目实战(按难度梯度))
    • [三、队列 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)" 出队,不允许随机访问中间元素;

  • 无界 / 有界

    • 无界队列:默认容量可动态扩容(如LinkedListPriorityQueue);

    • 有界队列:容量固定,满时入队阻塞 / 失败(如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)

题目描述

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

  • 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 特性可保证 "按层访问";

  • 步骤:

  1. 初始化队列,将根节点入队;

  2. 循环遍历队列,每次取出当前层的所有节点(通过queue.size()获取当前层节点数);

  3. 对每个节点,记录其值,再将左、右子节点入队(下一层);

  4. 直到队列为空,返回所有层的节点值列表。

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)的栈,并支持普通栈的全部四种操作(pushpoptopempty)。

解题思路
  • 核心矛盾:队列是 FIFO,栈是 LIFO,需用两个队列的 "转移" 实现 LIFO;

  • 设计两个队列(q1为主队列,q2为辅助队列):

    • push:直接入队q1

    • pop:将q1中除最后一个元素外的所有元素转移到q2,弹出q1的最后一个元素(即栈顶),再将q2的元素转移回q1

    • 优化:可省略 "转移回q1" 的步骤,每次pop后交换q1q2的引用(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 利用率,减少待命时间,需优先安排 "出现次数最多的任务"(避免因冷却时间导致大量待命);

  • 优先级队列(大根堆) 存储任务的出现次数(优先处理次数最多的任务);

  • 步骤:

  1. 统计每个任务的出现次数(如用数组计数);

  2. 将次数 > 0 的任务入大根堆(Java 中PriorityQueue默认小根堆,需自定义比较器改为大根堆);

  3. 循环处理堆,每次取n+1个任务(一个 "冷却周期" 最多处理 n+1 个不同任务),减少每个任务的次数,若次数仍 > 0 则暂存,待下一轮处理;

  4. 计算时间:若本轮处理了任务,时间增加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 名小伙伴围坐成一个圈。顺时针从 1n 编号。从第 1 号小伙伴开始顺时针报数,报到 k 的小伙伴出列,接着从下一个小伙伴开始继续报数。求最后一个留在圈里的小伙伴的编号。

解题思路
  • 核心是 "模拟出圈过程":队列的 FIFO 特性可模拟 "环形报数"(出队后重新入队,实现循环);

  • 步骤:

  1. 将 1~n 的编号入队;

  2. 循环报数:每次让前k-1个编号出队并重新入队(报数 1~k-1),第k个编号出队(淘汰);

  3. 重复步骤 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. 学习建议

  1. 先掌握基础队列 :熟悉Queue/Deque接口的核心操作,明确不同实现类的区别;

  2. 从 BFS 入手:层次遍历是队列最基础的 OJ 应用,先练熟该类题目,建立解题手感;

  3. 突破单调队列:滑动窗口最值是队列的难点,需理解 "单调维护" 的逻辑(为什么要移除小元素);

  4. 总结模板:如 BFS 模板(队列初始化→循环出队→入队子节点)、单调队列模板(移除超出范围→维护单调性→入队→记录结果),遇到类似题目可快速套用。

总结

Java 队列是 OJ 中的高频考点,核心在于理解不同队列的特性(FIFO、优先级、双端操作),并根据题目需求选择合适的队列类型。通过本文的基础梳理与 6 道经典题实战,你应能掌握队列的核心应用场景与解题思路。后续可进一步练习 "最短路径"(如 LeetCode 127. 单词接龙)、"阻塞队列"(多线程 OJ 题)等进阶题目,深化对队列的理解。

相关推荐
天地人-神君3 小时前
将.idea取消git托管
java·git·intellij-idea
譕痕3 小时前
Idea 启动报 未找到有效的 Maven 安装问题
java·maven·intellij-idea
aramae3 小时前
详细分析平衡树--红黑树(万字长文/图文详解)
开发语言·数据结构·c++·笔记·算法
Mr YiRan3 小时前
多线程性能优化基础
android·java·开发语言·性能优化
CHEN5_023 小时前
【leetcode100】和为k的子数组(两种解法)
java·数据结构·算法
liyi_hz20083 小时前
O2OA (翱途)开发平台新版本发布预告:架构升级、性能跃迁、功能全面进化
android·java·javascript·开源软件
熊猫钓鱼>_>3 小时前
Java String 性能优化与内存管理:现代开发实战指南
java·开发语言·性能优化
华仔啊3 小时前
Spring事件的3种高级玩法,90%的人根本不会用
java·后端
练习时长一年4 小时前
Spring容器的refresh()方法
java·开发语言