一文彻底搞懂「栈和队列」------从零基础到面试常考(含详细 Java 代码)
适合人群:零基础 / 小白 ,刚接触数据结构与 Java
学完收获:能听懂概念、写出代码、看懂面试题,对"栈"和"队列"形成一套完整的知识体系。
目录速览
- 一、为什么要学栈和队列?
- 二、栈(Stack)------像"盘子堆"一样的结构
*- 栈的生活类比
-
- 栈的基本概念和术语
-
- 栈的常见操作
- 三、用 Java 实现一个"顺序栈"(基于数组)
- 四、用 Java 的标准库来使用栈
- 五、队列(Queue)------像"排队买票"一样的结构
*- 队列的生活类比
-
- 队列的基本概念和术语
-
- 队列的常见操作
- 六、用数组实现一个"顺序队列"(简单版)
- 七、进阶:循环队列(解决空间浪费问题)
- 八、用 Java 标准库来使用队列
- 九、栈和队列的典型应用场景
- 十、常见面试题(附思路+代码)
- 十一、栈 vs 队列:一张表帮你对比记忆
- 十二、给初学者的一些小建议
快速记忆小口诀:
- 栈:一头进出,后进先出(像盘子堆)
- 队列:一头进,一头出,先进先出(像排队)
一、为什么要学栈和队列?
- 栈(Stack) 和 队列(Queue) 是最基础的两种数据结构
- 很多高级数据结构和算法都是在它们之上构建的
- 各大公司面试中,这两个是反复考、常考、必考的知识点
先记住一句话:
- 栈:后进先出(LIFO, Last In First Out)
- 队列:先进先出(FIFO, First In First Out)
我们先从生活中的例子理解,再看 Java 代码。
二、栈(Stack)------像"盘子堆"一样的结构
1. 栈的生活类比
想象一下食堂洗好的一摞盘子:
text
盘子示意图(上面是栈顶):
┌───────┐ ← 栈顶 Top
│ 盘子3 │ 后放进去,先拿出来
├───────┤
│ 盘子2 │
├───────┤
│ 盘子1 │ 最先放进去,最后才拿出来
└───────┘ ← 栈底 Bottom
- 新盘子只能放在最上面(压在上一个盘子上)
- 要拿盘子时,也只能从最上面拿
所以:
- 最后放上去的盘子,最先被拿走 ------ 这就是 后进先出(LIFO)
2. 栈的基本概念和术语
- 压栈(push):向栈顶放入一个元素
- 弹栈(pop):从栈顶取出一个元素
- 栈顶(top):当前可以操作的"最上面"的那个元素
- 栈底(bottom):最早被压入、在最下面的元素
- 空栈:栈中没有任何元素
3. 栈的常见操作
一般包含以下几个方法(用伪代码描述):
- push(x) :把元素
x压入栈顶 - pop():弹出栈顶元素,并返回
- peek() / top():只看一眼栈顶元素,但不删除
- isEmpty():栈是否为空
- size():栈中有多少个元素
三、用 Java 实现一个"顺序栈"(基于数组)
我们先不用现成的 java.util.Stack,自己实现一个栈,这样理解更深刻。
1. 核心设计思路
- 用一个 数组
data[]来存元素 - 用一个 整型变量
top来记录"当前栈顶的位置"
约定:
- 当栈为空时:
top = -1 - 当有元素时:
- 栈顶元素在
data[top] - 每次
push:先top++,然后赋值data[top] = x - 每次
pop:取出data[top],然后top--
- 栈顶元素在
2. 代码:基于数组实现的栈(含详细注释)
java
/**
* 一个简单的顺序栈实现(基于数组)
* 为了方便理解,我们只存 int 类型
*/
public class ArrayStack {
// 用数组存储栈中的元素
private int[] data;
// top 代表"栈顶"下标,-1 代表空栈
private int top;
// 构造方法:指定栈的容量
public ArrayStack(int capacity) {
data = new int[capacity]; // 初始化数组
top = -1; // 一开始是空栈
}
/**
* 入栈(压栈)操作
* @param value 要压入栈顶的元素
* @return 是否压入成功
*/
public boolean push(int value) {
// 栈满的判断:top 已经到了数组最后一个下标
if (top == data.length - 1) {
System.out.println("栈满了,无法再压栈!");
return false;
}
// 先移动 top,再赋值
top++;
data[top] = value;
return true;
}
/**
* 出栈(弹栈)操作
* @return 栈顶元素;如果为空,则抛出异常或返回一个特殊值
*/
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈为空,无法弹栈!");
}
// 先取值,再移动 top
int value = data[top];
top--;
return value;
}
/**
* 查看栈顶元素(但不删除)
*/
public int peek() {
if (isEmpty()) {
throw new RuntimeException("栈为空,没有栈顶元素!");
}
return data[top];
}
/**
* 栈是否为空
*/
public boolean isEmpty() {
return top == -1;
}
/**
* 当前栈中元素个数
*/
public int size() {
return top + 1;
}
/**
* 打印当前栈中元素(从栈底到栈顶)
*/
public void printStack() {
if (isEmpty()) {
System.out.println("栈是空的");
return;
}
System.out.print("栈中元素(从栈底到栈顶):");
for (int i = 0; i <= top; i++) {
System.out.print(data[i] + " ");
}
System.out.println();
}
// 简单测试一下
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(5);
stack.push(10);
stack.push(20);
stack.push(30);
stack.printStack(); // 10 20 30
System.out.println("当前栈顶元素:" + stack.peek()); // 30
int popped = stack.pop();
System.out.println("弹出了元素:" + popped); // 30
stack.printStack(); // 10 20
System.out.println("当前栈大小:" + stack.size()); // 2
}
}
四、用 Java 的标准库来使用栈
在真实开发中,我们通常不会自己写栈,而是用 JDK 提供的容器。
1. 方式一:java.util.Stack
java
import java.util.Stack;
public class StackDemo {
public static void main(String[] args) {
// 创建一个栈,元素类型为 Integer
Stack<Integer> stack = new Stack<>();
// 压栈
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("栈顶元素:" + stack.peek()); // 3
// 弹栈
System.out.println("弹出:" + stack.pop()); // 3
System.out.println("弹出:" + stack.pop()); // 2
// 判断是否为空
System.out.println("栈是否为空:" + stack.isEmpty()); // false
}
}
2. 方式二(面试常问):用 Deque 代替 Stack
Java 官方文档更推荐使用 Deque(双端队列)来实现栈,因为 Stack 是比较老的类,基于 Vector,有一些性能/设计上的历史包袱。
java
import java.util.ArrayDeque;
import java.util.Deque;
public class DequeAsStackDemo {
public static void main(String[] args) {
// 用 Deque 来模拟栈
Deque<Integer> stack = new ArrayDeque<>();
// 压栈:使用 push
stack.push(10);
stack.push(20);
stack.push(30);
// 查看栈顶元素
System.out.println("栈顶元素:" + stack.peek()); // 30
// 弹栈
System.out.println("弹出:" + stack.pop()); // 30
System.out.println("弹出:" + stack.pop()); // 20
System.out.println("是否为空:" + stack.isEmpty()); // false
}
}
面试小知识 :
问:Java 中实现栈用什么?
答:可以用
Stack,但更推荐使用Deque的实现类,比如ArrayDeque来实现栈结构。
五、队列(Queue)------像"排队买票"一样的结构
1. 队列的生活类比
最经典的例子:排队买奶茶。
- 最先排队的人,最先买到奶茶,然后离开队伍
- 新来的人,排到队伍的末尾
所以队列的规则是:
- 先来先服务:先进先出(FIFO, First In First Out)
2. 队列的基本概念和术语
- 入队(enqueue / offer):从队尾插入一个元素
- 出队(dequeue / poll):从队头取出一个元素
- 队头(front / head):下一个要被取出的那个元素
- 队尾(rear / tail):最后进入队列的那个元素
- 空队列:没有任何元素
3. 队列的常见操作
text
队列方向示意图:
入队方向 → [ 队头 ... 队列中间 ... 队尾 ] → 出队方向
front rear
- offer(x) / add(x):入队
- poll() / remove():出队
- peek() / element():只看一下队头元素,不删除
- isEmpty():是否为空
- size():当前元素个数
六、用数组实现一个"顺序队列"(最简单版本)
为了更容易理解,我们先写一个最简单的数组队列版本,暂时不考虑"循环利用空间"的问题。
1. 核心思路
- 用一个数组
data[]存元素 - 用两个指针(下标)
front:指向队头元素的位置rear:指向队尾后面一个位置(也可以理解为"下一个可以插入的位置")
简单版本约定:
- 初始时:
front = 0,rear = 0,队列为空 - 每次入队:把元素放在
data[rear],然后rear++ - 每次出队:返回
data[front],然后front++ - 当
front == rear:队列为空
缺点:当 rear 走到数组末尾时,即使前面有空间,也不能再插入(浪费空间)。
不过没关系,这个版本先帮你理解基本原理,后面我们再改进成循环队列。
2. 代码:简单数组队列实现
java
/**
* 一个简单的顺序队列(基于数组,不循环利用空间)
*/
public class SimpleArrayQueue {
private int[] data;
private int front; // 指向队头元素
private int rear; // 指向队尾后面一个位置(下一次入队的位置)
public SimpleArrayQueue(int capacity) {
data = new int[capacity];
front = 0;
rear = 0;
}
/**
* 入队
*/
public boolean offer(int value) {
if (rear == data.length) {
System.out.println("队列已满(简单实现,不可再入队)");
return false;
}
data[rear] = value;
rear++;
return true;
}
/**
* 出队
*/
public int poll() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法出队!");
}
int value = data[front];
front++;
return value;
}
/**
* 查看队头元素
*/
public int peek() {
if (isEmpty()) {
throw new RuntimeException("队列为空,没有队头元素!");
}
return data[front];
}
/**
* 是否为空
*/
public boolean isEmpty() {
return front == rear;
}
/**
* 当前元素个数
*/
public int size() {
return rear - front;
}
public void printQueue() {
if (isEmpty()) {
System.out.println("队列为空");
return;
}
System.out.print("队列中的元素(从队头到队尾):");
for (int i = front; i < rear; i++) {
System.out.print(data[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
SimpleArrayQueue queue = new SimpleArrayQueue(5);
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.printQueue(); // 1 2 3
System.out.println("队头元素:" + queue.peek()); // 1
System.out.println("出队:" + queue.poll()); // 1
queue.printQueue(); // 2 3
}
}
七、进阶:循环队列(解决空间浪费问题)
刚刚的简单队列有一个问题:前面空出来的位置用不了 。
解决办法:把数组当成一个"环"来使用,这就是循环队列(circular queue)。
1. 循环队列的思想(模运算)
我们依然用 front 和 rear 两个指针,只是每次移动时都让下标绕圈:
- 入队时:
rear = (rear + 1) % capacity - 出队时:
front = (front + 1) % capacity
为了区分"队满"和"队空",常用办法是:
- 保留一个空位
- 约定:
- 当
(rear + 1) % capacity == front时,队列"满" - 当
front == rear时,队列"空"
- 当
2. 代码:循环队列实现
java
/**
* 循环队列实现(基于数组,保留一个空位来区分满和空)
*/
public class CircularQueue {
private int[] data;
private int front; // 队头下标
private int rear; // 队尾后一个位置
public CircularQueue(int capacity) {
// 注意:实际可用的元素个数是 capacity - 1(因为要空出一个)
data = new int[capacity];
front = 0;
rear = 0;
}
/**
* 队列是否为空
*/
public boolean isEmpty() {
return front == rear;
}
/**
* 队列是否已满
* 条件:再往前走一步就撞上 front 了
*/
public boolean isFull() {
return (rear + 1) % data.length == front;
}
/**
* 入队操作
*/
public boolean offer(int value) {
if (isFull()) {
System.out.println("循环队列已满,无法入队!");
return false;
}
data[rear] = value;
rear = (rear + 1) % data.length; // 让 rear 向前走一步(绕圈)
return true;
}
/**
* 出队操作
*/
public int poll() {
if (isEmpty()) {
throw new RuntimeException("循环队列为空,无法出队!");
}
int value = data[front];
front = (front + 1) % data.length; // front 向前走一步
return value;
}
/**
* 查看队头元素
*/
public int peek() {
if (isEmpty()) {
throw new RuntimeException("循环队列为空!");
}
return data[front];
}
/**
* 当前元素个数
*/
public int size() {
// 环形队列的长度计算公式
return (rear - front + data.length) % data.length;
}
public void printQueue() {
if (isEmpty()) {
System.out.println("队列为空");
return;
}
System.out.print("队列中的元素(从队头到队尾):");
int i = front;
while (i != rear) {
System.out.print(data[i] + " ");
i = (i + 1) % data.length;
}
System.out.println();
}
public static void main(String[] args) {
// 注意:数组长度为 5,最多只能存 4 个元素
CircularQueue queue = new CircularQueue(5);
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
queue.printQueue(); // 1 2 3 4
System.out.println("队列是否已满:" + queue.isFull()); // true
System.out.println("出队:" + queue.poll()); // 1
System.out.println("出队:" + queue.poll()); // 2
queue.printQueue(); // 3 4
queue.offer(5);
queue.offer(6);
queue.printQueue(); // 3 4 5 6 (通过"绕圈"复用了空间)
}
}
八、用 Java 标准库来使用队列
Java 中常用的队列接口是 java.util.Queue,最常见的实现类有:
LinkedList:基于链表的队列ArrayDeque:基于数组的双端队列(既可以当队列,也可以当栈)
1. 使用 LinkedList 实现队列
java
import java.util.LinkedList;
import java.util.Queue;
public class LinkedListQueueDemo {
public static void main(String[] args) {
// 使用 LinkedList 来实现 Queue 接口
Queue<Integer> queue = new LinkedList<>();
// 入队
queue.offer(10); // 推荐使用 offer,队满时返回 false 不会抛异常
queue.offer(20);
queue.offer(30);
System.out.println("队头元素:" + queue.peek()); // 10
// 出队
System.out.println("出队:" + queue.poll()); // 10
System.out.println("出队:" + queue.poll()); // 20
System.out.println("队列是否为空:" + queue.isEmpty()); // false
}
}
2. 使用 ArrayDeque 作为队列
java
import java.util.ArrayDeque;
import java.util.Queue;
public class ArrayDequeQueueDemo {
public static void main(String[] args) {
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
System.out.println("队头:" + queue.peek()); // 1
System.out.println("出队:" + queue.poll()); // 1
System.out.println("出队:" + queue.poll()); // 2
}
}
小提示:
offer()/poll()/peek():推荐在队列中使用,更安全,不容易抛异常add()/remove()/element():在特殊情况下(队满、队空)会抛出异常
九、栈和队列在实际中的典型应用(帮助你更好理解)
1. 栈的典型应用
- 函数调用栈
- 每调用一个方法,就压入栈
- 方法执行完,就从栈中弹出
- 这就是为什么有"栈溢出(StackOverflowError)"
- 撤销(Undo)功能
- 比如文本编辑器的"撤销":
- 每次操作压栈
- 撤销时从栈顶取出上一步操作
- 比如文本编辑器的"撤销":
- 括号匹配 (面试高频)
- 如判断
({[]})是否是合法括号序列
- 如判断
- 表达式求值
- 中缀表达式转后缀表达式
- 计算后缀表达式等
2. 队列的典型应用
- 操作系统的任务调度
- 等待执行的任务,通常排队处理
- 消息队列(MQ)
- Kafka、RabbitMQ 等系统本质上都在实现队列结构
- 广度优先搜索(BFS)
- 图/树的层序遍历,用队列来实现
- 排队业务场景 :
- 银行排号、打印任务排队、网络请求处理等
十、常见面试题(附详解答案)
下面整理一些关于栈和队列的常见面试题,适合初级同学。
面试题 1:用栈实现队列
题目大意 :
请用两个栈,实现一个队列,要求实现队列的两个基本操作:入队(push) 和 出队(pop)。
1. 思路讲解
队列是 先进先出 ,栈是 后进先出 。
用两个栈倒来倒去,可以"反转顺序",从而模拟队列:
- 准备两个栈:
stackIn和stackOut - 入队时:永远往
stackIn里压栈 - 出队时:
- 如果
stackOut不为空:直接从stackOut弹栈 - 如果
stackOut为空:把stackIn中的所有元素依次弹出,压入stackOut - 然后再从
stackOut弹出元素
- 如果
这样:
- 进入顺序:
1, 2, 3(入stackIn) - 第一次出队时:
- 把
stackIn中的1, 2, 3依次弹出压入stackOut,顺序变成3, 2, 1 - 然后从
stackOut弹出栈顶1------ 就实现了"先来的先出队"
- 把
2. 代码实现(Java)
java
import java.util.Stack;
/**
* 用两个栈实现一个队列
*/
public class MyQueueByTwoStacks {
private Stack<Integer> stackIn; // 负责入队
private Stack<Integer> stackOut; // 负责出队
public MyQueueByTwoStacks() {
stackIn = new Stack<>();
stackOut = new Stack<>();
}
/**
* 入队操作
*/
public void offer(int value) {
stackIn.push(value);
}
/**
* 出队操作
*/
public int poll() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法出队!");
}
// 如果 stackOut 为空,就把 stackIn 的元素全部倒过去
if (stackOut.isEmpty()) {
while (!stackIn.isEmpty()) {
stackOut.push(stackIn.pop());
}
}
return stackOut.pop();
}
/**
* 查看队头元素
*/
public int peek() {
if (isEmpty()) {
throw new RuntimeException("队列为空!");
}
if (stackOut.isEmpty()) {
while (!stackIn.isEmpty()) {
stackOut.push(stackIn.pop());
}
}
return stackOut.peek();
}
/**
* 队列是否为空
*/
public boolean isEmpty() {
return stackIn.isEmpty() && stackOut.isEmpty();
}
public static void main(String[] args) {
MyQueueByTwoStacks queue = new MyQueueByTwoStacks();
queue.offer(1);
queue.offer(2);
queue.offer(3);
System.out.println(queue.poll()); // 1
System.out.println(queue.poll()); // 2
queue.offer(4);
System.out.println(queue.poll()); // 3
System.out.println(queue.poll()); // 4
}
}
复杂度分析:
- 入队
offer:平均时间复杂度为 (O(1))- 出队
poll:平均时间复杂度为 (O(1))(虽然有时会一次性搬很多元素,但摊还下来仍是常数级)
面试题 2:用队列实现栈
题目大意 :
只使用队列的基本操作(入队、出队等),实现一个栈的功能(push、pop、top)。
1. 核心思路
栈是后进先出,我们用一个队列来模拟:
- 方法一:每次
push时,保证新元素最后出队 - 常用做法:
push(x):先把x入队- 然后把队列中前面的所有元素依次出队,再入队
- 这样,新来的
x会被"旋转"到队头,之后pop时,总是先出x
2. 代码实现(Java)
java
import java.util.LinkedList;
import java.util.Queue;
/**
* 用一个队列实现栈
*/
public class MyStackByQueue {
private Queue<Integer> queue;
public MyStackByQueue() {
queue = new LinkedList<>();
}
/**
* 压栈操作
*/
public void push(int x) {
// 1. 先把新元素入队
queue.offer(x);
// 2. 再把之前的元素"旋转"到新元素后面
int size = queue.size();
// 除了刚入队的 x 之外,其它 size-1 个元素依次出队再入队
for (int i = 0; i < size - 1; i++) {
int value = queue.poll();
queue.offer(value);
}
}
/**
* 弹栈操作
*/
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈为空,无法弹栈!");
}
// 因为我们在 push 的时候保证了"栈顶"总在队头
return queue.poll();
}
/**
* 查看栈顶元素
*/
public int top() {
if (isEmpty()) {
throw new RuntimeException("栈为空!");
}
return queue.peek();
}
/**
* 栈是否为空
*/
public boolean isEmpty() {
return queue.isEmpty();
}
public static void main(String[] args) {
MyStackByQueue stack = new MyStackByQueue();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.top()); // 3
System.out.println(stack.pop()); // 3
System.out.println(stack.pop()); // 2
System.out.println(stack.pop()); // 1
}
}
复杂度分析:
push:时间复杂度 (O(n))(每次要旋转前面的元素)pop、top:时间复杂度 (O(1))
面试题 3:用栈判断括号是否合法(经典高频题)
题目描述 :
给定一个只包含 ()、[]、{} 的字符串,判断括号是否成对、顺序是否正确。
例如:
"()[]"合法"(]"不合法"([)]"不合法"{[]}"合法
1. 思路讲解
- 遍历字符串:
- 遇到左括号:入栈
- 遇到右括号:
- 如果栈为空:不合法
- 否则,弹出栈顶,看是否是对应的左括号
- 最后,栈必须为空,才算完全匹配
2. 代码实现(Java)
java
import java.util.Stack;
public class ValidParentheses {
public static boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
// 如果是左括号,就入栈
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
// 如果遇到右括号
if (stack.isEmpty()) {
return false; // 没有左括号与之匹配
}
char top = stack.pop();
// 判断是否匹配
if (c == ')' && top != '(') return false;
if (c == ']' && top != '[') return false;
if (c == '}' && top != '{') return false;
}
}
// 最后栈必须为空
return stack.isEmpty();
}
public static void main(String[] args) {
System.out.println(isValid("()[]{}")); // true
System.out.println(isValid("([)]")); // false
System.out.println(isValid("{[]}")); // true
}
}
这道题几乎是"栈"的最经典应用题,力扣(LeetCode)上非常高频,建议熟练掌握。
十一、栈 vs 队列:一张表帮你对比记忆
| 对比项 | 栈(Stack) | 队列(Queue) |
|---|---|---|
| 核心规则 | 后进先出 LIFO | 先进先出 FIFO |
| 形象类比 | 一摞盘子,只能从上面拿 | 排队买票,先到先走 |
| 主要操作 | push / pop / peek | offer / poll / peek |
| Java 常用实现 | Stack、Deque |
Queue + LinkedList / ArrayDeque |
| 典型应用 | 函数调用栈、撤销、括号匹配 | 任务调度、BFS、消息队列 |
十二、给初学者的一些小建议
- 多画图、多类比
- 画出数组 + 指针(top / front / rear)的变化,印象更深
- 手动模拟一遍操作
- 拿纸和笔模拟入栈、出栈,入队、出队
- 亲手敲一遍文中的代码
- 不要只复制粘贴,自己打字可以加深记忆
- 尝试自己扩展
- 把
int改成 泛型T,让栈 / 队列可以存任意类型 - 给栈 / 队列加一个
clear()方法
- 把