一文吃透差值法解决最小栈问题(超详细拆解)

112 阅读6分钟

很多同学觉得我上篇文章介绍用差值法解决最小栈问题“绕”,核心是没搞懂「差值如何隐含真实值」「最小值如何动态更新与恢复」。这篇文章会用  “人话 + 分步演示 + 公式推导”  的方式,从底层逻辑到代码实现,帮你彻底搞懂差值法的核心原理。

一、差值法的核心思想

差值法的本质是:不直接存储元素本身,而是存储 “当前元素与当前最小值的差值” ,再用一个变量记录「当前最小值」。通过这两个信息,反向推导真实元素值,同时保证所有操作都是 O (1) 时间复杂度。

核心解决的问题:不用辅助栈,仅用一个栈 + 一个变量,实现 “常数时间获取最小值” ,达到最优空间复杂度(O (n),无额外辅助栈开销)。

关键约定:

  • 栈 stack:存储「当前元素 - 当前最小值」的差值(记为 diff
  • 变量 currentMin:实时记录栈中所有元素的「当前最小值」
  • 核心逻辑:通过 diff 和 currentMin 互相推导,既保留真实元素信息,又能快速获取最小值

二、三大核心操作的底层逻辑(分步拆解)

我们以示例操作 push(-2) → push(0) → push(-3) → pop → top → getMin 为例,一步步演示每个操作的细节,让你直观看到 diff 和 currentMin 的变化。

1. 初始化

class MinStack {
  constructor() {
    this.stack = []; // 存储差值diff
    this.currentMin = null; // 记录当前最小值
  }
}

初始状态:栈空,currentMin = null(无任何元素,暂无最小值)。

2. Push 操作:如何存储差值?

Push 的核心是:计算当前元素与「当前最小值」的差值,存入栈中;若新元素是更小值,更新 currentMin

分两种情况:

情况 1:栈为空(第一次 push 元素)

  • 逻辑:此时没有 “之前的最小值”,新元素就是最小值。

  • 操作:

    1. 设新元素为 val,则 currentMin = val(新元素成为最小值)
    2. 差值 diff = val - currentMin = val - val = 0,将 0 存入栈中
  • 为什么存 0?因为第一个元素的 “自身与自身的差值” 是 0,后续能通过 currentMin + 0 恢复真实值。

情况 2:栈非空(后续 push 元素)

  • 逻辑:用新元素 val 减去「当前最小值 currentMin」得到 diff,存入栈中;若 diff < 0,说明 val 比当前最小值还小,需要更新 currentMin 为 val

  • 关键:diff 的正负能判断 val 是否是新的最小值:

    • diff ≥ 0val ≥ currentMin(不是新最小值,currentMin 不变)
    • diff < 0val < currentMin(是新最小值,更新 currentMin = val

示例 Push 分步演示:

操作栈状态(diff)currentMin计算逻辑
push(-2)[0]-2栈空,currentMin = -2,diff = -2 - (-2) = 0,存入 0
push(0)[0, 2]-2栈非空,diff = 0 - (-2) = 2 ≥ 0,不更新 currentMin,存入 2
push(-3)[0, 2, -1]-3栈非空,diff = -3 - (-2) = -1 < 0,更新 currentMin = -3,存入 -1

3. Pop 操作:如何恢复上一个最小值?

Pop 的核心是:弹出栈顶差值 diff;若 diff < 0,说明弹出的元素是「之前的最小值」,需要恢复 currentMin 到上一个最小值

关键推导(恢复上一个最小值):

假设之前的最小值为 oldMin,新元素 val 是更小值(val < oldMin),则:

  • 存入的 diff = val - oldMin(因为 push 时 currentMin 还是 oldMin
  • 之后 currentMin 被更新为 val(新最小值)
  • 现在要弹出这个 diff(即弹出 val),需要恢复 oldMin:由 diff = val - oldMin → 变形得 oldMin = val - diff而此时 currentMin = val,所以 oldMin = currentMin - diff(这就是恢复公式!)

示例 Pop 分步演示:

当前状态(push (-3) 后):栈 [0, 2, -1]currentMin = -3执行 pop ():

  1. 弹出栈顶 diff = -1
  2. 判断 diff = -1 < 0 → 说明弹出的元素是之前的最小值(-3),需要恢复 currentMin
  3. 恢复公式:oldMin = currentMin - diff = -3 - (-1) = -2
  4. 更新 currentMin = -2(回到上一个最小值)
  5. 弹出后栈状态:[0, 2]

4. Top 操作:如何通过差值获取真实元素?

Top 的核心是:根据栈顶 diff 的正负,反向推导真实元素值

分两种情况:

  • 情况 1:diff ≥ 0 → 真实值 = currentMin + diff原因:push 时 val ≥ currentMindiff 非负),diff = val - currentMin → val = currentMin + diff
  • 情况 2:diff < 0 → 真实值 = currentMin原因:push 时 val < currentMindiff 为负),此时 currentMin 已被更新为 val,所以真实值就是 currentMin

示例 Top 分步演示:

Pop 后状态:栈 [0, 2]currentMin = -2执行 top ():

  1. 栈顶 diff = 2(≥ 0)
  2. 真实值 = currentMin + diff = -2 + 2 = 0(与示例预期一致)

5. getMin 操作:直接返回当前最小值

因为 currentMin 实时记录着栈中所有元素的最小值,所以 getMin 直接返回 currentMin 即可,时间复杂度 O (1)。

示例 getMin () 演示:

  • Pop 后执行 getMin () → 返回 currentMin = -2(与示例预期一致)

三、完整代码(带详细注释)

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

  push(val) {
    if (this.stack.length === 0) {
      // 情况1:栈空,新元素就是最小值
      this.currentMin = val;
      this.stack.push(0); // 差值为 0(val - val)
    } else {
      // 情况2:栈非空,计算差值
      const diff = val - this.currentMin;
      this.stack.push(diff);
      // 若差值<0,说明新元素是更小值,更新currentMin
      if (diff < 0) {
        this.currentMin = val;
      }
    }
  }

  pop() {
    const diff = this.stack.pop(); // 弹出栈顶差值
    // 若差值<0,说明弹出的是之前的最小值,需要恢复上一个最小值
    if (diff < 0) {
      this.currentMin = this.currentMin - diff;
    }
  }

  top() {
    const diff = this.stack[this.stack.length - 1];
    // 差值≥0:真实值=当前最小值+差值;差值<0:真实值=当前最小值
    return diff >= 0 ? this.currentMin + diff : this.currentMin;
  }

  getMin() {
    return this.currentMin; // 直接返回实时最小值
  }
}

