一文通解:数据结构之 栈

上周同事小王找我 debug,说他的递归方法一直报 StackOverflowError,我一看代码------递归终止条件写反了。我说:"老弟,你这是栈溢出了。"他一脸茫然:"栈?什么栈?Java 里不就有个 Stack 类吗?"

很多 Java 程序员天天写代码,Stack 类倒也用过,JVM 栈溢出也见过,但真要系统地说清楚"栈到底是什么、它凭什么这么重要、它在 Java 世界里有多少种存在形式",可能就支支吾吾了。

这篇文章就是重点把"栈"这件事彻底搞明白的。我们不讲枯燥的理论,不写晦涩的数学公式,就从你每天都在用的功能出发:为什么按 Ctrl+Z 能撤销?为什么浏览器能后退?为什么递归太深会 StackOverflow?------读完你会发现,栈无处不在,而你已经用了它很多年,只是不认识它而已。


一、先从一个生活场景说起------叠盘子与"后入先出"

在正式写代码之前,我想请你想象一个场景:

你去自助餐厅吃饭,后厨洗完的盘子是这样叠放的:

复制代码
         ↑ 新的盘子放最上面(入栈)
    ┌───┐
    │ 5 │  ← 最后一个放上去的
    │ 4 │
    │ 3 │
    │ 2 │
    │ 1 │  ← 第一个放上去的
    └───┘
         ↓ 取盘子也从最上面取(出栈)

规则只有一条:你只能从最上面拿盘子。你不能从中间抽一个,也不能从最底下掏一个。

这就意味着:最后一个放上去的盘子,一定是第一个被拿走的 。英文里叫 LIFO(Last In, First Out),中文叫"后进先出"。

栈的 LIFO 特性不是人为规定的,而是"只能在一端操作"这个限制自然导致的结果。就像你没法从盘子堆的中间抽出一个盘子而不把上面的全弄倒------这不是规矩,是物理规律。


叠盘子的规则搞懂了,栈你也就懂了 70%。下面我们把它翻译成计算机语言。


二、栈的基本操作:就"三招两式"

栈是数据结构里最简单的之一------操作只有三种,一手数得过来:

2.1 核心三操作

操作 英文名 大白话解释 图解
入栈 push 把元素放到栈顶 往盘子堆顶上再摞一个
出栈 pop 把栈顶元素取出来(并删除) 从盘子堆顶上拿走一个
查看栈顶 peek/top 看一眼栈顶元素是什么(不删除) 瞅一眼最上面的盘子,但不拿走
java 复制代码
// 伪代码示意
Stack stack = new Stack();

stack.push(1);  // 栈:[1]
stack.push(2);  // 栈:[1, 2]
stack.push(3);  // 栈:[1, 2, 3]    ← 3 在栈顶

stack.peek();   // 返回 3           ← 只是看看,栈不变
stack.pop();    // 返回 3           ← 拿走栈顶,栈:[1, 2]
stack.pop();    // 返回 2           ← 栈:[1]

2.2 两个辅助操作

操作 英文名 大白话解释
判空 isEmpty 栈里还有东西吗?
查大小 size 栈里现在有几个元素?

就这么多。没了。栈就是这么朴素的家伙------不像树有一堆术语,不像图有各种遍历算法。正因为简单,所以它在软件工程中无处不在。


好了,概念讲完了。接下来我们动真格的------用 Java 亲手实现一个栈。自己写过一遍,比什么记忆法都好使。


三、用 Java 徒手撸一个栈:写过才真正理解

栈有两种主流实现方式:数组链表。两种各有优劣,咱们各写一个。

3.1 数组实现(顺序栈)

java 复制代码
public class ArrayStack<T> {
    private Object[] elements;   // 存储元素的数组
    private int top;             // 栈顶指针(指向栈顶元素的位置)
    private static final int DEFAULT_CAPACITY = 10;

    public ArrayStack() {
        this.elements = new Object[DEFAULT_CAPACITY];
        this.top = -1;           // -1 表示空栈
    }

    // 入栈
    public void push(T item) {
        if (top == elements.length - 1) {
            resize();            // 满了就扩容
        }
        elements[++top] = item;  // 先移动指针,再放入元素
    }

