本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。
点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!
系列文章目录
JAVA数据结构 DAY6-栈和队列
拓展目录
手把手教你用 ArrayList 实现杨辉三角:从逻辑推导到每行代码详解
目录
目录
[一、栈(Stack):后进先出的 "有序容器"](#一、栈(Stack):后进先出的 “有序容器”)
[1.1 概念深度剖析](#1.1 概念深度剖析)
[1.2 栈的常用方法与实战示例](#1.2 栈的常用方法与实战示例)
[1.3 栈的模拟实现(基于数组,超详细注释)](#1.3 栈的模拟实现(基于数组,超详细注释))
[1.4 栈的核心应用场景(含真题解析)](#1.4 栈的核心应用场景(含真题解析))
[场景 1:判断出栈序列的合法性](#场景 1:判断出栈序列的合法性)
[场景 2:递归转循环(以逆序打印链表为例)](#场景 2:递归转循环(以逆序打印链表为例))
[场景 3:括号匹配(LeetCode 经典题)](#场景 3:括号匹配(LeetCode 经典题))
[场景 4:其他核心应用](#场景 4:其他核心应用)
[1.5 易混淆概念区分(栈、虚拟机栈、栈帧)](#1.5 易混淆概念区分(栈、虚拟机栈、栈帧))
[二、队列(Queue):先进先出的 "有序队列"](#二、队列(Queue):先进先出的 “有序队列”)
[2.1 概念深度剖析](#2.1 概念深度剖析)
[2.2 队列的常用方法与实战示例](#2.2 队列的常用方法与实战示例)
[2.3 队列的模拟实现(基于双向链表,超详细)](#2.3 队列的模拟实现(基于双向链表,超详细))
[2.4 循环队列(数组实现,解决假溢出问题)](#2.4 循环队列(数组实现,解决假溢出问题))
[2.4.1 为什么需要循环队列?](#2.4.1 为什么需要循环队列?)
[2.4.2 循环队列的核心设计](#2.4.2 循环队列的核心设计)
[2.4.3 循环队列的模拟实现(基于数组,保留空闲位置)](#2.4.3 循环队列的模拟实现(基于数组,保留空闲位置))
[2.5 循环队列的应用场景](#2.5 循环队列的应用场景)
[三、双端队列(Deque):兼具栈与队列特性的 "全能选手"](#三、双端队列(Deque):兼具栈与队列特性的 “全能选手”)
[3.1 概念深度剖析](#3.1 概念深度剖析)
[3.2 Deque 的实现类与使用示例](#3.2 Deque 的实现类与使用示例)
[实战示例:用 Deque 模拟栈和队列](#实战示例:用 Deque 模拟栈和队列)
[3.3 Deque 的优势与应用场景](#3.3 Deque 的优势与应用场景)
[真题 1:用栈实现队列(LeetCode 232)](#真题 1:用栈实现队列(LeetCode 232))
[真题 2:用队列实现栈(LeetCode 225)](#真题 2:用队列实现栈(LeetCode 225))
前言
小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!
在数据结构与算法中,栈和队列是两种基础且核心的线性表变体,它们通过严格的操作规则约束元素的插入与删除,广泛应用于编程开发、算法设计、操作系统等领域。本文将从概念细节、方法拆解、模拟实现、应用场景、面试真题等维度,进行全方位、超详细的解析,帮助读者彻底掌握栈与队列的核心知识。
一、栈(Stack):后进先出的 "有序容器"
1.1 概念深度剖析
栈是一种操作受限的线性表 ,仅允许在表的一端(栈顶)进行插入和删除操作,另一端(栈底)固定不变。其核心特性是后进先出(LIFO,Last In First Out),即最后插入的元素最先被删除,就像日常生活中叠放的书籍 ------ 最后放上的书,能最先被拿到。
核心术语详解
- 栈顶(Top):允许插入和删除操作的一端,栈中元素的 "活跃端",元素入栈和出栈均围绕栈顶进行;
- 栈底(Bottom):固定不变的一端,是栈中最早插入元素的存放位置;
- 压栈 / 入栈(Push):将元素插入栈顶的操作,插入后该元素成为新的栈顶;
- 出栈(Pop):将栈顶元素删除并返回的操作,删除后栈顶指针向下移动(指向原栈顶的下一个元素);
- 空栈:栈中无任何元素的状态,此时栈顶与栈底重合。
现实场景类比
- 叠放的盘子:只能从最上方拿取盘子,也只能在最上方添加盘子;
- 浏览器的历史记录:点击 "后退" 按钮时,最后访问的页面最先被返回;
- 函数调用栈:JVM 中,函数调用时会创建栈帧压入虚拟机栈,函数执行完毕后栈帧出栈,遵循 "后调用先执行完毕" 的规则。
1.2 栈的常用方法与实战示例
Java 中提供了java.util.Stack类实现栈结构,该类继承自Vector(线程安全的动态数组),提供了完整的栈操作方法。以下是方法的详细说明及实战代码(含易错点解析):
核心方法详解
| 方法 | 功能描述 | 注意事项 |
|---|---|---|
Stack() |
构造一个空栈 | 无参数,初始化时栈的容量由Vector默认规则决定(初始容量 10,扩容时翻倍) |
E push(E e) |
将元素e压入栈顶,返回被压入的元素e |
入栈成功后,栈的有效元素个数(size)加 1 |
E pop() |
移除并返回栈顶元素 | 若栈为空,会抛出EmptyStackException,需提前用empty()判断 |
E peek() |
获取栈顶元素(不删除) | 仅查看栈顶,不改变栈的结构;栈为空时抛出EmptyStackException |
int size() |
返回栈中有效元素的个数 | 取值范围为0(空栈)到栈的最大容量 |
boolean empty() |
判断栈是否为空,为空返回true,否则返回false |
等同于size() == 0,但效率更高(直接判断内部计数器) |
实战代码与易错点分析
java
import java.util.Stack;
public class StackDemo {
public static void main(String[] args) {
// 1. 构造空栈
Stack<Integer> stack = new Stack<>();
// 2. 入栈操作(push)
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println("入栈后栈的元素(从栈底到栈顶):1,2,3,4");
// 3. 获取栈顶元素(peek)
Integer top = stack.peek();
System.out.println("当前栈顶元素:" + top); // 输出4(正确,栈顶为最后入栈的元素)
// 4. 获取有效元素个数(size)
System.out.println("栈中元素个数:" + stack.size()); // 输出4(4个元素均未出栈)
// 5. 出栈操作(pop)
Integer pop1 = stack.pop();
System.out.println("第一次出栈元素:" + pop1); // 输出4(栈顶元素出栈)
System.out.println("出栈后栈顶元素:" + stack.peek()); // 输出3(新栈顶为3)
System.out.println("出栈后元素个数:" + stack.size()); // 输出3(个数减1)
// 6. 空栈判断(empty)
if (stack.empty()) {
System.out.println("栈为空");
} else {
System.out.println("栈非空,剩余元素:1,2,3");
}
// 易错点1:栈为空时调用pop()/peek()会抛异常
// 连续出栈3次,使栈为空
stack.pop(); // 出栈3
stack.pop(); // 出栈2
stack.pop(); // 出栈1
System.out.println("连续出栈后栈是否为空:" + stack.empty()); // 输出true
// stack.pop(); // 报错:EmptyStackException,需避免
// 易错点2:push()方法的返回值是被压入的元素,而非栈顶元素(虽结果一致,但语义不同)
Integer pushResult = stack.push(5);
System.out.println("push(5)的返回值:" + pushResult); // 输出5(正确)
}
}
1.3 栈的模拟实现(基于数组,超详细注释)
为深入理解栈的底层原理,我们基于动态数组 手动实现栈(复刻Stack类的核心逻辑,补充完整边界处理):
java
import java.util.Arrays;
public class MyStack {
// 存储栈元素的数组(底层容器)
private int[] array;
// 记录栈中有效元素的个数(栈顶指针的本质:size-1即为栈顶元素的下标)
private int size;
// 栈的初始容量(可自定义,此处设为3)
private static final int INIT_CAPACITY = 3;
// 1. 构造函数:初始化空栈
public MyStack() {
this.array = new int[INIT_CAPACITY];
this.size = 0; // 初始时无有效元素
}
// 2. 入栈操作:将元素压入栈顶
public int push(int e) {
// 先确保数组容量充足,不足则扩容
ensureCapacity();
// 将元素存入栈顶(size为当前有效元素个数,下标为size的位置是新栈顶)
array[size] = e;
size++; // 有效元素个数加1
return e; // 返回被压入的元素
}
// 3. 出栈操作:移除并返回栈顶元素
public int pop() {
// 先判断栈是否为空,空栈直接抛异常(符合Java规范)
if (empty()) {
throw new RuntimeException("栈为空,无法执行出栈操作(EmptyStackException)");
}
// 获取栈顶元素(下标为size-1)
int topElement = array[size - 1];
size--; // 有效元素个数减1(间接实现栈顶指针下移,无需真正删除元素)
return topElement;
}
// 4. 获取栈顶元素(不删除)
public int peek() {
if (empty()) {
throw new RuntimeException("栈为空,无法获取栈顶元素(EmptyStackException)");
}
return array[size - 1];
}
// 5. 获取有效元素个数
public int size() {
return size;
}
// 6. 判断栈是否为空
public boolean empty() {
return size == 0;
}
// 核心辅助方法:确保数组容量充足,不足则扩容(扩容规则:容量翻倍)
private void ensureCapacity() {
// 当有效元素个数等于数组长度时,说明数组已满,需要扩容
if (size == array.length) {
// 扩容:将原数组复制到新数组,新数组长度为原长度的2倍
int newCapacity = array.length * 2;
array = Arrays.copyOf(array, newCapacity);
System.out.println("栈扩容成功:原容量" + array.length/2 + " → 新容量" + newCapacity);
}
}
// 测试方法
public static void main(String[] args) {
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.push(3);
System.out.println("当前栈顶元素:" + myStack.peek()); // 输出3
myStack.push(4); // 触发扩容:原容量3→6
System.out.println("入栈4后栈顶元素:" + myStack.peek()); // 输出4
System.out.println("出栈元素:" + myStack.pop()); // 输出4
System.out.println("出栈后栈顶元素:" + myStack.peek()); // 输出3
System.out.println("栈中元素个数:" + myStack.size()); // 输出3
}
}
模拟实现关键细节
- 栈顶指针的实现 :通过
size变量间接表示栈顶 ------size-1是栈顶元素的下标,无需额外定义指针变量; - 扩容机制 :当数组满时(
size == array.length),通过Arrays.copyOf将容量翻倍,保证入栈操作的连续性; - 边界处理 :空栈时调用
pop()或peek()会抛出明确异常,与 Java 原生Stack类行为一致,增强鲁棒性。
1.4 栈的核心应用场景(含真题解析)
栈的 "后进先出" 特性使其在多个场景中发挥不可替代的作用,以下是最常用的场景及对应的真题详解:
场景 1:判断出栈序列的合法性
核心问题:已知入栈序列,判断某一出栈序列是否合法(入栈过程中可随时出栈)。
真题 1 :若进栈序列为 1,2,3,4,进栈过程中可以出栈,则下列不可能的出栈序列是( )A: 1,4,3,2 B: 2,3,4,1 C: 3,1,4,2 D: 3,4,2,1
解析步骤:
- 遍历出栈序列,对每个元素执行 "入栈到目标元素,然后出栈" 的操作;
- 若过程中栈顶元素与出栈序列当前元素不一致,且无剩余元素可入栈,则序列非法。
选项 C 分析:
- 出栈序列第一个元素为 3:需先入栈 1、2、3,然后出栈 3(栈中剩余 1、2,栈顶为 2);
- 出栈序列第二个元素为 1:此时栈顶为 2,不等于 1,且无更多元素可入栈(入栈序列仅剩余 4),无法得到 1,因此序列非法。
答案:C
真题 2 :一个栈的初始状态为空,现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺序是( )A: 12345ABCDE B: EDCBA54321 C: ABCDE12345 D: 54321EDCBA
解析 :依次入栈后,栈中元素从栈底到栈顶为1、2、3、4、5、A、B、C、D、E,依次出栈遵循 LIFO 原则,最后入栈的 E 最先出栈,依次类推,最终顺序为EDCBA54321。
答案:B
场景 2:递归转循环(以逆序打印链表为例)
递归的底层实现依赖虚拟机栈,当递归深度过大时可能导致栈溢出,此时可通过栈将递归转化为循环。
递归实现:
java
// 递归逆序打印链表(依赖虚拟机栈保存方法调用)
public void printListReverse(Node head) {
if (head != null) {
printListReverse(head.next); // 先递归打印后续节点
System.out.print(head.val + " "); // 最后打印当前节点
}
}
栈实现循环(无递归):
java
// 用栈逆序打印链表(手动模拟递归栈)
public void printListReverseWithStack(Node head) {
if (head == null) {
return;
}
Stack<Node> stack = new Stack<>();
// 第一步:将链表所有节点入栈(从表头到表尾)
Node cur = head;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
// 第二步:出栈并打印(栈顶为链表尾节点,实现逆序)
while (!stack.empty()) {
System.out.print(stack.pop().val + " ");
}
}
// 链表节点定义
class Node {
int val;
Node next;
Node(int val) {
this.val = val;
}
}
核心逻辑:通过栈存储链表节点,入栈顺序为 "表头→表尾",出栈顺序为 "表尾→表头",完美复刻递归的逆序效果,且避免栈溢出风险。
场景 3:括号匹配(LeetCode 经典题)
问题描述 :给定一个只包含'('、')'、'{'、'}'、'['、']'的字符串,判断字符串是否有效(有效条件:括号必须成对出现,且左括号必须与对应的右括号匹配)。
解题思路:
- 遍历字符串,遇到左括号(
(、{、[)则压入栈; - 遇到右括号(
)、}、]),判断栈是否为空(为空则无匹配的左括号,无效); - 若栈非空,弹出栈顶元素,判断是否为对应的左括号(如
)对应(),不匹配则无效; - 遍历结束后,栈必须为空(否则存在未匹配的左括号,无效)。
代码实现:
java
import java.util.Stack;
public class ValidParentheses {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
// 左括号入栈
if (c == '(' || c == '{' || c == '[') {
stack.push(c);
} else {
// 右括号:先判断栈是否为空
if (stack.empty()) {
return false;
}
// 弹出栈顶左括号,判断是否匹配
char top = stack.pop();
if ((c == ')' && top != '(') ||
(c == '}' && top != '{') ||
(c == ']' && top != '[')) {
return false;
}
}
}
// 遍历结束后,栈必须为空(无未匹配的左括号)
return stack.empty();
}
public static void main(String[] args) {
ValidParentheses vp = new ValidParentheses();
System.out.println(vp.isValid("()[]{}")); // true
System.out.println(vp.isValid("(]")); // false
System.out.println(vp.isValid("([)]")); // false
System.out.println(vp.isValid("{[]}")); // true
}
}
场景 4:其他核心应用
- 逆波兰表达式求值:逆波兰表达式(后缀表达式)是 "操作数 + 操作数 + 运算符" 的形式,通过栈存储操作数,遇到运算符时弹出两个操作数计算,结果再入栈;
- 最小栈 :设计一个支持
push、pop、top操作,并能在常数时间内检索到最小元素的栈(解法:维护两个栈,一个存储元素,一个存储当前最小值); - 出栈入栈次序匹配:判断一组入栈和出栈操作序列是否合法(核心:出栈时栈中必须有对应元素)。
1.5 易混淆概念区分(栈、虚拟机栈、栈帧)
很多初学者会混淆 "栈" 的三个相关概念,这里进行明确区分:
| 概念 | 本质 | 归属 | 核心作用 |
|---|---|---|---|
| 栈(数据结构) | 操作受限的线性表 | 数据结构范畴 | 存储数据,遵循 LIFO 原则,用于序列处理、匹配等场景 |
| 虚拟机栈(JVM 内存区域) | JVM 运行时数据区域之一 | JVM 内存模型 | 存储方法调用时的栈帧,用于管理方法的执行与返回 |
| 栈帧(Stack Frame) | 方法调用时创建的内存块 | 虚拟机栈的组成单元 | 每个栈帧包含局部变量表、操作数栈、方法返回地址等,方法调用时入栈,执行完毕后出栈 |
通俗理解:虚拟机栈是 "容器",栈帧是 "容器中的物品",而数据结构中的栈是 "物品的存储规则"------ 虚拟机栈通过栈帧的 "入栈 / 出栈"(遵循 LIFO)来实现方法调用,本质是数据结构栈的实际应用。
二、队列(Queue):先进先出的 "有序队列"
2.1 概念深度剖析
队列是另一种操作受限的线性表 ,仅允许在表的一端(队尾)进行插入操作,在另一端(队头)进行删除操作,核心特性是先进先出(FIFO,First In First Out),即最早插入的元素最先被删除,就像日常生活中排队买票 ------ 先排队的人先购票,后排队的人只能在队尾等待。
核心术语详解
- 队头(Head/Front):允许删除操作的一端,是队列中最早插入元素的存放位置;
- 队尾(Tail/Rear):允许插入操作的一端,是队列中最新插入元素的存放位置;
- 入队(Enqueue):将元素插入队尾的操作,插入后该元素成为新的队尾;
- 出队(Dequeue):将队头元素删除并返回的操作,删除后队头指针向后移动(指向原队头的下一个元素);
- 空队列:队列中无任何元素的状态,此时队头与队尾重合。
现实场景类比
- 排队买票 / 结账:先到先服务,队尾新增排队人员,队头完成服务后离开;
- 打印机队列:多个文档请求打印时,按请求顺序依次打印;
- 消息队列:分布式系统中,消息生产者将消息入队,消费者从队头获取消息,实现异步通信。
2.2 队列的常用方法与实战示例
Java 中Queue是一个接口(而非类),位于java.util包下,其底层通常通过链表实现(因为链表的头尾操作效率更高)。由于接口无法直接实例化,需使用实现类(如LinkedList、ArrayDeque)来创建队列对象。
核心方法详解
| 方法 | 功能描述 | 注意事项 |
|---|---|---|
boolean offer(E e) |
将元素e入队(添加到队尾),成功返回true,失败返回false |
与add(E e)的区别:add失败时抛出异常,offer返回false(推荐使用offer) |
E poll() |
移除并返回队头元素,队列为空时返回null |
与remove()的区别:remove为空时抛出异常,poll返回null(推荐使用poll) |
E peek() |
获取队头元素(不删除),队列为空时返回null |
与element()的区别:element为空时抛出异常,peek返回null(推荐使用peek) |
int size() |
返回队列中有效元素的个数 | 取值范围为0(空队列)到队列的最大容量 |
boolean isEmpty() |
判断队列是否为空,为空返回true,否则返回false |
等同于size() == 0,效率更高 |
实战代码与注意事项
java
import java.util.LinkedList;
import java.util.Queue;
public class QueueDemo {
public static void main(String[] args) {
// 1. 实例化队列(Queue是接口,需用LinkedList实现类)
Queue<Integer> queue = new LinkedList<>();
// 2. 入队操作(offer)
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
queue.offer(5);
System.out.println("入队后队列元素(从队头到队尾):1,2,3,4,5");
// 3. 获取队头元素(peek)
Integer head = queue.peek();
System.out.println("当前队头元素:" + head); // 输出1(队头为最早入队的元素)
// 4. 获取有效元素个数(size)
System.out.println("队列中元素个数:" + queue.size()); // 输出5
// 5. 出队操作(poll)
Integer poll1 = queue.poll();
System.out.println("第一次出队元素:" + poll1); // 输出1(队头元素出队)
System.out.println("出队后队头元素:" + queue.peek()); // 输出2(新队头为2)
System.out.println("出队后元素个数:" + queue.size()); // 输出4
// 6. 空队列判断(isEmpty)
if (queue.isEmpty()) {
System.out.println("队列为空");
} else {
System.out.println("队列非空,剩余元素:2,3,4,5");
}
// 注意事项1:Queue接口不能用Stack实现类实例化(Stack未实现Queue接口)
// Queue<Integer> wrongQueue = new Stack<>(); // 编译报错
// 注意事项2:poll()和peek()在空队列时返回null,而非抛出异常
// 连续出队4次,使队列空
queue.poll(); // 出队2
queue.poll(); // 出队3
queue.poll(); // 出队4
queue.poll(); // 出队5
System.out.println("连续出队后队列是否为空:" + queue.isEmpty()); // 输出true
System.out.println("空队列poll()返回值:" + queue.poll()); // 输出null
System.out.println("空队列peek()返回值:" + queue.peek()); // 输出null
}
}
2.3 队列的模拟实现(基于双向链表,超详细)
队列的底层实现有两种选择:顺序结构(数组)和链式结构(链表)。由于数组实现队列时,出队操作(删除队头元素)需要移动后续所有元素,时间复杂度为 O (n),而链表实现的出队和入队操作时间复杂度均为 O (1),因此推荐使用链表实现队列。
以下是基于双向链表的队列模拟实现(含完整边界处理和测试):
java
public class MyQueue {
// 双向链表节点类(存储队列元素)
private static class ListNode {
int value; // 节点值
ListNode prev; // 前驱节点指针(用于双向链表反向遍历)
ListNode next; // 后继节点指针
// 节点构造函数
public ListNode(int value) {
this.value = value;
this.prev = null;
this.next = null;
}
}
private ListNode first; // 队头指针(指向双向链表的第一个节点)
private ListNode last; // 队尾指针(指向双向链表的最后一个节点)
private int size; // 队列中有效元素的个数
// 1. 构造函数:初始化空队列
public MyQueue() {
this.first = null;
this.last = null;
this.size = 0;
}
// 2. 入队操作:将元素添加到队尾(双向链表的尾部插入)
public void offer(int e) {
ListNode newNode = new ListNode(e); // 创建新节点
if (first == null) { // 队列空时,新节点既是队头也是队尾
first = newNode;
last = newNode;
} else { // 队列非空时,将新节点链接到队尾
last.next = newNode; // 原队尾的next指向新节点
newNode.prev = last; // 新节点的prev指向原队尾
last = newNode; // 更新队尾指针为新节点
}
size++; // 有效元素个数加1
}
// 3. 出队操作:移除并返回队头元素(双向链表的头部删除)
public Integer poll() {
// 情况1:队列为空,返回null
if (first == null) {
return null;
}
// 保存队头元素的值(用于返回)
int headValue = first.value;
// 情况2:队列中只有一个元素(队头和队尾指向同一个节点)
if (first == last) {
first = null;
last = null;
} else { // 情况3:队列中有多个元素
first = first.next; // 队头指针指向原队头的下一个节点
first.prev.next = null; // 断开原队头的next链接(垃圾回收)
first.prev = null; // 新队头的prev置为null(避免循环引用)
}
size--; // 有效元素个数减1
return headValue;
}
// 4. 获取队头元素(不删除)
public Integer peek() {
// 队列为空时返回null
return first == null ? null : first.value;
}
// 5. 获取有效元素个数
public int size() {
return size;
}
// 6. 判断队列是否为空
public boolean isEmpty() {
return first == null;
}
// 测试方法
public static void main(String[] args) {
MyQueue myQueue = new MyQueue();
myQueue.offer(1);
myQueue.offer(2);
myQueue.offer(3);
System.out.println("当前队头元素:" + myQueue.peek()); // 输出1
System.out.println("队列元素个数:" + myQueue.size()); // 输出3
System.out.println("出队元素:" + myQueue.poll()); // 输出1
System.out.println("出队后队头元素:" + myQueue.peek()); // 输出2
System.out.println("出队后元素个数:" + myQueue.size()); // 输出2
myQueue.offer(4);
System.out.println("入队4后队头元素:" + myQueue.peek()); // 输出2
System.out.println("入队4后元素个数:" + myQueue.size()); // 输出3
// 清空队列
myQueue.poll(); // 出队2
myQueue.poll(); // 出队3
myQueue.poll(); // 出队4
System.out.println("清空后队列是否为空:" + myQueue.isEmpty()); // 输出true
System.out.println("空队列poll()返回值:" + myQueue.poll()); // 输出null
}
}
模拟实现关键细节
- 双向链表的优势:相比单向链表,双向链表的头部删除操作更高效(无需遍历找到前驱节点),仅需调整指针即可;
- 队头 / 队尾指针 :通过
first和last指针直接指向队头和队尾节点,入队和出队操作均为 O (1) 时间复杂度; - 边界处理 :队列为空时
poll()和peek()返回null,队列只有一个元素时需同时更新first和last指针,避免空指针异常。
2.4 循环队列(数组实现,解决假溢出问题)
2.4.1 为什么需要循环队列?
普通数组实现的队列存在 "假溢出" 问题:当队尾指针达到数组末尾时,队列看似已满,但数组前端可能存在空闲空间(因元素出队后前端位置空出)。循环队列通过 "下标循环复用" 的方式,解决了假溢出问题,提高了数组空间的利用率。
循环队列的核心思想:将数组视为一个环形空间,队尾指针到达数组末尾时,若数组前端有空闲空间,队尾指针可绕回数组开头继续入队。
2.4.2 循环队列的核心设计
- 底层存储:数组(固定容量,需提前指定);
- 队头指针(front):指向队头元素的下标;
- 队尾指针(rear):指向队尾元素的下一个空闲位置(便于计算入队位置);
- 下标循环技巧 :
- 下标向后移动(入队时 rear 更新):
rear = (rear + 1) % array.length; - 下标向前移动(出队时 front 更新):
front = (front + 1) % array.length;
- 下标向后移动(入队时 rear 更新):
- 空满判断(关键难点) :由于
front == rear既可能表示队列空,也可能表示队列满,需通过以下方式区分:- 方法 1:保留一个空闲位置 :队列满时,
(rear + 1) % array.length == front;队列空时,front == rear(推荐使用,实现简单); - 方法 2:维护 size 变量 :
size == 0表示空,size == array.length表示满; - 方法 3:使用标记位 :新增一个
flag变量,flag == false表示空,flag == true表示满。
- 方法 1:保留一个空闲位置 :队列满时,
2.4.3 循环队列的模拟实现(基于数组,保留空闲位置)
java
public class CircularQueue {
private int[] array; // 存储元素的数组
private int front; // 队头指针(指向队头元素)
private int rear; // 队尾指针(指向队尾元素的下一个空闲位置)
private int capacity; // 队列的最大容量(数组长度)
// 构造函数:指定队列最大容量
public CircularQueue(int capacity) {
this.capacity = capacity;
this.array = new int[capacity];
this.front = 0; // 初始时队头指针为0
this.rear = 0; // 初始时队尾指针为0(队列空)
}
// 1. 入队操作:将元素添加到队尾
public boolean offer(int e) {
// 先判断队列是否满
if (isFull()) {
System.out.println("队列已满,入队失败");
return false;
}
// 将元素存入rear指向的空闲位置
array[rear] = e;
// 队尾指针循环后移
rear = (rear + 1) % capacity;
return true;
}
// 2. 出队操作:移除并返回队头元素
public Integer poll() {
// 先判断队列是否空
if (isEmpty()) {
System.out.println("队列为空,出队失败");
return null;
}
// 保存队头元素的值
int headValue = array[front];
// 队头指针循环后移
front = (front + 1) % capacity;
return headValue;
}
// 3. 获取队头元素(不删除)
public Integer peek() {
if (isEmpty()) {
System.out.println("队列为空,无法获取队头元素");
return null;
}
return array[front];
}
// 4. 判断队列是否满(核心:(rear + 1) % capacity == front)
public boolean isFull() {
return (rear + 1) % capacity == front;
}
// 5. 判断队列是否空(front == rear)
public boolean isEmpty() {
return front == rear;
}
// 6. 获取队列中有效元素个数
public int size() {
return (rear - front + capacity) % capacity;
}
// 测试方法
public static void main(String[] args) {
CircularQueue cq = new CircularQueue(5); // 最大容量5(实际可存储4个元素,保留1个空闲位置)
cq.offer(1);
cq.offer(2);
cq.offer(3);
cq.offer(4);
System.out.println("队列是否满:" + cq.isFull()); // 输出true(4个元素已存满)
cq.offer(5); // 入队失败(队列满)
System.out.println("队头元素:" + cq.peek()); // 输出1
System.out.println("有效元素个数:" + cq.size()); // 输出4
System.out.println("出队元素:" + cq.poll()); // 输出1
System.out.println("出队后队列是否满:" + cq.isFull()); // 输出false
cq.offer(5); // 入队成功
System.out.println("入队5后队头元素:" + cq.peek()); // 输出2
System.out.println("有效元素个数:" + cq.size()); // 输出4
}
}
循环队列关键细节
- 容量说明 :若数组长度为
capacity,采用 "保留一个空闲位置" 的判断方式时,队列的最大存储元素个数为capacity - 1(如数组长度 5,最多存 4 个元素); - 下标循环 :通过取模运算(
% capacity)实现下标绕回,避免数组越界; - 元素个数计算 :
(rear - front + capacity) % capacity,确保在rear < front(队尾绕回数组开头)时也能正确计算个数。
2.5 循环队列的应用场景
- 生产者 - 消费者模型:操作系统中,生产者线程向队列中添加数据,消费者线程从队列中获取数据,循环队列的空间复用特性适合高并发场景;
- 缓冲区设计:如 IO 缓冲区、网络数据缓冲区,通过循环队列暂存数据,避免频繁扩容;
- 滑动窗口算法:部分滑动窗口问题可通过循环队列优化,提高算法效率。
三、双端队列(Deque):兼具栈与队列特性的 "全能选手"
3.1 概念深度剖析
双端队列(Deque,全称 Double Ended Queue)是一种操作不受限的队列 ,允许在队头和队尾两端同时进行入队和出队操作。它融合了栈和队列的特性:
- 若仅使用队头的入队和出队操作,可模拟栈(LIFO);
- 若仅使用队尾入队、队头出队操作,可模拟普通队列(FIFO)。
核心特性
- 两端均可插入元素(
addFirst/offerFirst、addLast/offerLast); - 两端均可删除元素(
removeFirst/pollFirst、removeLast/pollLast); - 可获取两端元素(
getFirst/peekFirst、getLast/peekLast)。
3.2 Deque 的实现类与使用示例
Java 中Deque是接口,继承自Queue接口,常用实现类有:
LinkedList:基于双向链表实现,支持高效的两端操作,适合元素个数不固定的场景;ArrayDeque:基于数组实现,两端操作效率高(比LinkedList更快),适合元素个数固定或变化不大的场景;LinkedBlockingDeque:线程安全的双端队列,适合并发场景。
常用方法(核心)
| 方法 | 功能 |
|---|---|
void addFirst(E e) |
在队头插入元素,失败抛出异常 |
void addLast(E e) |
在队尾插入元素,失败抛出异常 |
boolean offerFirst(E e) |
在队头插入元素,失败返回false |
boolean offerLast(E e) |
在队尾插入元素,失败返回false |
E removeFirst() |
删除并返回队头元素,空队列抛出异常 |
E removeLast() |
删除并返回队尾元素,空队列抛出异常 |
E pollFirst() |
删除并返回队头元素,空队列返回null |
E pollLast() |
删除并返回队尾元素,空队列返回null |
E getFirst() |
获取队头元素,空队列抛出异常 |
E getLast() |
获取队尾元素,空队列抛出异常 |
E peekFirst() |
获取队头元素,空队列返回null |
E peekLast() |
获取队尾元素,空队列返回null |
实战示例:用 Deque 模拟栈和队列
java
import java.util.ArrayDeque;
import java.util.Deque;
public class DequeDemo {
public static void main(String[] args) {
// 1. 用Deque模拟栈(LIFO):仅使用队头的入队和出队操作
Deque<Integer> stack = new ArrayDeque<>();
stack.offerFirst(1); // 队头入队(模拟push)
stack.offerFirst(2);
stack.offerFirst(3);
System.out.println("栈顶元素(peekFirst):" + stack.peekFirst()); // 输出3
System.out.println("出栈元素(pollFirst):" + stack.pollFirst()); // 输出3(模拟pop)
System.out.println("栈中剩余元素个数:" + stack.size()); // 输出2
// 2. 用Deque模拟普通队列(FIFO):队尾入队,队头出队
Deque<Integer> queue = new ArrayDeque<>();
queue.offerLast(1); // 队尾入队(模拟offer)
queue.offerLast(2);
queue.offerLast(3);
System.out.println("队头元素(peekFirst):" + queue.peekFirst()); // 输出1
System.out.println("出队元素(pollFirst):" + queue.pollFirst()); // 输出1(模拟poll)
System.out.println("队列中剩余元素个数:" + queue.size()); // 输出2
// 3. 双端操作示例
Deque<String> deque = new ArrayDeque<>();
deque.offerFirst("A"); // 队头入队:[A]
deque.offerLast("B"); // 队尾入队:[A, B]
deque.offerFirst("C"); // 队头入队:[C, A, B]
System.out.println("队头元素:" + deque.peekFirst()); // 输出C
System.out.println("队尾元素:" + deque.peekLast()); // 输出B
System.out.println("删除队尾元素:" + deque.pollLast()); // 输出B
System.out.println("删除后队列:[C, A]");
}
}
3.3 Deque 的优势与应用场景
- 替代 Stack 类 :Java 原生
Stack类继承自Vector,存在线程安全开销,且方法命名不够直观,Deque的offerFirst/pollFirst可更高效地模拟栈操作; - 替代 Queue 接口 :
Deque兼容Queue的所有方法(如offer等价于offerLast,poll等价于pollFirst),且提供更多双端操作; - 实际应用:滑动窗口最大值(LeetCode 239)、单调队列、表达式求值等算法题中,Deque 是核心数据结构。
四、面试高频真题与解题思路
真题 1:用栈实现队列(LeetCode 232)
题目描述 :请你仅使用两个栈实现先入先出队列。队列应当支持一般队列的支持的所有操作(push、pop、peek、empty)。
解题思路:
- 两个栈分工:
pushStack负责入队(存储元素),popStack负责出队(输出元素); - 入队操作:直接将元素压入
pushStack; - 出队 /peek 操作:若
popStack为空,将pushStack中所有元素弹出并压入popStack(此时popStack的栈顶为队列的队头),再执行出队或 peek。
代码实现:
java
import java.util.Stack;
class MyQueue {
private Stack<Integer> pushStack;
private Stack<Integer> popStack;
public MyQueue() {
pushStack = new Stack<>();
popStack = new Stack<>();
}
// 入队
public void push(int x) {
pushStack.push(x);
}
// 出队
public int pop() {
if (popStack.empty()) {
// 将pushStack的元素转移到popStack
while (!pushStack.empty()) {
popStack.push(pushStack.pop());
}
}
return popStack.pop();
}
// 获取队头元素
public int peek() {
if (popStack.empty()) {
while (!pushStack.empty()) {
popStack.push(pushStack.pop());
}
}
return popStack.peek();
}
// 判断队列是否为空
public boolean empty() {
return pushStack.empty() && popStack.empty();
}
}
真题 2:用队列实现栈(LeetCode 225)
题目描述 :请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、pop、top、empty)。
解题思路:
- 两个队列分工:
activeQueue(活跃队列,存储当前元素)和tempQueue(临时队列,辅助出栈); - 入队操作:直接将元素加入
activeQueue; - 出栈 /top 操作:将
activeQueue中前size-1个元素转移到tempQueue,此时activeQueue中剩余的 1 个元素即为栈顶,执行出栈或 top 后,交换activeQueue和tempQueue的引用。
代码实现:
java
import java.util.LinkedList;
import java.util.Queue;
class MyStack {
private Queue<Integer> activeQueue;
private Queue<Integer> tempQueue;
public MyStack() {
activeQueue = new LinkedList<>();
tempQueue = new LinkedList<>();
}
// 入栈
public void push(int x) {
activeQueue.offer(x);
}
// 出栈
public int pop() {
// 将activeQueue中前size-1个元素转移到tempQueue
while (activeQueue.size() > 1) {
tempQueue.offer(activeQueue.poll());
}
// activeQueue中剩余的1个元素即为栈顶
int top = activeQueue.poll();
// 交换activeQueue和tempQueue的引用
Queue<Integer> temp = activeQueue;
activeQueue = tempQueue;
tempQueue = temp;
return top;
}
// 获取栈顶元素
public int top() {
while (activeQueue.size() > 1) {
tempQueue.offer(activeQueue.poll());
}
int top = activeQueue.peek();
// 注意:top操作不删除元素,需将栈顶元素转移到tempQueue
tempQueue.offer(activeQueue.poll());
Queue<Integer> temp = activeQueue;
activeQueue = tempQueue;
tempQueue = temp;
return top;
}
// 判断栈是否为空
public boolean empty() {
return activeQueue.isEmpty();
}
}
五、总结
栈和队列是两种基础且核心的数据结构,其核心区别在于操作规则:
- 栈:后进先出(LIFO),仅允许栈顶操作,适合逆序处理、匹配、递归转循环等场景;
- 队列:先进先出(FIFO),仅允许队尾入队、队头出队,适合顺序处理、缓冲、异步通信等场景;
- 双端队列(Deque):融合两者特性,两端均可操作,是实际开发中的优选(替代
Stack和Queue)。
掌握栈和队列的关键在于:
- 理解其 "操作受限" 的本质,以及 LIFO/FIFO 特性在实际场景中的应用;
- 熟练掌握模拟实现(数组 / 链表),理解底层原理;
- 攻克面试高频题(用栈实现队列、用队列实现栈、括号匹配等),灵活运用特性解题。
无论是编程入门还是算法进阶,栈和队列都是必须扎实掌握的基础,其思想贯穿于后续的复杂数据结构(如树、图)和算法(如动态规划、贪心)中,打好基础才能稳步提升。

总结
以上就是今天要讲的内容,本文简单记录了java数据结构,仅作为一份简单的笔记使用,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!