"栈就像叠盘子,最后放上去的最先拿下来!" 🍽️
😊 什么是栈?叠盘子的故事
🍽️ 生活中的栈
想象你在餐厅洗盘子:
┌─────┐
│盘子3│ ← 最后放的,最先拿
├─────┤
│盘子2│
├─────┤
│盘子1│ ← 最先放的,最后拿
└─────┘
↑
栈底(不能动)
规则:
- 📥 入栈(Push):把新盘子放在最上面
- 📤 出栈(Pop):只能从最上面拿盘子
- 👀 查看栈顶(Peek):看一眼最上面的盘子,但不拿走
这就是栈!后进先出(LIFO - Last In First Out) ⚡
🏗️ 栈的原理
核心概念
栈(Stack) = 一端开口的容器
只能从栈顶操作:
┌──────────┐
│ │ ← 栈顶(Top)- 唯一的出入口
├──────────┤
│ 30 │
├──────────┤
│ 20 │
├──────────┤
│ 10 │
└──────────┘
↑
栈底(Bottom)
基本操作
| 操作 | 说明 | 时间复杂度 |
|---|---|---|
| push(x) | 入栈:将元素x压入栈顶 | O(1) ⚡ |
| pop() | 出栈:移除并返回栈顶元素 | O(1) ⚡ |
| peek() | 查看栈顶元素(不删除) | O(1) ⚡ |
| isEmpty() | 判断栈是否为空 | O(1) ⚡ |
| size() | 返回栈中元素个数 | O(1) ⚡ |
🎬 栈操作动画演示
入栈(Push)过程
初始状态: Push(30): Push(40):
┌──────┐ ┌──────┐ ┌──────┐
│ 空 │ │ 30 │ ← top │ 40 │ ← top
└──────┘ └──────┘ ├──────┤
│ 30 │
└──────┘
Push(10): Push(20):
┌──────┐ ┌──────┐
│ 10 │ ← top │ 20 │ ← top
└──────┘ ├──────┤
│ 10 │
└──────┘
出栈(Pop)过程
初始状态: Pop() → 返回40: Pop() → 返回30:
┌──────┐ ┌──────┐ ┌──────┐
│ 40 │ ← top │ 30 │ ← top │ 20 │ ← top
├──────┤ ├──────┤ ├──────┤
│ 30 │ │ 20 │ │ 10 │
├──────┤ ├──────┤ └──────┘
│ 20 │ │ 10 │
├──────┤ └──────┘
│ 10 │
└──────┘
💻 Java实现栈
方式1:用数组实现
public class ArrayStack {
private int[] arr; // 存储栈元素的数组
private int top; // 栈顶指针
private int capacity; // 栈的容量
// 构造函数
public ArrayStack(int size) {
arr = new int[size];
capacity = size;
top = -1; // -1表示栈为空
}
// 📥 入栈
public void push(int value) {
if (isFull()) {
System.out.println("❌ 栈满了!无法插入 " + value);
return;
}
arr[++top] = value;
System.out.println("✅ 入栈: " + value);
}
// 📤 出栈
public int pop() {
if (isEmpty()) {
System.out.println("❌ 栈为空!无法出栈");
return -1;
}
int value = arr[top--];
System.out.println("📤 出栈: " + value);
return value;
}
// 👀 查看栈顶
public int peek() {
if (isEmpty()) {
System.out.println("❌ 栈为空!");
return -1;
}
return arr[top];
}
// 判断栈是否为空
public boolean isEmpty() {
return top == -1;
}
// 判断栈是否满了
public boolean isFull() {
return top == capacity - 1;
}
// 获取栈的大小
public int size() {
return top + 1;
}
// 🖨️ 打印栈
public void printStack() {
if (isEmpty()) {
System.out.println("栈为空 🈳");
return;
}
System.out.println("━━━━━ 栈内容 ━━━━━");
for (int i = top; i >= 0; i--) {
if (i == top) {
System.out.println("│ " + arr[i] + " │ ← top");
} else {
System.out.println("│ " + arr[i] + " │");
}
}
System.out.println("━━━━━━━━━━━━━━━━");
}
}
// 测试代码
public class Main {
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(5);
stack.push(10);
stack.push(20);
stack.push(30);
stack.printStack();
System.out.println("栈顶元素: " + stack.peek());
stack.pop();
stack.pop();
stack.printStack();
System.out.println("栈的大小: " + stack.size());
}
}
方式2:用链表实现
public class LinkedStack {
// 节点定义
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
this.next = null;
}
}
private Node top; // 栈顶指针
private int size; // 栈的大小
// 构造函数
public LinkedStack() {
this.top = null;
this.size = 0;
}
// 📥 入栈
public void push(int value) {
Node newNode = new Node(value);
newNode.next = top; // 新节点指向原栈顶
top = newNode; // 更新栈顶
size++;
System.out.println("✅ 入栈: " + value);
}
// 📤 出栈
public int pop() {
if (isEmpty()) {
System.out.println("❌ 栈为空!");
return -1;
}
int value = top.data;
top = top.next; // 栈顶移到下一个节点
size--;
System.out.println("📤 出栈: " + value);
return value;
}
// 👀 查看栈顶
public int peek() {
if (isEmpty()) {
System.out.println("❌ 栈为空!");
return -1;
}
return top.data;
}
// 判断栈是否为空
public boolean isEmpty() {
return top == null;
}
// 获取栈的大小
public int size() {
return size;
}
// 🖨️ 打印栈
public void printStack() {
if (isEmpty()) {
System.out.println("栈为空 🈳");
return;
}
System.out.println("━━━━━ 栈内容 ━━━━━");
Node current = top;
while (current != null) {
if (current == top) {
System.out.println("│ " + current.data + " │ ← top");
} else {
System.out.println("│ " + current.data + " │");
}
current = current.next;
}
System.out.println("━━━━━━━━━━━━━━━━");
}
}
方式3:使用Java自带的Stack类
import java.util.Stack;
public class StackDemo {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
// 入栈
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
// 栈的大小
System.out.println("栈大小: " + stack.size()); // 1
}
}
🎯 栈的经典应用场景
1️⃣ 函数调用栈(Call Stack)
最重要的应用! 程序运行时,每次函数调用都会在栈上创建一个"栈帧":
void functionA() {
int a = 10;
functionB(); // 调用B
}
void functionB() {
int b = 20;
functionC(); // 调用C
}
void functionC() {
int c = 30;
System.out.println("C执行中");
}
调用栈的变化:
开始: 调用B后: 调用C后:
┌──────┐ ┌──────┐ ┌──────┐
│main()│ │main()│ │main()│
└──────┘ ├──────┤ ├──────┤
│func A│ │func A│
└──────┘ ├──────┤
│func B│
├──────┤
│func C│ ← 当前执行
└──────┘
C执行完: B执行完: A执行完:
┌──────┐ ┌──────┐ ┌──────┐
│main()│ │main()│ │main()│
├──────┤ ├──────┤ └──────┘
│func A│ │func A│
├──────┤ └──────┘
│func B│
└──────┘
这就是为什么递归太深会"栈溢出"(Stack Overflow)! 💥
2️⃣ 括号匹配检查
检查表达式中的括号是否匹配:
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.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(); // 栈为空说明完全匹配
}
// 测试
System.out.println(isValid("()")); // true
System.out.println(isValid("()[]{}")); // true
System.out.println(isValid("(]")); // false
System.out.println(isValid("([)]")); // false
System.out.println(isValid("{[]}")); // true
过程演示:
输入: "{[()]}"
步骤1: 读取 '{' → push │ { │
步骤2: 读取 '[' → push │ [ │
│ { │
步骤3: 读取 '(' → push │ ( │
│ [ │
│ { │
步骤4: 读取 ')' → pop │ [ │ (匹配成功)
│ { │
步骤5: 读取 ']' → pop │ { │ (匹配成功)
步骤6: 读取 '}' → pop (空) (匹配成功)
结果: true ✅
3️⃣ 表达式求值(后缀表达式)
将中缀表达式转换为后缀表达式,并求值:
中缀表达式: 3 + 5 * 2
后缀表达式: 3 5 2 * +
求值过程:
读取3 → push │ 3 │
读取5 → push │ 5 │
│ 3 │
读取2 → push │ 2 │
│ 5 │
│ 3 │
读取* → pop 2, pop 5, 计算5*2=10, push │ 10 │
│ 3 │
读取+ → pop 10, pop 3, 计算3+10=13, push │ 13 │
结果: 13 ✅
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (String token : tokens) {
if (token.equals("+")) {
stack.push(stack.pop() + stack.pop());
} else if (token.equals("-")) {
int b = stack.pop();
int a = stack.pop();
stack.push(a - b);
} else if (token.equals("*")) {
stack.push(stack.pop() * stack.pop());
} else if (token.equals("/")) {
int b = stack.pop();
int a = stack.pop();
stack.push(a / b);
} else {
stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}
// 测试
String[] tokens = {"2", "1", "+", "3", "*"}; // (2 + 1) * 3
System.out.println(evalRPN(tokens)); // 9
4️⃣ 浏览器的前进/后退
class Browser {
Stack<String> backStack = new Stack<>(); // 后退栈
Stack<String> forwardStack = new Stack<>(); // 前进栈
String currentPage;
// 访问新页面
void visit(String url) {
if (currentPage != null) {
backStack.push(currentPage);
}
currentPage = url;
forwardStack.clear(); // 清空前进栈
System.out.println("访问: " + url);
}
// 后退
void back() {
if (backStack.isEmpty()) {
System.out.println("无法后退");
return;
}
forwardStack.push(currentPage);
currentPage = backStack.pop();
System.out.println("后退到: " + currentPage);
}
// 前进
void forward() {
if (forwardStack.isEmpty()) {
System.out.println("无法前进");
return;
}
backStack.push(currentPage);
currentPage = forwardStack.pop();
System.out.println("前进到: " + currentPage);
}
}
5️⃣ 撤销(Undo)功能
文本编辑器的撤销:
class TextEditor {
Stack<String> textStack = new Stack<>();
Stack<String> undoStack = new Stack<>();
// 输入文本
void type(String text) {
textStack.push(text);
undoStack.clear();
System.out.println("输入: " + text);
}
// 撤销
void undo() {
if (textStack.isEmpty()) {
System.out.println("无法撤销");
return;
}
String text = textStack.pop();
undoStack.push(text);
System.out.println("撤销: " + text);
}
// 重做
void redo() {
if (undoStack.isEmpty()) {
System.out.println("无法重做");
return;
}
String text = undoStack.pop();
textStack.push(text);
System.out.println("重做: " + text);
}
}
🏆 栈的经典面试题
1. 最小栈(LeetCode 155)⭐⭐⭐
设计一个支持 push、pop、top 和 getMin 操作的栈,要求 getMin 在 O(1) 时间内返回最小值。
class MinStack {
Stack<Integer> dataStack; // 数据栈
Stack<Integer> minStack; // 最小值栈
public MinStack() {
dataStack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
dataStack.push(val);
// 如果最小栈为空,或者新值更小,则入最小栈
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}
public void pop() {
int val = dataStack.pop();
// 如果弹出的是最小值,也要从最小栈弹出
if (val == minStack.peek()) {
minStack.pop();
}
}
public int top() {
return dataStack.peek();
}
public int getMin() {
return minStack.peek(); // O(1) 时间
}
}
// 使用示例
MinStack stack = new MinStack();
stack.push(-2);
stack.push(0);
stack.push(-3);
System.out.println(stack.getMin()); // -3
stack.pop();
System.out.println(stack.top()); // 0
System.out.println(stack.getMin()); // -2
2. 用栈实现队列(LeetCode 232)
class MyQueue {
Stack<Integer> inStack; // 入队栈
Stack<Integer> outStack; // 出队栈
public MyQueue() {
inStack = new Stack<>();
outStack = new Stack<>();
}
// 入队:直接push到inStack
public void push(int x) {
inStack.push(x);
}
// 出队:从outStack pop,如果outStack为空,把inStack倒过来
public int pop() {
if (outStack.isEmpty()) {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
return outStack.pop();
}
public int peek() {
if (outStack.isEmpty()) {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
return outStack.peek();
}
public boolean empty() {
return inStack.isEmpty() && outStack.isEmpty();
}
}
3. 逆波兰表达式求值(LeetCode 150)
见上面的"表达式求值"部分 ⬆️
📊 栈的时间复杂度
| 操作 | 数组实现 | 链表实现 |
|---|---|---|
| push | O(1) ⚡ | O(1) ⚡ |
| pop | O(1) ⚡ | O(1) ⚡ |
| peek | O(1) ⚡ | O(1) ⚡ |
| isEmpty | O(1) ⚡ | O(1) ⚡ |
| size | O(1) ⚡ | O(1) ⚡ |
所有操作都是 O(1),这就是栈的魅力! ⚡
🎯 栈的使用场景
✅ 适合使用栈的场景
- 需要"后进先出" - 函数调用、撤销操作
- 括号匹配 - 编译器语法检查
- 表达式求值 - 计算器
- 深度优先搜索(DFS) - 图/树的遍历
- 回溯算法 - N皇后、迷宫问题
❌ 不适合使用栈的场景
- 需要随机访问 - 用数组
- 先进先出 - 用队列
- 需要从中间删除 - 用链表
📝 总结
🎓 记忆口诀
栈像叠盘子,
后进必先出。
只能顶上操作,
底部不能碰。
函数调用用它,
括号匹配靠它,
撤销功能也是它,
O(1)时间真厉害!
核心特点
| 特性 | 说明 | 符号 |
|---|---|---|
| 原则 | LIFO(后进先出) | 🍽️ |
| 操作 | 只能在栈顶 | 🔝 |
| 时间 | 所有操作O(1) | ⚡ |
| 应用 | 函数调用、撤销、DFS | 🎯 |
🚀 下一步学习
掌握了栈,接下来可以学习:
- 队列(Queue) - 先进先出的兄弟结构 🎫
- 单调栈 - 栈的高级应用 📈
- 深度优先搜索(DFS) - 用栈实现 🌲
恭喜你!🎉 你已经掌握了栈这个简单但强大的数据结构!
记住:栈就是叠盘子,后进先出,只能从顶部操作! 🍽️
简单、高效、无处不在,这就是栈的魅力!💪
📌 小练习:尝试用栈实现一个简单的计算器,支持加减乘除和括号!
🤔 思考题:为什么递归可以用栈来模拟?
(答案:递归本质上就是利用系统的函数调用栈,我们可以用自己的栈来模拟这个过程!)