    // 出栈
    @SuppressWarnings("unchecked")
    public T pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        T item = (T) elements[top];
        elements[top] = null;    // 防止内存泄漏
        top--;
        return item;
    }

    // 查看栈顶
    @SuppressWarnings("unchecked")
    public T peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return (T) elements[top];
    }

    public boolean isEmpty() {
        return top == -1;
    }

    public int size() {
        return top + 1;
    }

    // 扩容为原来的 2 倍
    private void resize() {
        elements = Arrays.copyOf(elements, elements.length * 2);
    }
}

图解数组栈的工作过程:

复制代码
初始状态:           push(10) 后:         push(20) 后:         pop() 后:
┌───┬───┬───┬───┐   ┌───┬───┬───┬───┐    ┌───┬───┬───┬───┐    ┌───┬───┬───┬───┐
│   │   │   │   │   │10 │   │   │   │    │10 │20 │   │   │    │10 │20 │   │   │
└───┴───┴───┴───┘   └───┴───┴───┴───┘    └───┴───┴───┴───┘    └───┴───┴───┴───┘
  top = -1                top = 0              top = 1              top = 0
                                                                    (20 被取出)

3.2 链表实现(链式栈)

java 复制代码
public class LinkedStack<T> {
    // 节点内部类
    private static class Node<T> {
        T data;
        Node<T> next;

        Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
    }

    private Node<T> top;     // 栈顶节点
    private int size;

    // 入栈:在链表头部插入
    public void push(T item) {
        top = new Node<>(item, top);   // 新节点指向旧栈顶
        size++;
    }

    // 出栈:从链表头部删除
    public T pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        T item = top.data;
        top = top.next;     // 栈顶指针后移
        size--;
        return item;
    }

    // 查看栈顶
    public T peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return top.data;
    }

    public boolean isEmpty() {
        return top == null;
    }

    public int size() {
        return size;
    }
}

链表实现中,pushpop 都在链表头部 操作,时间复杂度都是 O(1)。注意:是头部而不是尾部------如果你每次 push 都要遍历到链表尾部,那时间复杂度就变成 O(n) 了,这是初学者常犯的错误。

3.3 两种实现的对比

维度 数组实现 链表实现
访问速度 快(连续内存,缓存友好) 稍慢(节点分散,缓存不友好)
动态扩容 需要,扩容时有 O(n) 开销 天然动态,随用随加
内存占用 可能浪费(预留空间) 每个节点多一个指针开销
线程安全 都不安全 都不安全
适用场景 数据量可预估 数据量不确定

Java 标准库里的 Stack 类用的是数组(继承了 Vector),而 ArrayDeque(推荐用来替代 Stack)也是用循环数组实现的。下面会详细讲。


自己手写了两个栈,你应该对栈的底层原理了然于胸了。但写项目时没人会手写栈------接下来我们看看 Java 标准库里给我们准备了什么。


四、Java 集合框架中的栈:Stack 与 Deque 的"新旧交替"

4.1 Stack 类------一个不应该再用的"老古董"

java.util.Stack 是 Java 1.0 就有的类,直接继承自 Vector

java 复制代码
public class Stack<E> extends Vector<E> {
    public E push(E item) { ... }
    public synchronized E pop() { ... }
    public synchronized E peek() { ... }
    public boolean empty() { ... }
    public synchronized int search(Object o) { ... }
}

问题很大

  1. 继承 Vector :Stack 是 LIFO 的,但继承了 Vector 以后,你可以调用 get(index) 从中间取元素、add(index, e) 往中间插入------完全破坏了栈的语义
  2. 所有方法都是 synchronized:不管你需不需要线程安全,它都给你加锁,性能差。
  3. 官方都不推荐了 :Java 官方文档明确说"应该使用 Deque 接口来替代"。
java 复制代码
// 反例:虽然编译通过,但完全违背了栈的设计初衷
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.add(1, 99);     // 往栈中间插入!这不是栈该有的操作!
stack.get(0);          // 从栈底取元素!这也行?!

4.2 Deque------官方推荐的"栈替身"

Deque(Double Ended Queue,双端队列)是 Java 6 引入的接口,你可以把它当成栈用,也可以当成队列用:

java 复制代码
// 把 Deque 当栈用
Deque<Integer> stack = new ArrayDeque<>();

