在算法与数据结构中,最小栈(Min Stack)是一个经典问题 🧠。它要求我们设计一个栈,除了支持常规的 push、pop、top 操作外,还能以 O(1) 时间复杂度获取当前栈中的最小元素。
本文将从问题出发,逐步引导你理解如何通过 辅助栈(Auxiliary Stack)高效实现最小栈,并深入探讨其背后的原理——单调栈思想 ⚙️。
一、问题描述 📌
设计一个栈,支持以下操作:
push(x)➕:将元素 x 压入栈pop()➖:移除栈顶元素top()🔝:获取栈顶元素getMin()📉:获取栈中的最小元素
✅ 所有操作的时间复杂度应为 O(1) 。
二、朴素解法(O(n) 获取最小值)⚠️
最直接的想法是:每次调用 getMin() 时遍历整个栈,找出最小值。
// 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 undefined;
return this.stack[this.stack.length - 1];
};
MinStack.prototype.getMin = function () {
let min = Infinity;
for (let i = 0; i < this.stack.length; i++) {
min = Math.min(min, this.stack[i]);
}
return min;
};
✅ 优点:逻辑简单,空间占用少(仅一个栈)
❌ 缺点:getMin() 时间复杂度为 O(n) ,不满足题目要求 ❌
三、优化方案:引入辅助栈(O(1) 获取最小值)🚀
为了实现 O(1) 的 getMin(),我们需要 空间换时间 💾 —— 使用一个 辅助栈(minStack)来实时维护当前栈中的最小值。
🔑 核心思想
- 主栈
stack:正常存储所有元素 - 辅助栈
minStack:单调非递增栈,栈顶始终是当前主栈中的最小值
📌 单调栈:一种特殊的栈结构,其中元素按某种顺序(如非递增或非递减)排列。这里我们维护一个 非递增 的辅助栈。
🛠️ 操作规则
| 操作 | 主栈行为 | 辅助栈行为 |
|---|---|---|
push(x) ➕ | 压入 x | 若 minStack 为空 或 x <= minStack.top(),则压入 x |
pop() ➖ | 弹出栈顶 | 若弹出的元素等于 minStack.top(),则 minStack 也弹出 |
getMin() 📉 | — | 直接返回 minStack.top() |
⚠️ 注意:使用
<=而非<是为了处理 重复最小值 的情况。例如连续 push 两个1,若只用<,第二个1不会进入辅助栈,导致 pop 第一个1后最小值丢失!
四、代码实现(ES5 构造函数)💻
const MinStack = function () {
this.stack = []; // 主栈
this.minStack = []; // 辅助栈(单调非递增)
};
MinStack.prototype.push = function (x) {
this.stack.push(x);
// 当 minStack 为空,或 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 !== undefined && top === this.minStack[this.minStack.length - 1]) {
this.minStack.pop();
}
return top;
};
MinStack.prototype.top = function () {
if (this.stack.length === 0) return undefined;
return this.stack[this.stack.length - 1];
};
MinStack.prototype.getMin = function () {
if (this.minStack.length === 0) return undefined;
return this.minStack[this.minStack.length - 1];
};
五、示例演示 🎯
假设执行以下操作序列:
const ms = new MinStack();
ms.push(-2);
ms.push(0);
ms.push(-3);
console.log(ms.getMin()); // → -3 📉
ms.pop();
console.log(ms.top()); // → 0 🔝
console.log(ms.getMin()); // → -2 📉
内部状态变化:
| 操作 | stack | minStack | getMin() |
|---|---|---|---|
push(-2) | [-2] | [-2] | -2 |
push(0) | [-2, 0] | [-2] | -2 |
push(-3) | [-2, 0, -3] | [-2, -3] | -3 ✅ |
pop() | [-2, 0] | [-2] | -2 ✅ |
可以看到,minStack 始终保持非递增,且栈顶即为当前最小值 🎯。
六、为什么这是“单调栈”?🧩
辅助栈 minStack 实际上是一个 单调非递增栈(Monotonic Non-Increasing Stack):
- ✅ 每次只有当新元素 不大于 当前栈顶时才入栈
- ✅ 这保证了栈中元素从底到顶 不增加
- ✅ 因此,栈顶永远是最小值,且在主栈弹出最小值时,辅助栈同步弹出,下一个最小值自然浮出
这种设计巧妙地利用了栈的 后进先出(LIFO)特性与单调性,实现了高效的最小值维护 💡。
七、复杂度分析 📊
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
push ➕ | O(1) | O(1)(均摊) |
pop ➖ | O(1) | O(1) |
top 🔝 | O(1) | O(1) |
getMin 📉 | O(1) | O(1) |
- 总空间复杂度:O(n),最坏情况下(如输入序列单调递减),辅助栈与主栈大小相同。
八、常见误区与注意事项 ⚠️
- 比较条件必须用
<=
🔸 若用<,重复最小值会导致辅助栈提前弹空,后续getMin()出错! - 边界判断不可少
🔸 在pop和top中需检查栈是否为空,避免访问undefined。 - 不要混淆“最小值”和“全局最小”
🔸getMin()返回的是 当前栈内 的最小值,不是历史最小。
九、总结 🎉
最小栈问题通过引入 辅助栈,将 getMin() 从 O(n) 优化到 O(1),是 空间换时间 的经典范例 💎。其核心在于利用 单调栈 的思想,让辅助栈始终保持非递增结构,从而在任意时刻都能快速访问最小值。
掌握这一技巧,不仅能解决 LeetCode 第 155 题,还能为后续学习更复杂的单调栈应用(如滑动窗口最大值、柱状图最大矩形等)打下坚实基础 🚀。
💡 思考题:能否只用一个栈实现最小栈?(提示:可以,但需要对入栈元素进行编码,牺牲数值范围换取空间)