在数据结构的世界里,栈和队列是两种极为基础且应用广泛的线性表结构。它们凭借独特的元素操作规则,在算法设计、系统开发等领域发挥着关键作用。本文将基于经典数据结构理论,从概念定义、常用方法、模拟实现到实际应用,全方位剖析栈与队列,帮助大家夯实基础,应对各类编程场景。
一、栈(Stack):后进先出的“数据容器”
栈就像我们日常生活中叠放的盘子——只能从最顶端拿取盘子,也只能将新盘子放在最顶端。这种“后进先出”的特性,是栈的核心标志。
1.1 栈的核心概念
- 栈顶:允许插入和删除元素的一端,是栈的“活动端”。
- 栈底:固定不动的一端,元素一旦压入栈底,需等上方所有元素出栈后才能访问。
- 压栈(Push):将元素插入栈顶的操作。
- 出栈(Pop):将栈顶元素删除并返回的操作。
用一个简单的示例理解栈的操作流程:
若依次执行 Push(7) → Push(1) → Push(8) → Push(2),栈内元素从栈底到栈顶的顺序为 7 → 1 → 8 → 2;再依次执行 Pop() → Pop() → Pop(),出栈元素顺序为 2 → 8 → 1,剩余栈顶元素为 7。
1.2 栈的常用方法(Java实现)
在Java中,java.util.Stack 类提供了栈的基础实现,常用方法如下表所示:
| 方法名 | 功能描述 | 返回值 |
|---|---|---|
Stack() | 构造一个空栈 | 无 |
push(E e) | 将元素e压入栈顶 | 被压入的元素e |
pop() | 移除并返回栈顶元素 | 栈顶元素(若栈空会抛异常) |
peek() | 获取栈顶元素(不删除) | 栈顶元素(若栈空会抛异常) |
size() | 获取栈中有效元素个数 | 整数(元素个数) |
empty() | 判断栈是否为空 | true(空栈)/false(非空) |
代码示例:栈的基础使用
import java.util.Stack;
public class StackDemo {
public static void main(String[] args) {
// 1. 初始化栈
Stack<Integer> stack = new Stack<>();
// 2. 压栈操作
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println("栈中元素个数:" + stack.size()); // 输出:4
System.out.println("当前栈顶元素:" + stack.peek()); // 输出:4
// 3. 出栈操作
stack.pop(); // 4出栈
System.out.println("出栈后栈顶元素:" + stack.pop()); // 输出:3(3出栈)
// 4. 判断栈是否为空
if (stack.empty()) {
System.out.println("栈空");
} else {
System.out.println("剩余元素个数:" + stack.size()); // 输出:2(剩余1、2)
}
}
}
1.3 栈的模拟实现(基于数组)
Java中的Stack类继承自Vector(线程安全的动态数组),我们可以基于普通数组手动模拟栈的实现,核心思路是:
- 用数组存储元素,
size记录有效元素个数; - 压栈前检查数组容量,不足则扩容(如2倍扩容);
- 出栈/获取栈顶元素前检查栈是否为空,空则抛异常。
代码实现:自定义数组栈
import java.util.Arrays;
public class MyStack {
// 存储元素的数组
private int[] array;
// 栈中有效元素个数
private int size;
// 构造方法:初始化数组容量为3
public MyStack() {
array = new int[3];
}
// 压栈操作
public int push(int e) {
// 检查容量,不足则扩容
ensureCapacity();
array[size++] = e;
return e;
}
// 出栈操作
public int pop() {
// 栈空则抛异常
if (empty()) {
throw new RuntimeException("栈为空,无法执行出栈操作");
}
int topVal = peek();
size--; // 直接通过size减少实现“删除”(后续元素会覆盖)
return topVal;
}
// 获取栈顶元素
public int peek() {
if (empty()) {
throw new RuntimeException("栈为空,无法获取栈顶元素");
}
return array[size - 1];
}
// 获取元素个数
public int size() {
return size;
}
// 判断栈是否为空
public boolean empty() {
return size == 0;
}
// 扩容方法:当元素个数等于数组长度时,扩容为原来的2倍
private void ensureCapacity() {
if (size == array.length) {
array = Arrays.copyOf(array, size * 2);
}
}
// 测试
public static void main(String[] args) {
MyStack stack = new MyStack();
stack.push(5);
stack.push(6);
stack.push(7);
stack.push(8); // 触发扩容(数组长度从3变为6)
System.out.println("栈顶元素:" + stack.peek()); // 输出:8
System.out.println("出栈元素:" + stack.pop()); // 输出:8
System.out.println("剩余元素个数:" + stack.size()); // 输出:3
}
}
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出栈后,栈顶为2, next出栈元素只能是2,而非1,因此C不合法。
2. 将递归转化为循环(以“逆序打印链表”为例)
递归的本质是系统栈的调用,当递归深度过大时可能导致栈溢出,此时可用手动栈将递归改为循环。
递归实现:
public void printListRecursive(Node head) {
if (head != null) {
printListRecursive(head.next); // 先递归到链表尾部
System.out.print(head.val + " "); // 回溯时打印
}
}
栈的循环实现:
import java.util.Stack;
public void printListLoop(Node head) {
if (head == null) return;
Stack<Node> stack = new Stack<>();
Node cur = head;
// 1. 将链表所有节点压入栈
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
// 2. 出栈并打印(逆序)
while (!stack.empty()) {
System.out.print(stack.pop().val + " ");
}
}
3. 其他常见应用
- 括号匹配:用栈存储左括号,遇到右括号时弹出栈顶左括号,判断是否匹配(如LeetCode 20. 有效的括号)。
- 逆波兰表达式求值:将中缀表达式转为后缀表达式(逆波兰式),再用栈计算结果(如LeetCode 150. 逆波兰表达式求值)。
- 最小栈:设计一个栈,能在O(1)时间内获取栈中的最小值(如LeetCode 155. 最小栈)。
1.5 概念辨析:栈、虚拟机栈、栈帧
很多初学者会混淆这三个概念,其实它们分属不同层面:
- 栈:数据结构层面的概念,是一种“后进先出”的线性表。
- 虚拟机栈:Java虚拟机(JVM)的内存区域之一,用于存储方法调用时的局部变量、操作数栈等信息。
- 栈帧:虚拟机栈的基本单位,每个方法调用时会创建一个栈帧,方法执行完后栈帧出栈。
二、队列(Queue):先进先出的“数据管道”
队列类似日常生活中排队买票的场景——先排队的人先买票,后排队的人后买票。这种“先进先出”(FIFO,First In First Out)的特性,是队列的核心标志。
2.1 队列的核心概念
- 队头(Front):允许删除元素的一端,是队列的“出口”。
- 队尾(Rear):允许插入元素的一端,是队列的“入口”。
- 入队(Enqueue):将元素插入队尾的操作。
- 出队(Dequeue):将队头元素删除并返回的操作。
2.2 队列的常用方法(Java实现)
在Java中,Queue是一个接口(而非类),底层通常通过链表实现,常用实现类是LinkedList。Queue的常用方法如下表所示:
| 方法名 | 功能描述 | 返回值 |
|---|---|---|
offer(E e) | 将元素e入队(队尾) | true(成功)/false(失败,如队列满) |
poll() | 移除并返回队头元素 | 队头元素(若队空返回null) |
peek() | 获取队头元素(不删除) | 队头元素(若队空返回null) |
size() | 获取队列中有效元素个数 | 整数(元素个数) |
isEmpty() | 判断队列是否为空 | true(空队)/false(非空) |
代码示例:队列的基础使用
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. 入队操作
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
queue.offer(5);
System.out.println("队列元素个数:" + queue.size()); // 输出:5
System.out.println("当前队头元素:" + queue.peek()); // 输出:1
// 3. 出队操作
queue.poll(); // 1出队
System.out.println("出队后队头元素:" + queue.poll()); // 输出:2(2出队)
// 4. 判断队列是否为空
if (queue.isEmpty()) {
System.out.println("队空");
} else {
System.out.println("剩余元素个数:" + queue.size()); // 输出:3(剩余3、4、5)
}
}
}
2.3 队列的模拟实现(基于双向链表)
队列的实现有两种选择:顺序结构(数组)和链式结构(链表)。由于数组实现队列时,出队操作会导致数组前端出现“空洞”(需移动元素,时间复杂度O(n)),因此链式结构(尤其是双向链表)是队列的更优选择。
核心思路:
- 用双向链表存储元素,
first指向队头,last指向队尾; - 入队:在队尾(
last)添加新节点; - 出队:删除队头(
first)节点; - 用
size记录元素个数,或通过first == null判断队列是否为空。
代码实现:自定义双向链表队列
public class MyQueue {
// 双向链表节点类
private static class ListNode {
ListNode prev; // 前驱节点
ListNode next; // 后继节点
int value; // 节点值
public ListNode(int value) {
this.value = value;
}
}
private ListNode first; // 队头(出队端)
private ListNode last; // 队尾(入队端)
private int size; // 元素个数
// 入队操作(队尾添加)
public void offer(int e) {
ListNode newNode = new ListNode(e);
if (first == null) {
// 队空:新节点既是队头也是队尾
first = newNode;
} else {
// 队非空:链接到队尾
last.next = newNode;
newNode.prev = last;
}
last = newNode; // 更新队尾
size++;
}
// 出队操作(队头删除)
public Integer poll() {
if (first == null) {
// 队空:返回null
return null;
}
int value = first.value;
if (first == last) {
// 只有一个元素:删除后队空
first = null;
last = null;
} else {
// 多个元素:删除队头,更新first
first = first.next;
first.prev.next = null; // 断开原队头的后继
first.prev = null; // 断开新队头的前驱
}
size--;
return value;
}
// 获取队头元素
public Integer peek() {
if (first == null) {
return null;
}
return first.value;
}
// 获取元素个数
public int size() {
return size;
}
// 判断队列是否为空
public boolean isEmpty() {
return first == null;
}
// 测试
public static void main(String[] args) {
MyQueue queue = new MyQueue();
queue.offer(10);
queue.offer(20);
queue.offer(30);
System.out.println("队头元素:" + queue.peek()); // 输出:10
System.out.println("出队元素:" + queue.poll()); // 输出:10
System.out.println("剩余元素个数:" + queue.size()); // 输出:2
}
}
2.4 循环队列:解决数组队列的“空洞”问题
用普通数组实现队列时,出队后数组前端会产生“空洞”(如[null,2,3,4],null即为空洞),导致数组空间利用率低。循环队列通过“下标循环”的技巧,让数组空间可重复利用,是操作系统“生产者-消费者模型”的常用数据结构。
1. 循环队列的核心设计
- 底层存储:数组。
- 下标循环技巧:
- 下标向后移动:
index = (index + offset) % array.length(如index=7,array.length=9,offset=4,结果为2)。 - 下标向前移动:
index = (index + array.length - offset) % array.length(如index=2,array.length=9,offset=4,结果为7)。
- 下标向后移动:
- 空队与满队的判断:
- 方法1:用
size变量记录元素个数(size=0为空,size=array.length为满)。 - 方法2:保留一个数组位置(满队时
(rear + 1) % array.length == front,空队时front == rear)。
- 方法1:用
2. 代码实现:循环队列(基于数组+size判断)
public class CircularQueue {
private int[] array;
private int front; // 队头下标(出队端)
private int rear; // 队尾下标(入队端的下一个位置)
private int size; // 元素个数
// 构造方法:初始化队列容量
public CircularQueue(int capacity) {
array = new int[capacity];
front = 0;
rear = 0;
size = 0;
}
// 入队
public boolean offer(int e) {
if (isFull()) {
// 队列满,入队失败
return false;
}
array[rear] = e;
rear = (rear + 1) % array.length; // 循环移动队尾
size++;
return true;
}
// 出队
public Integer poll() {
if (isEmpty()) {
// 队列空,出队失败
return null;
}
int value = array[front];
front = (front + 1) % array.length; // 循环移动队头
size--;
return value;
}
// 获取队头元素
public Integer peek() {
if (isEmpty()) {
return null;
}
return array[front];
}
// 判断队列是否为空
public boolean isEmpty() {
return size == 0;
}
// 判断队列是否满
public boolean isFull() {
return size == array.length;
}
// 获取元素个数
public int size() {
return size;
}
// 测试
public static void main(String[] args) {
CircularQueue cq = new CircularQueue(3);
System.out.println(cq.offer(1)); // 输出:true
System.out.println(cq.offer(2)); // 输出:true
System.out.println(cq.offer(3)); // 输出:true
System.out.println(cq.offer(4)); // 输出:false(队列满)
System.out.println("队头元素:" + cq.peek()); // 输出:1
System.out.println("出队元素:" + cq.poll()); // 输出:1
System.out.println("入队元素4:" + cq.offer(4)); // 输出:true(利用循环空间)
}
}
2.5 双端队列(Deque):灵活的“双向队列”
双端队列(Deque,Double Ended Queue)是队列的扩展,允许在队头和队尾同时进行入队和出队操作,兼具栈和队列的特性。在实际开发中,Deque的使用频率远高于传统的Stack和Queue。
1. Deque的核心特性
- 可作为栈使用:用
push()(队头入队)和pop()(队头出队)模拟栈的操作。 - 可作为队列使用:用
offer()(队尾入队)和poll()(队头出队)模拟队列的操作。 - Java中,Deque是接口,常用实现类有
LinkedList(链式实现)和ArrayDeque(数组实现,效率更高)。
2. 代码示例:Deque的使用
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;
public class DequeDemo {
public static void main(String[] args) {
// 1. 作为栈使用(ArrayDeque效率更高)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
System.out.println("栈顶元素:" + stack.peek()); // 输出:2
System.out.println("出栈元素:" + stack.pop()); // 输出:2
// 2. 作为队列使用(LinkedList实现)
Deque<Integer> queue = new LinkedList<>();
queue.offer(10);
queue.offer(20);
System.out.println("队头元素:" + queue.peek()); // 输出:10
System.out.println("出队元素:" + queue.poll()); // 输出:10
}
}
三、面试实战:栈与队列的相互实现
栈和队列的相互实现是面试中的经典题型,核心是利用“两个栈/队列”模拟对方的操作规则。
3.1 用队列实现栈(LeetCode 225. 用队列实现栈)
思路
- 用两个队列
q1(主队列)和q2(辅助队列); - 入栈:直接将元素入队
q1; - 出栈:将
q1中除最后一个元素外的所有元素转移到q2,弹出q1的最后一个元素(即栈顶),再将q2的元素转移回q1。
代码实现
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<>();
}
// 入栈
public void push(int x) {
q1.offer(x);
}
// 出栈
public int pop() {
// 将q1的n-1个元素转移到q2
while (q1.size() > 1) {
q2.offer(q1.poll());
}
// 弹出q1的最后一个元素(栈顶)
int topVal = q1.poll();
// 交换q1和q2,让q1始终为主队列
Queue<Integer> temp = q1;
q1 = q2;
q2 = temp;
return topVal;
}
// 获取栈顶元素
public int top() {
int topVal = pop();
push(topVal); // 弹出后重新入栈
return topVal;
}
// 判断栈是否为空
public boolean empty() {
return q1.isEmpty();
}
}
3.2 用栈实现队列(LeetCode 232. 用栈实现队列)
思路
- 用两个栈
stackIn(入队栈)和stackOut(出队栈); - 入队:直接将元素压入
stackIn; - 出队:若
stackOut为空,将stackIn的所有元素转移到stackOut,弹出stackOut的栈顶元素(即队头)。
代码实现
import java.util.Stack;
class MyQueue {
private Stack<Integer> stackIn; // 入队栈
private Stack<Integer> stackOut; // 出队栈
public MyQueue() {
stackIn = new Stack<>();
stackOut = new Stack<>();
}
// 入队
public void push(int x) {
stackIn.push(x);
}
// 出队
public int pop() {
// 若stackOut为空,转移stackIn的元素
if (stackOut.empty()) {
while (!stackIn.empty()) {
stackOut.push(stackIn.pop());
}
}
return stackOut.pop();
}
// 获取队头元素
public int peek() {
int frontVal = pop();
stackOut.push(frontVal); // 弹出后重新压栈
return frontVal;
}
// 判断队列是否为空
public boolean empty() {
return stackIn.empty() && stackOut.empty();
}
}
四、总结
栈和队列是数据结构的基石,它们的核心区别在于元素操作规则:
- 栈:后进先出 ,仅栈顶可操作,适合需要“逆序处理”的场景(如递归转循环、括号匹配)。
- 队列:先进先出 ,队头出、队尾入,适合需要“顺序处理”的场景(如任务调度、生产者-消费者模型)。
掌握栈和队列的实现原理与应用场景,不仅能应对面试中的基础题型,更能为后续复杂算法(如动态规划、图论)的学习打下坚实基础。建议大家结合本文代码,动手实现一遍,加深对这两种结构的理解!
982

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