stack.push(1);     // 入栈
stack.push(2);     // 入栈
stack.push(3);     // 入栈
stack.pop();       // 出栈,返回 3
stack.peek();      // 查看栈顶,返回 2
操作 Stack 类的方法 Deque 当作栈用的方法 Deque 当作队列用的方法
入栈 / 入队 push(e) push(e)addFirst(e) offer(e)addLast(e)
出栈 / 出队 pop() pop()removeFirst() poll()removeFirst()
查看 / 查看 peek() peek()peekFirst() peek()peekFirst()

4.3 ArrayDeque 和 LinkedList,选哪个?

ArrayDequeLinkedList 都实现了 Deque 接口,都可以当栈用:

java 复制代码
Deque<String> stack1 = new ArrayDeque<>();   // 推荐
Deque<String> stack2 = new LinkedList<>();   // 也可以,但不够好
维度 ArrayDeque LinkedList
底层结构 循环数组 双向链表
内存占用 更低(无节点开销) 更高(每个 Node 多两个指针)
扩容开销 需要扩容(翻倍)
缓存友好度 高(连续内存) 低(内存分散)
支持 null? ❌ 不支持 ✅ 支持

推荐做法 :当栈用时,永远用 ArrayDeque。除非你需要频繁在栈中间操作元素(但这本身就不是栈的用法),否则 LinkedList 没有优势。

java 复制代码
// 标准姿势
Deque<Integer> stack = new ArrayDeque<>();

// 面试时如果你还在写 Stack<Integer> stack = new Stack<>()
// 面试官可能会在心里默默扣分

好了,栈的基础知识和 Java 实现都讲清楚了。下面才是最精彩的部分------栈到底能干什么?你平时用的哪些功能,背后都是栈在默默工作?


五、栈的经典应用场景------原来这些功能都是它干的

5.1 浏览器前进后退------双栈协作

你用浏览器时一定点过"后退"和"前进"按钮。这个功能就是两个栈 实现的:

java 复制代码
public class BrowserHistory {
    private Deque<String> backStack = new ArrayDeque<>();
    private Deque<String> forwardStack = new ArrayDeque<>();
    private String currentUrl;

    // 访问新页面
    public void visit(String url) {
        if (currentUrl != null) {
            backStack.push(currentUrl);  // 当前页进后退栈
        }
        currentUrl = url;
        forwardStack.clear();            // 前进栈清空(新页面后不能前进了)
    }

    // 后退
    public String back() {
        if (backStack.isEmpty()) return null;
        forwardStack.push(currentUrl);   // 当前页进前进栈
        currentUrl = backStack.pop();    // 从后退栈取上一个页面
        return currentUrl;
    }

    // 前进
    public String forward() {
        if (forwardStack.isEmpty()) return null;
        backStack.push(currentUrl);      // 当前页进后退栈
        currentUrl = forwardStack.pop(); // 从前进栈取下一个页面
        return currentUrl;
    }
}

5.2 撤销操作(Undo/Redo)------写代码最离不开的功能

你写代码时狂按 Ctrl+Z 的时候,背后就是一个栈在帮你记住每一步操作:

java 复制代码
public class TextEditor {
    private Deque<String> undoStack = new ArrayDeque<>();
    private Deque<String> redoStack = new ArrayDeque<>();
    private String text = "";

    // 执行操作前保存快照
    public void type(String newText) {
        undoStack.push(text);       // 旧状态入栈
        text = newText;
        redoStack.clear();          // 新操作后 redo 栈清空
    }

    // Ctrl+Z
    public String undo() {
        if (undoStack.isEmpty()) return text;
        redoStack.push(text);       // 当前状态进 redo 栈
        text = undoStack.pop();     // 恢复上一个状态
        return text;
    }

    // Ctrl+Y
    public String redo() {
        if (redoStack.isEmpty()) return text;
        undoStack.push(text);       // 当前状态进 undo 栈
        text = redoStack.pop();     // 恢复下一个状态
        return text;
    }
}

5.3 括号匹配------IDEA 的自动括号高亮就是它

你写代码时,IDEA 会自动把匹配的括号高亮出来。这个功能的底层算法就是用栈做的:

