在算法的世界里,时间与空间是一对永恒的矛盾。而“最小栈”问题,正是我们练习如何在二者之间取得平衡的经典案例。
引言:什么是最小栈?
最小栈(Min Stack) 是一种支持以下四种操作的数据结构:
push(x):将元素 x 压入栈中pop():弹出栈顶元素top():获取栈顶元素getMin():以 O(1) 时间复杂度 获取栈中的最小值
乍一看,前三项是标准栈的操作,但最后一项却暗藏玄机——如何在常数时间内获取当前栈中的最小值?
本文将带你从最朴素的思路出发,逐步优化,最终实现一个高效、优雅的最小栈,并深入剖析其背后的单调栈思想。
第一版:暴力遍历(O(n) 时间)
我们先来看最直观的做法:每次调用 getMin() 时,遍历整个栈,找出最小值。
const MiniStack = function() {
this.stack = [];
}
MiniStack.prototype.push = function(x) {
this.stack.push(x);
}
MiniStack.prototype.pop = function() {
return this.stack.pop();
}
MiniStack.prototype.top = function() {
if (!this.stack.length) return;
return this.stack[this.stack.length - 1];
}
MiniStack.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²) —— 这显然无法满足工程需求。
第二版:预留接口(未完成)
在 2.js 中,作者只定义了骨架,getMin 方法留空:
MiniStack.prototype.getMin = function() {
// O(n)
}
这其实是一个很好的教学设计:引导你思考——如何突破 O(n) 的瓶颈?
答案是:空间换时间。
第三版:辅助栈 + 单调栈思想(O(1) 时间)
真正的优雅解法来了!我们引入一个辅助栈(minStack) ,专门用来维护当前栈中的最小值。
核心思想:
- 主栈
stack负责正常入栈出栈; - 辅助栈
stack2维护一个非严格递减序列(即单调不增),栈顶始终是当前最小值。
入栈规则:
- 当
x <= stack2[栈顶]时,也将x压入stack2。
出栈规则:
- 如果主栈弹出的元素等于
stack2的栈顶,则stack2也同步弹出。
const MiniStack = function() {
this.stack = [];
this.stack2 = []; // 辅助栈:单调不增
}
MiniStack.prototype.push = function(x) {
this.stack.push(x);
// 注意:使用 >=,保证重复最小值也能正确维护
if (this.stack2.length === 0 || this.stack2[this.stack2.length - 1] >= x) {
this.stack2.push(x);
}
}
MiniStack.prototype.pop = function() {
const top = this.stack.pop();
if (top === this.stack2[this.stack2.length - 1]) {
this.stack2.pop();
}
}
MiniStack.prototype.top = function() {
return this.stack[this.stack.length - 1];
}
MiniStack.prototype.getMin = function() {
return this.stack2[this.stack2.length - 1];
}
✅ 优点:
- 所有操作均为 O(1) 时间复杂度;
- 代码简洁,逻辑清晰;
- 利用了单调栈的经典思想。
💡 关键洞察:
辅助栈其实是一个单调不增栈(Monotonic Decreasing Stack) 。它并不存储所有元素,只保留那些“曾经成为最小值”的候选者。这种“懒惰维护”策略,正是算法优化的精髓。
对比总结
| 实现方式 | getMin() 时间 | 空间开销 | 是否满足题目要求 |
|---|---|---|---|
| 暴力遍历 | O(n) | O(1) | ❌ |
| 辅助栈(单调) | O(1) | O(n) | ✅ |
在大多数场景下,用少量额外空间换取时间效率的大幅提升,是非常值得的。
延伸思考:为什么用 >= 而不是 >?
考虑如下入栈序列:[3, 1, 1]
- 若使用
>,第二个1不会入辅助栈; - 当第一个
1被pop()时,辅助栈变为空,此时getMin()就会出错!
因此,必须使用 >=,确保重复的最小值被完整记录,从而在出栈时能正确同步。
结语
最小栈问题看似简单,却蕴含着深刻的算法思想:
- 空间换时间 的权衡艺术;
- 单调栈 在动态维护极值中的巧妙应用;
- 边界条件(如重复值)的严谨处理。
下次当你面对“需要快速获取历史极值”的问题时,不妨想想:是否可以用一个辅助结构来维护单调性?
算法之美,不在炫技,而在恰到好处的平衡。
参考资料:
- LeetCode 155. Min Stack
- 《算法导论》——栈与单调性结构
- 本文代码基于 ES5 构造函数实现,兼容性好,适合深入理解原型链机制
✨ 欢迎点赞、收藏、评论交流!你还有什么优化思路?