LeetCode Hot100 最小栈秒杀指南:双栈 + 单调栈双解法,面试再也不怕被追问!

99 阅读6分钟

🔥 LeetCode Hot100 最小栈秒杀指南:双栈 + 单调栈双解法,面试再也不怕被追问!

作为面试高频 “守门员” 题,LeetCode 155. 最小栈堪称 “算法基础必过关卡”—— 看似简单的 push/pop/top/getMin 操作,却藏着时间复杂度与空间复杂度的权衡艺术。很多开发者刷题时只会死记 “双栈模板”,一旦面试官追问 “能不能用一个栈实现?”“为什么辅助栈要存重复最小值?” 就当场卡壳。

今天这篇文章,从问题本质拆解→双栈标准解法→单栈极致优化→面试追问应对,用最通俗的语言 + 可视化步骤 + 可运行代码,带你吃透最小栈的核心逻辑,不仅能秒杀 Hot100 原题,还能轻松应对 90% 的面试变种问题!

🚀 问题直击:最小栈的核心矛盾是什么?

先明确问题要求(LeetCode 155 原题):

设计一个支持 push ,pop ,top 操作,并能在常数时间 O (1) 内检索到最小元素的栈。

常规栈的 push/pop/top 都是 O (1),但getMin 要 O (1) 是关键难点—— 如果每次 getMin 都遍历栈找最小值,时间复杂度会变成 O (n),不符合要求。

核心矛盾:栈的 “后进先出” 特性最小值的 “随机访问” 需求的冲突。解决方案的核心思路:用额外空间 “缓存” 最小值,或通过 “编码技巧” 在单个栈中同时存储数据和最小值信息。

🌟 解法一:双栈法(面试首选,简单易懂)

1. 原理:用 “辅助栈” 专门缓存最小值

  • 主栈(dataStack) :正常存储所有元素,负责 push/pop/top 操作;
  • 辅助栈(minStack) :专门存储 “当前栈中的最小值”,确保栈顶永远是最小值。

2. 关键规则(避免踩坑!)

  • push 时:如果新元素≤辅助栈栈顶(注意是 “≤” 不是 “<”,避免重复最小值被覆盖),则新元素同时入辅助栈;
  • pop 时:如果主栈弹出的元素等于辅助栈栈顶(说明弹出的是当前最小值),则辅助栈也同步弹出;
  • getMin 时:直接返回辅助栈栈顶元素(O (1))。

3. 可视化步骤(举例:push 3→2→5→2)

操作主栈(dataStack)辅助栈(minStack)最小值
push 3[3][3]3
push 2[3,2][3,2]2
push 5[3,2,5][3,2]2
push 2[3,2,5,2][3,2,2]2
pop[3,2,5][3,2]2
pop[3,2][3,2]2
pop[3][3]3

4. 完整代码(JavaScript 实现)

javascript

运行

class MinStack {
  constructor() {
    this.dataStack = []; // 主栈:存所有元素
    this.minStack = []; // 辅助栈:存当前最小值
  }

  // 入栈操作
  push(val) {
    this.dataStack.push(val);
    // 辅助栈为空 或 新元素≤当前最小值 → 同步入栈
    if (this.minStack.length === 0 || val <= this.minStack[this.minStack.length - 1]) {
      this.minStack.push(val);
    }
  }

  // 出栈操作
  pop() {
    const top = this.dataStack.pop();
    // 弹出的是当前最小值 → 辅助栈同步弹出
    if (top === this.minStack[this.minStack.length - 1]) {
      this.minStack.pop();
    }
    return top;
  }

  // 获取栈顶元素
  top() {
    return this.dataStack[this.dataStack.length - 1];
  }

  // 获取最小值(O(1))
  getMin() {
    return this.minStack[this.minStack.length - 1];
  }
}

// 测试代码
const minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
console.log(minStack.getMin()); // -3
minStack.pop();
console.log(minStack.top()); // 0
console.log(minStack.getMin()); // -2

5. 复杂度分析

  • 时间复杂度:所有操作(push/pop/top/getMin)均为 O (1);
  • 空间复杂度:O (n),最坏情况下(元素单调递减)辅助栈需存储所有元素。

⚡ 解法二:单栈法(极致优化,面试加分项)

如果面试官问 “能不能不用辅助栈?”,单栈法就是你的 “加分答案”—— 核心思路是在栈中存储 “元素与当前最小值的差值” ,通过差值反推最小值,无需额外空间。

