在日常开发中,我们经常会遇到需要快速获取当前数据结构中最小值的需求。如果只是简单地使用一个普通栈(Stack),那么每次调用 getMin() 都需要遍历整个栈来找到最小值,时间复杂度为 O(n) ,效率较低。
今天我们就来深入剖析一种经典且高效的解决方案——最小栈(Min Stack) ,并结合代码和逻辑图解,带你彻底理解如何通过「辅助栈」的设计思想,在常数时间内完成 getMin 操作!
🎯 问题描述
设计一个支持以下操作的栈:
push(x):将元素 x 压入栈顶。pop():移除栈顶元素。top():获取栈顶元素。getMin():获取栈中最小元素。
要求:所有操作的时间复杂度均为 O(1) 。
💡 解题思路:辅助栈法
✅ 核心思想:两个栈
我们使用两个栈来实现这个功能:
- 主栈(stack) :存储所有实际压入的元素。
- 辅助栈(stack1) :只存储“可能成为最小值”的元素,即维护一个单调递减栈。
🔍 关键点:辅助栈不是记录所有元素,而是记录每个阶段的最小值。
🔁 入栈逻辑
当新元素 val 入栈时:
if (this.stack1.length === 0 || val <= this.stack1[this.stack1.length - 1]) {
this.stack1.push(val);
}
this.stack.push(val);
- 如果辅助栈为空,或者
val小于等于当前辅助栈顶(即比现有最小值还小或相等),就将val推入辅助栈。 - 这样保证了辅助栈始终是非递增的(单调递减栈)。
⚠️ 注意:这里用了
<=而不是<,是为了处理重复最小值的情况。比如连续压入两个 3,我们要保留两个 3 在辅助栈中,否则出栈后无法正确恢复最小值。
🚫 出栈逻辑
const val = this.stack.pop();
if (val === this.stack1[this.stack1.length - 1]) {
this.stack1.pop();
}
- 先从主栈弹出元素。
- 若该元素恰好等于辅助栈顶,则说明它是当前阶段的最小值,也需从辅助栈弹出。
✅ 这样能确保辅助栈永远保存的是对应时刻下的最小值。
🔍 查看栈顶与最小值
top() { return this.stack[this.stack.length - 1]; }
getMin() { return this.stack1[this.stack1.length - 1]; }
top()返回主栈顶。getMin()直接返回辅助栈顶即可,因为辅助栈顶就是当前全局最小值。
🧠 示例演示
假设我们执行如下操作序列:
push(5)
push(3)
push(8)
push(1)
pop()
getMin()
| 操作 | 主栈 | 辅助栈 |
|---|---|---|
| push(5) | [5] | [5] |
| push(3) | [5,3] | [5,3] |
| push(8) | [5,3,8] | [5,3] |
| push(1) | [5,3,8,1] | [5,3,1] |
| pop() | [5,3,8] | [5,3] |
| getMin() | —— | → 3 |
可以看到,即使中间有更大的数插入,只要最小值发生变化,辅助栈就会更新;而删除时也能精准同步。
✅ 代码完整版(JavaScript)
var MinStack = function() {
this.stack = [];
this.stack1 = []; // 辅助栈,存储最小值
};
MinStack.prototype.push = function(val) {
if (this.stack1.length === 0 || val <= this.stack1[this.stack1.length - 1]) {
this.stack1.push(val);
}
this.stack.push(val);
};
MinStack.prototype.pop = function() {
const val = this.stack.pop();
if (val === this.stack1[this.stack1.length - 1]) {
this.stack1.pop();
}
};
MinStack.prototype.top = function() {
return this.stack[this.stack.length - 1];
};
MinStack.prototype.getMin = function() {
return this.stack1[this.stack1.length - 1];
};
📈 复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| push | O(1) | O(n) |
| pop | O(1) | O(n) |
| top | O(1) | O(1) |
| getMin | O(1) | O(1) |
✅ 所有操作均达到最优的 O(1),空间上最坏情况是 O(n),但平均情况下远低于此。
🎯 总结:为什么辅助栈这么牛?
“以空间换时间” 的极致体现
- 主栈负责正常的数据进出;
- 辅助栈则像一位“观察员”,默默记录每一个关键时刻的最小值;
- 不依赖遍历,不浪费时间,真正实现了高效查询。