最小栈(Min Stack)实现详解:从 O(n) 到 O(1) 的优化

82 阅读3分钟

在 LeetCode 中,「最小栈」是一道经典的栈操作题目(LeetCode 155. Min Stack),要求我们设计一个栈,除了支持常规的 pushpoptop 操作外,还能在 常数时间 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)(最坏)
实现难度⭐⭐
工程实用性✅✅✅
是否满足题目

✅ 推荐做法

辅助栈法是标准解法,简洁、高效、鲁棒,应作为首选。