从 O(n) 到 O(1):最小栈的优化之路
最小栈(Min Stack)是一个经典的数据结构面试题,要求我们设计一个栈,除了支持常规的
push、pop、top操作外,还能以 常数时间 获取当前栈中的最小值。本文将带你从最直观但效率较低的 O(n) 实现出发,逐步理解如何通过“辅助栈”技巧将其优化为真正的 O(1) 时间复杂度。
一、朴素实现:每次遍历找最小值(O(n))
首先,我们最容易想到的方法是——用一个普通数组模拟栈,然后在需要获取最小值时遍历整个栈:
// 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 || !this.stack.length) return;
return this.stack[this.stack.length - 1];
};
// O(n) 时间复杂度
MinStack.prototype.getMin = function() {
let minValue = Infinity;
const { stack } = this;
for (let i = 0; i < stack.length; i++) {
minValue = Math.min(minValue, stack[i]);
}
return minValue;
};
这个实现非常直观,但问题也很明显:每次调用 getMin() 都要遍历整个栈,时间复杂度为 O(n)。如果频繁调用 getMin(),性能会成为瓶颈。
那么,有没有办法让 getMin() 也达到 O(1) 呢?
二、优化思路:用空间换时间 —— 辅助栈
答案是肯定的。我们可以引入一个辅助栈(minStack) ,专门用来记录当前主栈中的最小值。
核心思想:
- 主栈
stack负责正常入栈出栈。 - 辅助栈
minStack的栈顶始终保存当前主栈中的最小值。 - 入栈时:如果新元素 ≤
minStack栈顶,则也将其压入minStack。 - 出栈时:如果主栈弹出的元素等于
minStack栈顶,则同步弹出minStack的栈顶。
这样,getMin() 只需返回 minStack 的栈顶,时间复杂度为 O(1)!
看完你是不是觉得似乎并不难理解?,然而自信满满的你可能已经落入陷阱!!!
有时候一行代码足以让你的整个逻辑毁于一旦-----> 一行代码的代价:最小栈为何要用 >= 而不是>
代码实现(O(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 || this.minStack[this.minStack.length - 1] >= x) {
this.minStack.push(x);
}
};
MinStack.prototype.pop = function() {
const popped = this.stack.pop();
// 如果弹出的值等于当前最小值,辅助栈也要同步弹出
if (popped === this.minStack[this.minStack.length - 1]) {
this.minStack.pop();
}
};
MinStack.prototype.top = function() {
if (!this.stack.length) return;
return this.stack[this.stack.length - 1];
};
// O(1) 时间复杂度!
MinStack.prototype.getMin = function() {
return this.minStack[this.minStack.length - 1];
};
关键点解析:
对于入栈操作时,使用>=而不是>,这是因为考虑到当我们连续压入相同的元素入栈时,如果使用>那么就会导致,只有第一个可能会入辅助栈,而后续的无法进入。这会导致一个什么问题呢?--->当我们执行出栈操作时,会导致辅助栈提前清空,因为存在多个相等的元素,这就有可能提取弹出了最小元素,导致结果出错。所以使用>=是为了处理重复最小值的情况
三、为什么这个方法有效?
关键在于单调性:minStack 是一个非严格递减栈(单调栈的一种)。它只在遇到更小或相等的值时才增长,因此栈顶永远代表“从栈底到当前位置”的最小值。
这正是“最小栈”问题的经典解法,也是面试中高频考察的思维模型——用额外空间换取时间效率。
四、总结
| 方法 | getMin() 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 朴素遍历 | O(n) | O(1) | 数据量小、getMin 调用少 |
| 辅助栈 | O(1) | O(n) | 高频查询最小值,追求性能 |
通过引入辅助栈,我们将最小值查询从线性时间优化到了常数时间,完美满足了题目要求。这种“空间换时间”的思想,在算法设计中极为常见,值得深入掌握。
希望这篇文章能帮你清晰理解最小栈的优化过程!如果你正在准备面试,不妨动手实现一遍,并思考边界情况(如空栈、重复最小值等)。
Happy coding! 🚀