很多同学觉得我上篇文章介绍用差值法解决最小栈问题“绕”,核心是没搞懂「差值如何隐含真实值」「最小值如何动态更新与恢复」。这篇文章会用 “人话 + 分步演示 + 公式推导” 的方式,从底层逻辑到代码实现,帮你彻底搞懂差值法的核心原理。
一、差值法的核心思想
差值法的本质是:不直接存储元素本身,而是存储 “当前元素与当前最小值的差值” ,再用一个变量记录「当前最小值」。通过这两个信息,反向推导真实元素值,同时保证所有操作都是 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 元素)
-
逻辑:此时没有 “之前的最小值”,新元素就是最小值。
-
操作:
- 设新元素为
val,则currentMin = val(新元素成为最小值) - 差值
diff = val - currentMin = val - val = 0,将0存入栈中
- 设新元素为
-
为什么存 0?因为第一个元素的 “自身与自身的差值” 是 0,后续能通过
currentMin + 0恢复真实值。
情况 2:栈非空(后续 push 元素)
-
逻辑:用新元素
val减去「当前最小值currentMin」得到diff,存入栈中;若diff < 0,说明val比当前最小值还小,需要更新currentMin为val。 -
关键:
diff的正负能判断val是否是新的最小值:diff ≥ 0:val ≥ currentMin(不是新最小值,currentMin不变)diff < 0:val < 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 ():
- 弹出栈顶
diff = -1 - 判断
diff = -1 < 0→ 说明弹出的元素是之前的最小值(-3),需要恢复currentMin - 恢复公式:
oldMin = currentMin - diff = -3 - (-1) = -2 - 更新
currentMin = -2(回到上一个最小值) - 弹出后栈状态:
[0, 2]
4. Top 操作:如何通过差值获取真实元素?
Top 的核心是:根据栈顶 diff 的正负,反向推导真实元素值。
分两种情况:
- 情况 1:
diff ≥ 0→ 真实值 =currentMin + diff原因:push 时val ≥ currentMin(diff非负),diff = val - currentMin→val = currentMin + diff - 情况 2:
diff < 0→ 真实值 =currentMin原因:push 时val < currentMin(diff为负),此时currentMin已被更新为val,所以真实值就是currentMin
示例 Top 分步演示:
Pop 后状态:栈 [0, 2],currentMin = -2执行 top ():
- 栈顶
diff = 2(≥ 0) - 真实值 =
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 个公式 / 规则:
- Push 差值:
diff = val - currentMin(栈空时存 0) - Pop 恢复:
currentMin = currentMin - diff(仅当diff < 0时) - Top 真实值:
diff ≥ 0 → currentMin + diff;diff < 0 → currentMin
跟着示例一步步走一遍,再自己手动模拟一遍「连续 push 相同值」「单调递增序列」等场景,就能彻底掌握啦!