📚 栈(Stack):后进先出的魔法盒,程序员的撤销键!

34 阅读7分钟

"栈就像叠盘子,最后放上去的最先拿下来!" 🍽️


😊 什么是栈?叠盘子的故事

🍽️ 生活中的栈

想象你在餐厅洗盘子:

           ┌─────┐
           │盘子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  │ ← top40  │ ← top
└──────┘              └──────┘              ├──────┤
                                            │  30  │
                                            └──────┘

Push(10):             Push(20):
┌──────┐              ┌──────┐
│  10  │ ← top20  │ ← top
└──────┘              ├──────┤
                       │  10  │
                       └──────┘

出栈(Pop)过程

初始状态:              Pop() → 返回40Pop() → 返回30:
┌──────┐              ┌──────┐              ┌──────┐
│  40  │ ← top30  │ ← top20  │ ← 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 * +

求值过程:
读取3push3 │
读取5push5 │
              │ 3 │
读取2push2 │
              │ 5 │
              │ 3 │
读取*  → pop 2, pop 5, 计算5*2=10, push10 │
                                        │ 3  │
读取+  → pop 10, pop 3, 计算3+10=13, push13 │

结果: 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)⭐⭐⭐

设计一个支持 pushpoptopgetMin 操作的栈,要求 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)

见上面的"表达式求值"部分 ⬆️


📊 栈的时间复杂度

操作数组实现链表实现
pushO(1) ⚡O(1) ⚡
popO(1) ⚡O(1) ⚡
peekO(1) ⚡O(1) ⚡
isEmptyO(1) ⚡O(1) ⚡
sizeO(1) ⚡O(1) ⚡

所有操作都是 O(1),这就是栈的魅力!


🎯 栈的使用场景

✅ 适合使用栈的场景

  1. 需要"后进先出" - 函数调用、撤销操作
  2. 括号匹配 - 编译器语法检查
  3. 表达式求值 - 计算器
  4. 深度优先搜索(DFS) - 图/树的遍历
  5. 回溯算法 - N皇后、迷宫问题

❌ 不适合使用栈的场景

  1. 需要随机访问 - 用数组
  2. 先进先出 - 用队列
  3. 需要从中间删除 - 用链表

📝 总结

🎓 记忆口诀

栈像叠盘子,
后进必先出。
只能顶上操作,
底部不能碰。
函数调用用它,
括号匹配靠它,
撤销功能也是它,
O(1)时间真厉害!

核心特点

特性说明符号
原则LIFO(后进先出)🍽️
操作只能在栈顶🔝
时间所有操作O(1)
应用函数调用、撤销、DFS🎯

🚀 下一步学习

掌握了栈,接下来可以学习:

  1. 队列(Queue) - 先进先出的兄弟结构 🎫
  2. 单调栈 - 栈的高级应用 📈
  3. 深度优先搜索(DFS) - 用栈实现 🌲

恭喜你!🎉 你已经掌握了栈这个简单但强大的数据结构!

记住:栈就是叠盘子,后进先出,只能从顶部操作! 🍽️

简单、高效、无处不在,这就是栈的魅力!💪


📌 小练习:尝试用栈实现一个简单的计算器,支持加减乘除和括号!

🤔 思考题:为什么递归可以用栈来模拟?

(答案:递归本质上就是利用系统的函数调用栈,我们可以用自己的栈来模拟这个过程!)