在数据结构中,栈和队列是两种极具实用性的特殊线性表。它们在遵循线性表基本存储特性的同时,对元素的插入与删除操作施加了严格的位置限制,这一特性使其在算法设计、JVM 内存管理、操作系统任务调度等场景中发挥着不可替代的作用。
一、栈(Stack)
1.1 栈的核心定义与特性
栈是一种仅允许在固定一端进行插入和删除操作的线性表,该操作端被称为栈顶,另一端则为栈底。其最核心的特性是后进先出(LIFO,Last In First Out) —— 最后进入栈的元素,会成为第一个被弹出的元素。
- 压栈(入栈):栈的插入操作,始终在栈顶执行,新元素入栈后会成为新的栈顶,栈内元素个数随之增加。
- 出栈:栈的删除操作,同样在栈顶执行,栈顶元素弹出后,其下方的元素会成为新的栈顶,栈内元素个数减少。
举个直观例子:若依次将 1、2、3、4 压栈,栈内元素从栈底到栈顶的顺序为 [1,2,3,4];此时执行出栈操作,会先弹出 4,再弹出 3,后续依次为 2、1,完全遵循 “后进先出” 规则。
1.2 栈的常用方法与使用规范
在 Java 中,Stack类封装了栈的核心操作,通过这些方法可便捷地完成栈的创建、元素操作及状态查询,具体方法及功能如下:
| 方法名称 | 功能描述 |
|---|---|
Stack() | 构造一个空栈,初始状态下无任何元素 |
push(E e) | 将元素e压入栈顶,操作成功后返回该元素e |
pop() | 弹出栈顶元素并返回,若栈为空会抛出异常,操作后栈内有效元素个数减少 1 |
peek() | 获取栈顶元素(仅读取不弹出),栈结构保持不变,栈为空时会抛出异常 |
size() | 返回栈内当前有效元素的个数,结果为非负整数 |
empty() | 判断栈是否为空,若栈内无元素返回true,否则返回false |
需要注意的是,Java 中的Stack类继承自Vector,而Vector是线程安全的动态顺序表,因此Stack的所有操作也具备线程安全特性,适合多线程场景下使用。
1.3 栈的模拟实现逻辑
若需手动模拟栈的实现,核心是选择数组作为底层存储结构(数组在随机访问和操作效率上更优),并通过size变量记录栈内有效元素个数,同时处理数组扩容问题,具体步骤如下:
- 初始化:创建一个固定长度的数组(如初始长度为 3),
size初始化为 0,代表栈为空。 - 压栈(push):先检查数组是否已满(
size == 数组长度),若已满则通过Arrays.copyOf()将数组长度翻倍(实现动态扩容);再将元素存入array[size],最后size++。 - 出栈(pop):先调用
peek()获取栈顶元素(即array[size-1]),再将size--(通过逻辑删除实现元素出栈,无需真正清空数组元素),最后返回栈顶元素。 - 获取栈顶(peek):先判断栈是否为空(
size == 0),若为空则抛出 “栈空” 异常,否则返回array[size-1]的值。
1.4 栈的典型应用场景
栈的 “后进先出” 特性使其在多个领域中成为关键工具,常见应用场景包括:
- 元素序列判断:给定入栈序列,判断某一出栈序列是否合法。例如入栈序列为 1,2,3,4,选项 “3,1,4,2” 是不可能的 —— 因为 3 出栈后,栈内剩余元素为 1,2,下一个出栈元素只能是 2,而非 1。
- 递归转循环:递归本质是依赖虚拟机栈保存函数调用状态,若递归深度过大易导致栈溢出。此时可手动用栈模拟递归,例如逆序打印链表:先将链表节点依次压栈,再弹出节点并打印,即可实现逆序效果。
- 其他高频场景:还包括括号匹配(用栈存储左括号,遇到右括号时弹出栈顶匹配)、逆波兰表达式求值(用栈存储操作数,遇到运算符时弹出两个数计算)、最小栈(额外维护一个栈存储当前最小值,实现 O (1) 时间获取最小值)等。
1.5 概念辨析:栈、虚拟机栈、栈帧
很多学习者会混淆这三个概念,实则它们分属不同层面,具体区别如下:
- 栈:数据结构层面,是用于存储数据的容器,遵循 LIFO 原则,可独立于编程语言存在。
- 虚拟机栈:JVM 内存区域层面,是 JVM 为每个线程分配的内存空间,用于存储方法调用时的局部变量、操作数栈等信息。
- 栈帧:虚拟机栈的基本组成单位,每个方法调用时会创建一个栈帧,方法执行完毕后栈帧出栈,释放内存。
二、队列(Queue)
2.1 队列的核心定义与特性
队列与栈相反,是一种 “两端操作受限” 的线性表:仅允许在队尾(Tail/Rear) 进行插入操作(入队),在队头(Head/Front) 进行删除操作(出队),核心特性是先进先出(FIFO,First In First Out) —— 最早入队的元素,会最早出队。
- 入队(Enqueue):在队尾添加元素,新元素入队后成为新的队尾,队列长度增加 1。
- 出队(Dequeue):在队头删除元素,原队头的下一个元素成为新的队头,队列长度减少 1。
例如:依次将 1,2,3,4 入队,队列内顺序为 [1,2,3,4];出队时先弹出 1,再弹出 2,后续依次为 3、4,完全符合 “先进先出” 规则。
2.2 队列的常用方法与使用规范
在 Java 中,Queue是一个接口(而非类),底层通常通过链表实现(链表可避免数组 “假溢出” 问题),实例化时需使用其实现类(如LinkedList)。Queue的核心方法及功能如下:
| 方法名称 | 功能描述 |
|---|---|
offer(E e) | 将元素e入队(添加到队尾),操作成功返回true,失败返回false |
poll() | 出队(删除并返回队头元素),若队列为空返回null,而非抛出异常 |
peek() | 获取队头元素(仅读取不删除),队列为空时返回null |
size() | 返回队列内当前有效元素的个数,结果为非负整数 |
isEmpty() | 判断队列是否为空,若队内无元素返回true,否则返回false |
相较于Stack类,Queue接口的方法设计更贴合队列特性,且poll()和peek()在队空时返回null,避免了异常抛出,使用更灵活。
2.3 队列的模拟实现逻辑
队列的底层实现推荐使用双向链表(双向链表在队头删除和队尾插入操作上效率更高),核心是维护first(队头指针)、last(队尾指针)和size(元素个数)三个属性,具体实现步骤如下:
- 节点设计:双向链表节点包含
value(存储元素值)、prev(前驱指针,指向前一个节点)、next(后继指针,指向后一个节点)。 - 入队(offer):若队列为空(
first == null),则新节点既是队头也是队尾;否则在队尾(last)后添加新节点,更新last为新节点,最后size++。 - 出队(poll):若队列为空返回
null;若队列只有一个元素(first == last),删除后将first和last均设为null;否则删除first节点,更新first为其下一个节点,最后size--。 - 获取队头(peek):若队列为空返回
null,否则返回first.value。
2.4 循环队列:解决数组假溢出的优化方案
在实际开发中(如操作系统生产者 - 消费者模型),常使用循环队列(基于数组实现),其核心是让数组下标 “循环利用”,避免传统数组队列的 “假溢出” 问题(即队头有空位但队尾已达数组边界)。
(1)下标循环的实现技巧
- 下标向后移动(如入队时队尾下标更新):
index = (index + offset) % array.length。例如数组长度为 9,当前下标为 7,偏移量为 4,计算结果为(7+4)%9=2,实现下标从 7 “循环” 到 2。 - 下标向前移动(如出队时队头下标更新):
index = (index + array.length - offset) % array.length。例如数组长度为 9,当前下标为 2,偏移量为 4,计算结果为(2+9-4)%9=7,实现下标从 2 “循环” 到 7。
(2)队列空与满的判断方案
循环队列的关键问题是如何区分 “空” 和 “满”(两者均可能出现rear == front),常见三种解决方案:
- 添加
size属性:size == 0时为队空,size == array.length时为队满。 - 保留一个位置:约定 “队尾的下一个位置是队头” 时为队满,即
(rear + 1) % array.length == front;rear == front时为队空。 - 使用标记变量:通过布尔变量(如
isFull)标记队列状态,入队成功后设为true,出队成功后根据情况设为false。
2.5 双端队列(Deque):灵活的 “双向操作容器”
双端队列(Deque,Double Ended Queue)是队列的扩展形式,允许在两端同时进行入队和出队操作—— 既可以从队头入队 / 出队,也可以从队尾入队 / 出队,灵活性远超普通队列。
- Java 中的 Deque:
Deque是接口,实例化时可选择LinkedList(链式实现,适合频繁插入删除)或ArrayDeque(线性实现,适合随机访问)。 - 实际应用场景:
Deque可同时实现栈和队列的功能 —— 模拟栈时,用addFirst()(压栈)和removeFirst()(出栈);模拟队列时,用addLast()(入队)和removeFirst()(出队),是工程中推荐的替代Stack和普通Queue的方案。
三、栈与队列的相互实现
栈和队列的相互实现是面试中最常见的题目,核心思路是利用 “两个容器” 的元素转移,实现目标数据结构的特性,具体方案如下:
-
用队列实现栈:准备两个队列
Q1和Q2,入栈时将元素加入非空队列;出栈时,将非空队列中除最后一个元素外的所有元素转移到另一个队列,最后弹出剩余的那个元素(该元素即为栈顶元素)。
class MyStack {
//定义两个队列作为底层容器
public Queue<Integer> queue1;
public Queue<Integer> queue2;
//初始化队列
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
//入栈:将元素加入非空队列(若都为空,默认加入queue1)
public void push(int x) {
if (!queue1.isEmpty()) {
queue1.offer(x);
} else if (!queue2.isEmpty()) {
queue2.offer(x);
} else {
queue1.offer(x);
}
}
//出栈:将非空队列中除最后一个元素外的所有元素转移到另一个队列,弹出剩余元素
public int pop() {
if (empty()) {
return -1;
}
if (!queue1.isEmpty()) {
int size = queue1.size();
for (int i = 0; i < size - 1; i++) {
queue2.offer(queue1.poll());
}
return queue1.poll();
} else {
int size = queue2.size();
for (int i = 0; i < size - 1; i++) {
queue1.offer(queue2.poll());
}
return queue2.poll();
}
}
//获取栈顶元素(不弹出)
public int top() {
if (empty()) {
return -1;
}
if (!queue1.isEmpty()) {
int size = queue1.size();
int val = 0;
for (int i = 0; i < size; i++) {
val = queue1.poll();
queue2.offer(val);
}
return val;
} else {
int size = queue2.size();
int val = 0;
for (int i = 0; i < size; i++) {
val = queue2.poll();
queue1.offer(val);
}
return val;
}
}
//判断栈是否为空
public boolean empty() {
return queue1.isEmpty() && queue2.isEmpty();
}
}
-
用栈实现队列:准备两个栈
stack1和stack2,入队时将元素压入stack1;出队时,若stack2为空,将stack1中所有元素弹出并压入stack2,此时stack2的栈顶即为队列的队头,弹出stack2栈顶元素即可完成出队。
class MyQueue {
//定义两个栈作为底层容器(stack1用于入队,stack2用于出队)
public ArrayDeque<Integer> stack1;
public ArrayDeque<Integer> stack2;
//初始化栈(推荐用Deque的实现类,功能更完善)
public MyQueue() {
stack1 = new ArrayDeque<>();
stack2 = new ArrayDeque<>();
}
//入队:直接将元素压入stack1
public void push(int x) {
stack1.push(x);
}
//出队:若stack2为空,将stack1所有元素转移到stack2,弹出stack2栈顶元素(队头)
public int pop() {
if (empty()) {
return -1;
}
//若stack2为空,将stack1元素全部转移到stack2(反转顺序,实现先进先出)
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
//弹出stack2栈顶元素(即原队列的队头)
return stack2.pop();
}
//获取队头元素(不弹出)
public int peek() {
if (empty()) {
return -1;
}
if (stack2.isEmpty()) {
//第一个栈里面所有的元素 放到第二个栈当中
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
//判断队列是否为空
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
四、总结
栈和队列虽同为特殊线性表,但核心特性截然不同:栈遵循 “后进先出”,适合需要 “回溯” 的场景(如递归、括号匹配);队列遵循 “先进先出”,适合需要 “有序处理” 的场景(如任务调度、消息队列)。学习时需重点掌握它们的实现逻辑(数组 / 链表)、核心方法及应用场景,而栈与队列的相互实现则是面试中的高频考点。掌握这些知识点,不仅能轻松应对面试,更能在实际开发中根据场景选择更合适的数据结构,提升程序效率。
1587

被折叠的 条评论
为什么被折叠?