1. 原理:用 “差值编码” 替代辅助栈

  • 栈中存储的不是原始元素,而是「当前元素 - 上一个最小值」的差值;

  • 用一个变量minVal实时记录当前栈中的最小值;

  • 通过差值的正负性判断:

    • 若差值≥0:说明当前元素≥上一个最小值,原始元素 = minVal + 差值;
    • 若差值 < 0:说明当前元素是新的最小值,原始元素 = minVal(因为差值 = 当前元素 - 上一个最小值 → 当前元素 = minVal),且上一个最小值 = 当前元素 - 差值(反推旧最小值)。

2. 可视化步骤(举例:push 3→2→5→2)

操作栈中存储(差值)minVal(当前最小值)原始元素推导
push 3[0]33(差值 0=3-3)
push 2[0, -1]22(差值 - 1=2-3 → 新 minVal=2)
push 5[0, -1, 3]25(差值 3=5-2 ≥0 → 5=2+3)
push 2[0, -1, 3, 0]22(差值 0=2-2 ≥0 → 2=2+0)
pop[0, -1, 3]2弹出差值 0 → 原始元素 = 2+0=2
pop[0, -1]3弹出差值 3 → 原始元素 = 2+3=5
pop[0]3弹出差值 - 1 → 旧 minVal=2 - (-1)=3,原始元素 = 2

3. 完整代码(JavaScript 实现)

javascript

运行

class MinStack {
  constructor() {
    this.stack = []; // 栈中存储「元素与当前最小值的差值」
    this.minVal = null; // 实时记录当前最小值
  }

  push(val) {
    if (this.stack.length === 0) {
      // 栈为空时,差值为0,minVal=当前元素
      this.stack.push(0);
      this.minVal = val;
    } else {
      const diff = val - this.minVal; // 计算差值
      this.stack.push(diff);
      // 差值<0 → 当前元素是新的最小值
      if (diff < 0) {
        this.minVal = val;
      }
    }
  }

  pop() {
    const diff = this.stack.pop();
    let topVal;
    if (diff < 0) {
      // 弹出的是最小值,反推上一个最小值
      topVal = this.minVal;
      this.minVal = this.minVal - diff; // 旧minVal = 当前minVal - 差值
    } else {
      // 弹出的不是最小值,原始值=当前minVal + 差值
      topVal = this.minVal + diff;
    }
    return topVal;
  }

  top() {
    const diff = this.stack[this.stack.length - 1];
    return diff < 0 ? this.minVal : this.minVal + diff;
  }

  getMin() {
    return this.minVal;
  }
}

// 测试代码
const minStack2 = new MinStack();
minStack2.push(-2);
minStack2.push(0);
minStack2.push(-3);
console.log(minStack2.getMin()); // -3
minStack2.pop();
console.log(minStack2.top()); // 0
console.log(minStack2.getMin()); // -2

4. 复杂度分析

  • 时间复杂度:所有操作均为 O (1);
  • 空间复杂度:O (n),但无额外辅助栈,实际运行时内存占用比双栈法低(尤其适合大数据量场景)。

🎯 面试追问:这些坑你必须知道!

  1. 双栈法中,辅助栈为什么要存 “≤” 栈顶的元素? 答:避免重复最小值被覆盖。比如 push 2→2,若辅助栈只存 “<”,则第二个 2 不会入栈,pop 第一个 2 后,辅助栈栈顶还是 2(正确);但如果是 push 2→3→2,pop 3 后,第二个 2 需要成为最小值,若辅助栈没存第二个 2,就会错误返回 3。

  2. 单栈法中,差值可能出现溢出吗? 答:在 JavaScript 中不会,因为 JS 的 Number 是 64 位浮点数,支持很大范围;但在 Java/C++ 等语言中,若元素是 int 类型,可能出现 “当前元素 - 上一个最小值” 溢出(比如当前元素是 Integer.MIN_VALUE,上一个最小值是正数),此时需用 long 类型存储差值。

  3. 两种方法的适用场景? 答:

    • 双栈法:代码简单易懂,维护成本低,适合日常开发和面试基础题;
    • 单栈法:空间利用率更高,适合对内存敏感的场景(如嵌入式开发),但逻辑稍复杂,面试时能讲清原理可加分。

📝 实战总结:秒杀最小栈的 3 个关键

  1. 理解核心矛盾:栈的 LIFO 特性与 getMin 的 O (1) 需求冲突,解决方案是 “缓存最小值”;
  2. 双栈法是基础:必须熟练掌握,面试时优先写双栈法(不易出错);
  3. 单栈法是进阶:记住 “差值编码” 思路,能应对面试官的优化追问。

最后,建议大家动手实现两种解法,再做几道变种题(如 “实现最大栈”“栈中元素频次统计”),彻底巩固栈的操作和复杂度优化思维~

你在面试中遇到过最小栈的哪些变种问题?欢迎在评论区分享你的经历!👇