java 复制代码
public class BracketMatcher {
    public static boolean isValid(String s) {
        Deque<Character> stack = new ArrayDeque<>();
        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 != '(') ||
                    (c == ']' && top != '[') ||
                    (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 20,面试出镜率极高。建议你自己手写三遍,直到不看模板也能写出来。

5.4 表达式求值------计算器背后的"中缀转后缀"

你在电脑上敲 3 + 4 × 2,计算器得出 11 而不是 14------这是因为它先算乘除再算加减。这个"优先级处理"就是用栈来实现的。

这个过程分两步:中缀表达式 → 后缀表达式 → 计算

java 复制代码
public class ExpressionEvaluator {
    // 计算后缀表达式(逆波兰表达式)
    public static int evalRPN(String[] tokens) {
        Deque<Integer> stack = new ArrayDeque<>();
        for (String token : tokens) {
            switch (token) {
                case "+" -> stack.push(stack.pop() + stack.pop());
                case "-" -> {
                    int b = stack.pop();
                    int a = stack.pop();
                    stack.push(a - b);       // 注意顺序
                }
                case "*" -> stack.push(stack.pop() * stack.pop());
                case "/" -> {
                    int b = stack.pop();
                    int a = stack.pop();
                    stack.push(a / b);       // 注意顺序
                }
                default -> stack.push(Integer.parseInt(token));
            }
        }
        return stack.pop();
    }

    public static void main(String[] args) {
        // "3 + 4 × 2" 的后缀表达式:["3", "4", "2", "*", "+"]
        System.out.println(evalRPN(new String[]{"3", "4", "2", "*", "+"}));
        // 输出:11
    }
}

计算过程图:

复制代码
3  4  2  *  +
│  │  │  │  │
↓  ↓  ↓  ↓  ↓
3  4  2       → 读到 *:弹出 2 和 4,计算 4×2=8,推回 8
3  8          → 读到 +:弹出 8 和 3,计算 3+8=11,推回 11
11            → 最终结果

5.5 深度优先搜索(DFS)------树的三种遍历全靠栈

二叉树的先序、中序、后序遍历,递归写法确实简单,但面试官通常还会追问一句:"你能用迭代的方式写吗?"

迭代版的二叉树遍历,核心就是手动用一个栈模拟递归

java 复制代码
public class TreeTraversal {
    // 迭代版中序遍历(左-根-右)
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode current = root;

        while (current != null || !stack.isEmpty()) {
            // 一路向左,沿途全部入栈
            while (current != null) {
                stack.push(current);
                current = current.left;
            }
            // 左到底了,弹出栈顶(根),然后转向右子树
            current = stack.pop();
            result.add(current.val);
            current = current.right;
        }
        return result;
    }
}

这些应用场景看下来,你应该能感受到------栈虽然结构简单,但在实际开发中的应用场景非常多。面试中这些都是高频考点,尤其是括号匹配和表达式求值,几乎是栈相关题目的"标准题库"。

好,接下来我们往深里走一层------栈不仅在应用层面有用,在 JVM 底层,它更是 Java 程序运行的生命线


六、深入 JVM:每个 Java 线程都背着一条"栈龙"

6.1 JVM 中的栈是什么?

JVM 的内存模型中,每个线程都有自己独享 的一块内存区域,叫作 Java 虚拟机栈(Java Virtual Machine Stack)

6.2 栈帧:方法的"运行档案"

每当你调用一个方法,JVM 就在虚拟机栈里创建一个栈帧(Stack Frame),把它压入栈顶。方法执行完,这个栈帧就从栈顶弹出。

复制代码
main() 调用               methodA() 调用            methodB() 调用
    ┌──────┐               ┌──────┐               ┌──────┐
    │main  │               │  A   │               │  B   │
    │栈帧  │               │栈帧   │               │栈帧  │
    └──────┘               │main  │               │  A   │
                           │栈帧  │               │栈帧   │
                           └──────┘               │main  │
                                                  │栈帧  │
                                                  └──────┘

methodB() 返回            methodA() 返回           main() 返回
    ┌──────┐               ┌──────┐               ┌──────┐
    │  A   │               │main  │               │      │
    │栈帧  │               │栈帧   │               │  空  │
    │main  │               └──────┘               └──────┘
    │栈帧  │
    └──────┘

每个栈帧包含四部分:

组成部分 存了什么 大白话
局部变量表(Local Variables) 方法参数 + 方法内的局部变量 方法的"私人物品清单"
操作数栈(Operand Stack) 计算过程中的中间数据 方法的"草稿纸"
动态链接(Dynamic Linking) 指向运行时常量池中该方法的引用 方法的"身份证"
返回地址(Return Address) 方法执行完后回到哪里 方法的"回家地址"

来一段代码感受一下:

java 复制代码
public class StackFrameDemo {
    public static void main(String[] args) {
        int a = 10;              // main 的局部变量
        int result = add(a, 20); // 调用 add,创建 add 的栈帧
        System.out.println(result);
    }                           // main 的栈帧销毁