四、常见疑问解答(帮你避坑)

1. 为什么差值法不用辅助栈也能工作?

因为 diff 隐含了 “当前元素与最小值的关系”,currentMin 记录了 “当前最小值”,两者结合既能恢复真实元素,又能在最小值被弹出时恢复上一个最小值,相当于把辅助栈的功能 “压缩” 到了一个栈和一个变量中。

2. 差值会不会出现溢出?

  • JS 中不会:JS 的数值是 64 位浮点数,没有整数溢出限制。
  • 其他语言(如 Java/C++):会!若 val 和 currentMin 是极值(如 Integer.MIN_VALUE 和 Integer.MAX_VALUE),val - currentMin 会超出 int 范围,需要用 long 类型存储 diff

3. 什么时候用差值法?

  • 场景:追求极致空间效率(不允许用辅助栈)。
  • 面试中:先讲辅助栈解法(易理解),再补充差值法(展示思维深度),但要主动说明其语言局限性。

五、总结

差值法的核心是「用差值隐含元素信息,用变量跟踪最小值」,关键记住 3 个公式 / 规则:

  1. Push 差值:diff = val - currentMin(栈空时存 0)
  2. Pop 恢复:currentMin = currentMin - diff(仅当 diff < 0 时)
  3. Top 真实值:diff ≥ 0 → currentMin + diffdiff < 0 → currentMin

跟着示例一步步走一遍,再自己手动模拟一遍「连续 push 相同值」「单调递增序列」等场景,就能彻底掌握啦!