在 LeetCode 中,「最小栈」是一道经典的栈操作题目(LeetCode 155. Min Stack),要求我们设计一个栈,除了支持常规的 push、pop、top 操作外,还能在 常数时间 O(1) 内获取当前栈中的最小值。
本文将带你从最朴素的 O(n) 实现出发,逐步优化到高效的 O(1) 解法,并深入剖析其原理与注意事项。
一、问题需求回顾
实现一个 MinStack 类,支持以下操作:
push(x):将元素 x 压入栈。pop():删除栈顶元素。top():获取栈顶元素。getMin():获取栈中的最小元素,且要求 时间复杂度为 O(1) 。
⚠️ 注意:所有操作都必须在 常数时间 内完成,尤其是
getMin()。
二、方案一:暴力遍历(O(n) 时间)
实现思路
最直观的做法是:用一个普通数组模拟栈,getMin() 时遍历整个栈找最小值。
javascript
编辑
// ES5 构造函数
const MinStack = function() {
this.stack = []; // 主栈
};
MinStack.prototype.push = function(x) {
this.stack.push(x);
};
MinStack.prototype.pop = function() {
return this.stack.pop();
};
MinStack.prototype.top = function() {
if (this.stack.length > 0) {
return this.stack[this.stack.length - 1];
}
return undefined; // 安全处理空栈
};
MinStack.prototype.getMin = function() {
let minValue = Infinity;
for (let i = 0; i < this.stack.length; i++) {
if (this.stack[i] < minValue) {
minValue = this.stack[i];
}
}
return minValue;
};
优缺点分析
| 优点 | 缺点 |
|---|---|
| 实现简单,逻辑清晰 | getMin() 时间复杂度为 O(n),不满足题目要求 |
| 空间复杂度 O(n) | 高频调用 getMin() 时性能差 |
✅ 适合理解题意,但不能通过 LeetCode 的 O(1) 要求。
三、方案二:辅助栈法(O(1) 时间)
核心思想
引入第二个栈(辅助栈) ,专门用于维护当前栈中的最小值。
-
主栈:正常存储所有元素。
-
辅助栈:栈顶始终是当前主栈的最小值。
- 入栈时:若新元素 ≤ 辅助栈栈顶,则压入辅助栈。
- 出栈时:若主栈弹出的元素 == 辅助栈栈顶,则辅助栈也弹出。
💡 这本质上是一个单调非递增栈(Monotonic Stack)。
代码实现(ES5)
javascript
编辑
const MinStack = function() {
this.stack = []; // 主栈
this.minStack = []; // 辅助栈,栈顶为当前最小值
};
MinStack.prototype.push = function(x) {
this.stack.push(x);
// 注意:使用 <= 而非 <,以处理重复最小值
if (this.minStack.length === 0 || x <= this.minStack[this.minStack.length - 1]) {
this.minStack.push(x);
}
};
MinStack.prototype.pop = function() {
const top = this.stack.pop();
if (top === this.minStack[this.minStack.length - 1]) {
this.minStack.pop();
}
};
MinStack.prototype.top = function() {
return this.stack[this.stack.length - 1];
};
MinStack.prototype.getMin = function() {
return this.minStack[this.minStack.length - 1];
};
执行流程示例
假设依次执行:
js
编辑
push(-2); push(0); push(-3);
getMin(); // → -3
pop();
top(); // → 0
getMin(); // → -2
| 操作 | 主栈 stack | 辅助栈 minStack | 说明 |
|---|---|---|---|
| push(-2) | [-2] | [-2] | 首次入栈,辅助栈也入 |
| push(0) | [-2, 0] | [-2] | 0 > -2,不入辅助栈 |
| push(-3) | [-2, 0, -3] | [-2, -3] | -3 ≤ -2,入辅助栈 |
| getMin() | — | — | 返回 -3 ✅ |
| pop() | [-2, 0] | [-2] | 弹出 -3,等于辅助栈顶,同步弹出 |
| getMin() | — | — | 返回 -2 ✅ |
四、关键细节与注意事项
1. 为什么用 <= 而不是 <?
考虑重复最小值的情况:
js
编辑
push(1); push(1); push(1);
pop(); // 如果只用 <,辅助栈只存一个 1
getMin(); // 此时辅助栈已空!错误!
✅ 使用 <= 可确保每个最小值的出现都被记录,出栈时才能正确匹配。
2. 空栈安全处理
虽然题目通常保证不会在空栈上调用 pop/top/getMin,但在实际工程中建议增加判空:
js
编辑
if (this.stack.length === 0) throw new Error('Stack is empty');
3. 空间复杂度
- 最坏情况(如递减序列):辅助栈大小 = 主栈大小 → O(n)
- 平均情况:优于 O(n),但仍是 O(n) 级别
📌 空间换时间的经典案例。
五、其他优化思路(拓展思考)
方案三:单栈 + 差值编码(进阶)
不使用额外栈,而是用数学方法记录“最小值变化”。
- 存储
x - min而非x - 当
x < currentMin时,更新min,并压入负数 - 出栈时根据符号判断是否要还原
min
✅ 优点:空间更省
❌ 缺点:易溢出、逻辑复杂、可读性差
👉 适合面试炫技,工程中不推荐
六、总结要点
| 维度 | 暴力法(O(n)) | 辅助栈法(O(1)) |
|---|---|---|
| 时间复杂度 | getMin: O(n) | 所有操作:O(1) |
| 空间复杂度 | O(n) | O(n)(最坏) |
| 实现难度 | ⭐ | ⭐⭐ |
| 工程实用性 | ❌ | ✅✅✅ |
| 是否满足题目 | 否 | 是 |
✅ 推荐做法
辅助栈法是标准解法,简洁、高效、鲁棒,应作为首选。