    public static int add(int x, int y) {
        int sum = x + y;         // add 的局部变量
        return sum;
    }                           // add 的栈帧销毁,返回 main
}

执行过程

复制代码
main 开始执行:          add 被调用:            add 返回后:
┌──────────────┐       ┌──────────────┐         ┌──────────────┐
│ main 栈帧     │       │ add 栈帧     │ ← 栈顶   │ main 栈帧     │
│ a = 10       │       │ x = 10       │         │ a = 10       │
│ result = ?   │       │ y = 20       │         │ result = 30  │
└──────────────┘       │ sum = 30     │         └──────────────┘
                       ├──────────────┤
                       │ main 栈帧     │
                       └──────────────┘

6.3 StackOverflowError------每个 Java 人都见过的红色

现在你应该能理解这个经典错误了:

java 复制代码
public void recursive() {
    recursive();  // 没有终止条件,无限递归
}

// java.lang.StackOverflowError

原因 :每次调用 recursive() 都会创建一个新的栈帧压入虚拟机栈。栈的容量是有限的(默认约 1MB),当栈帧数量超过栈的容量,就抛出 StackOverflowError

复制代码
// 可以通过 JVM 参数调整栈大小
// -Xss256k(设置每个线程的栈大小为 256KB)
// -Xss2m (设置每个线程的栈大小为 2MB)

面试常见追问StackOverflowErrorOutOfMemoryError 有什么区别?

  • StackOverflowError:栈空间不够用,通常是递归太深导致的。
  • OutOfMemoryError: Java heap space:堆空间不够用,通常是创建了太多对象。
  • 关键区别:一个发生在栈(线程私有),一个发生在堆(线程共享)。

JVM 的栈原理讲完了。你可能已经发现------JVM 的栈和数据结构里的栈,本质上就是同一个东西在不同层面的应用。下面我们聊一个与之密切相关的话题:递归


七、栈与递归:你不写栈,但栈无处不在

7.1 递归本质上就是栈

写过递归的同学都知道,递归代码通常很简洁,但背后 JVM 帮你做了一堆事情------每次递归调用都创建一个新栈帧。

以经典问题------反转单链表为例:

java 复制代码
// 递归版:优雅但暗藏玄机
public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) {
        return head;                            // 递归终止条件
    }
    ListNode newHead = reverseList(head.next);   // 递归深入
    head.next.next = head;                       // 反转指针
    head.next = null;
    return newHead;                              // 层层返回
}

递归调用栈的过程(反转链表 1→2→3→null):

java 复制代码
reverseList(1):  newHead = reverseList(2)    等待中...
    reverseList(2):  newHead = reverseList(3)    等待中...
        reverseList(3):  返回 3                    ← 触底反弹
    reverseList(2):  3.next = 2, 2.next = null    返回 3
reverseList(1):  2.next = 1, 1.next = null    返回 3

7.2 递归的隐患与解法

递归虽然优雅,但有两大问题:

  1. 栈溢出:深度大了就 StackOverflow
  2. 重复计算 :斐波那契递归是最经典的例子,fib(5) 会重复计算 fib(3) 两次、fib(2) 三次......
java 复制代码
// 递归版斐波那契:时间复杂度 O(2^n),极其低效
public int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}

// 迭代版(用栈思想但不递归):时间复杂度 O(n)
public int fib(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1;
    for (int i = 2; i <= n; i++) {
        int sum = a + b;
        a = b;
        b = sum;
    }
    return b;
}

经验法则:能用迭代解决的问题尽量用迭代。递归只在"分治"场景(如树的遍历、归并排序)才真正有优势。别为了代码短而滥用递归。


理论知识学得差不多了。下面我们转入"面试实战模式"------把前面学的东西变成面试官眼中的高分答案。


八、面试高频考题实战------把知识变成分数

8.1 最小栈(Min Stack)------LeetCode 155

题目 :设计一个栈,支持 pushpoptop 操作,并能在 O(1) 时间内获取栈中最小值。

这个题的核心技巧是用两个栈:一个正常存数据,一个存"当前最小值"。

java 复制代码
class MinStack {
    private Deque<Integer> dataStack;   // 存数据
    private Deque<Integer> minStack;    // 存每个阶段的最小值

    public MinStack() {
        dataStack = new ArrayDeque<>();
        minStack = new ArrayDeque<>();
    }

    public void push(int val) {
        dataStack.push(val);
        // 如果 minStack 为空,或 val 比当前最小值更小,则入 minStack
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        } else {
            minStack.push(minStack.peek());  // 重复压入当前最小值
        }
    }

    public void pop() {
        dataStack.pop();
        minStack.pop();  // 同步弹出
    }

    public int top() {
        return dataStack.peek();
    }

    public int getMin() {
        return minStack.peek();
    }
}

图解

复制代码
push(5)    push(2)    push(3)    push(1)    pop()
dataStack  dataStack  dataStack  dataStack  dataStack
┌───┐      ┌───┐      ┌───┐      ┌───┐      ┌───┐
│ 5 │      │ 2 │      │ 3 │      │ 1 │      │ 3 │
└───┘      │ 5 │      │ 2 │      │ 2 │      │ 2 │
           └───┘      │ 5 │      │ 5 │      │ 5 │
                      └───┘      └───┘      └───┘

minStack   minStack   minStack   minStack   minStack
┌───┐      ┌───┐      ┌───┐      ┌───┐      ┌───┐
│ 5 │      │ 2 │      │ 2 │      │ 1 │      │ 2 │
└───┘      │ 5 │      │ 2 │      │ 2 │      │ 2 │
           └───┘      │ 5 │      │ 5 │      │ 5 │
                      └───┘      └───┘      └───┘
getMin()=5 getMin()=2 getMin()=2 getMin()=1 getMin()=2

8.2 用栈实现队列------LeetCode 232

题目 :用两个栈实现一个队列,支持 pushpoppeekempty 操作。

栈是 LIFO,队列是 FIFO。两个栈配合,可以翻转两次,负负得正

java 复制代码
class MyQueue {
    private Deque<Integer> inStack;   // 输入栈(push 用)
    private Deque<Integer> outStack;  // 输出栈(pop/peek 用)

    public MyQueue() {
        inStack = new ArrayDeque<>();
        outStack = new ArrayDeque<>();
    }

    public void push(int x) {
        inStack.push(x);   // 直接往入栈放
    }

    public int pop() {
        if (outStack.isEmpty()) {
            transfer();    // outStack 空了就从 inStack 倒过来
        }
        return outStack.pop();
    }

    public int peek() {
        if (outStack.isEmpty()) {
            transfer();
        }
        return outStack.peek();
    }

    public boolean empty() {
        return inStack.isEmpty() && outStack.isEmpty();
    }

    // 关键操作:把 inStack 的元素全部倒入 outStack
    private void transfer() {
        while (!inStack.isEmpty()) {
            outStack.push(inStack.pop());
        }
    }
}

核心图解

复制代码
push(1) → push(2) → push(3):          第一次 pop():
inStack  outStack                     inStack → transfer → outStack
┌───┐    ┌───┐                       ┌───┐              ┌───┐
│ 3 │    │   │                       │   │              │ 1 │  ← pop 出 1
│ 2 │    │   │                       │   │              │ 2 │
│ 1 │    │   │                       │   │              │ 3 │
└───┘    └───┘                       └───┘              └───┘
(栈顶是3,先进后出)                     (栈顶是1,先进先出!)

复杂度 :虽然单次 pop 可能是 O(n),但均摊时间复杂度是 O(1)------每个元素只会被 transfer 一次。面试时能说出"均摊 O(1)"是一个加分点。

8.3 有效的括号------LeetCode 20

这道题在 5.3 节已经讲过,这里补充几个要点:

  • 时间复杂度 O(n),只需遍历一遍字符串
  • 空间复杂度 O(n),最坏情况下全是左括号,全入栈
  • 面试官可能追问:如果括号类型扩展到 4 种、5 种怎么办? ------代码不改逻辑,只需增加判断条件。用 HashMap 存储配对关系更优雅:
java 复制代码
private static final Map<Character, Character> PAIRS = Map.of(
    ')', '(',
    ']', '[',
    '}', '{'
);

8.4 每日温度------LeetCode 739(单调栈经典题)

题目:给定一个温度数组,对于每一天,需要等多少天才能遇到更高的温度。如果之后没有更高的温度,记为 0。

这道题是单调栈的经典应用:

java 复制代码
public int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length;
    int[] result = new int[n];
    Deque<Integer> stack = new ArrayDeque<>();  // 存下标,栈底→栈顶递减

    for (int i = 0; i < n; i++) {
        // 当前温度比栈顶对应的温度高 → 找到栈顶那天之后第一个高温
        while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
            int prevDay = stack.pop();
            result[prevDay] = i - prevDay;
        }
        stack.push(i);
    }
    return result;  // 栈里剩下的都是后面没有更高温度的
}
java 复制代码
temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
result       = [ 1,  1,  4,  2,  1,  1,  0,  0]

图解:
i=0(73): push(0)    栈:[0]
i=1(74): 74>73 → pop(0), result[0]=1, push(1)    栈:[1]
i=2(75): 75>74 → pop(1), result[1]=1, push(2)    栈:[2]
i=3(71): 71<75 → push(3)    栈:[2,3]
i=4(69): 69<71 → push(4)    栈:[2,3,4]
i=5(72): 72>69 → pop(4), result[4]=1
         72>71 → pop(3), result[3]=2
         72<75 → push(5)    栈:[2,5]
...

单调栈的特点:栈内元素始终保持单调递增或单调递减。一旦遇到破坏单调性的元素,就弹出栈顶来处理。这类题目还有"接雨水"(LeetCode 42)、"柱状图中最大矩形"(LeetCode 84)等,套路类似。


面试题也刷完了,最后我们来一个"串珠子"式的总结------把今天聊的所有内容串成一条线。


九、总结:栈很小,世界很大

让我们回头看一眼这趟"栈之旅":

java 复制代码
生活中的栈           →  叠盘子、子弹夹
    ↓
数据结构中的栈       →  LIFO、push/pop/peek,两种实现(数组 / 链表)
    ↓
Java 中的栈         →  别用 Stack 类,用 Deque + ArrayDeque
    ↓
栈的应用            →  浏览器前进后退、Ctrl+Z 撤销、括号匹配、表达式求值
    ↓
JVM 中的栈          →  方法调用栈帧、局部变量表、操作数栈
    ↓
栈的"翻车"现场       →  StackOverflowError、递归过深
    ↓
面试中的栈           →  最小栈、双栈模拟队列、单调栈

栈可能是所有数据结构里最简单的一个------没有旋转、没有平衡、没有复杂的遍历算法。但正是这种极致的简单,让它成为了计算机科学中最基础、最通用的工具之一。

从你按下 Ctrl+Z 的那一刻,到 JVM 内部的每一次方法调用,栈都在默默地执行着它的 LIFO 原则------后来者居上,先到者垫底

相关推荐
罗超驿6 小时前
9.优先级队列与堆:从底层原理到实战应用的完整指南
数据结构·算法
ch.ju6 小时前
Java Programming Chapter 4——lambda expression
java·开发语言
布朗克1686 小时前
Go 入门到精通-18-文件操作
java·数据库·golang
罗超驿6 小时前
10.Java对象比较完全指南:从equals到Comparable与Comparator
java·开发语言
LiaoWL1237 小时前
【JUC合集-01】JUC并发编程复习总结
java
牧以南歌〆7 小时前
数据结构<三>单循环链表
c语言·数据结构·算法·链表
Tisfy7 小时前
LeetCode 1979.找出数组的最大公约数:模拟(附手动gcd)
java·数学·算法·leetcode·题解·最大公约数
糖果店的幽灵7 小时前
【langgraph 从入门到精通graphApi 篇】节点与边 —— 图的骨架
java·数据库·人工智能·redis·langgraph
CHANG_THE_WORLD7 小时前
7.C++标准多线程实现 TCP通信
java·c++·tcp/ip
chen_ke_hao7 小时前
K8s 高可用集群部署
java·docker·kubernetes