【LeetCode Hot100 刷题日记 (70/100)】155. 最小栈 —— 辅助栈设计思想🧠

3 阅读5分钟

📌 题目链接:155. 最小栈 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:栈、设计、辅助栈

⏱️ 目标时间复杂度:O(1) (每个操作)

💾 空间复杂度:O(n)


我们遇到了一道经典的数据结构设计题最小栈(Min Stack) 。这道题不仅考察你对栈基本操作的理解,更深入检验你是否掌握如何通过辅助结构优化查询性能的能力。

这类题目在面试中极其常见,尤其是大厂系统设计或基础能力轮次——因为“常数时间获取最小值”看似简单,实则暗藏玄机。今天我们就来彻底拆解它!


🔍 题目分析

题目要求我们实现一个特殊的栈 MinStack,支持以下 五个操作

  • push(val):将元素压入栈
  • pop():弹出栈顶元素
  • top():返回栈顶元素(不弹出)
  • getMin()在 O(1) 时间内返回当前栈中的最小元素

⚠️ 关键约束:

  • 所有操作必须在 常数时间 O(1) 内完成;
  • poptopgetMin 只在非空栈上调用;
  • 值域为 [-2^31, 2^31 - 1],即标准 int 范围。

💡 核心难点
普通栈无法直接获取最小值(需遍历,O(n))。如何在不牺牲时间效率的前提下,动态维护最小值


🛠️ 核心算法及代码讲解:辅助栈(Auxiliary Stack)

✅ 算法思想:空间换时间 + 同步维护最小值

我们引入一个辅助栈 min_stack,与主栈 x_stack 同步操作,但只存储“到当前位置为止的最小值”。

🎯 关键洞察:

对于任意时刻,若主栈顶部是元素 a,那么从栈底到 a 的所有元素中,最小值是确定的。我们可以a 入栈时就记录这个最小值,并存入辅助栈。

这样,getMin() 就退化为 min_stack.top(),天然 O(1)!

🔄 操作规则:

操作主栈 x_stack辅助栈 min_stack
push(x)压入 x压入 min(min_stack.top(), x)
pop()弹出栈顶同步弹出栈顶
getMin()返回 min_stack.top()

📌 注意:初始化时,min_stack 可先压入 INT_MAX,避免空栈判断(C++ 版常用技巧)。


💻 C++ 核心代码(带逐行注释)

class MinStack {
    stack<int> x_stack;      // 主栈:存储所有元素
    stack<int> min_stack;    // 辅助栈:存储对应位置的最小值
public:
    MinStack() {
        min_stack.push(INT_MAX); // 初始化辅助栈,避免空栈比较
    }
    
    void push(int x) {
        x_stack.push(x); // 主栈正常入栈
        // 辅助栈入栈:当前最小值 = min(之前最小值, 新元素)
        min_stack.push(min(min_stack.top(), x));
    }
    
    void pop() {
        x_stack.pop();   // 主栈弹出
        min_stack.pop(); // 辅助栈同步弹出
    }
    
    int top() {
        return x_stack.top(); // 返回主栈栈顶
    }
    
    int getMin() {
        return min_stack.top(); // 辅助栈栈顶即为当前最小值
    }
};

为什么这样是对的?

  • 每次 push 时,min_stack 记录的是 从栈底到当前元素的全局最小值
  • pop 时同步弹出,保证两个栈始终对齐
  • 因此 min_stack.top() 永远代表当前主栈的最小值。

🧩 解题思路(分步拆解)

  1. 明确需求:需要 O(1) 获取最小值 → 普通栈不行,必须预存信息。

  2. 选择策略:使用辅助数据结构 → 栈天然匹配 LIFO 特性,选辅助栈

  3. 设计同步机制

    • 入栈时,辅助栈压入 min(当前最小, 新值)
    • 出栈时,辅助栈同步弹出;
  4. 处理边界

    • 初始化辅助栈为 INT_MAX,简化逻辑;
    • 题目保证非空调用,无需额外判空(但实际工程建议加)。
  5. 验证示例

    push(-2): x=[-2], min=[INT_MAX, -2]
    push(0):  x=[-2,0], min=[..., -2]
    push(-3): x=[-2,0,-3], min=[..., -3]
    getMin → -3 ✅
    pop → x=[-2,0], min=[..., -2]
    getMin → -2 ✅
    

📊 算法分析

项目分析
时间复杂度所有操作均为 O(1) 。每个操作最多调用两次栈操作(push/pop/top),而栈操作本身是 O(1)。
空间复杂度O(n) ,其中 n 是 push 操作次数。最坏情况下,两个栈各存 n 个元素。
是否可优化空间?有!可以只在新元素 ≤ 当前最小值时才压入辅助栈(见 JS 版本),但 C++ 官方解法采用“全同步”更简洁,且面试中两种都可接受。
面试高频点✅ 如何 O(1) 获取最值? ✅ 辅助栈 vs 单变量?(单变量无法处理 pop 后最小值恢复问题) ✅ 能否用 vector/deque 实现?(可以,但 stack 语义更清晰)

💡 延伸思考:如果要求 getMax() 呢?同理,再加一个 max_stack 即可!


💻 完整代码

✅ C++ 版本

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class MinStack {
    stack<int> x_stack;
    stack<int> min_stack;
public:
    MinStack() {
        min_stack.push(INT_MAX);
    }
    
    void push(int x) {
        x_stack.push(x);
        min_stack.push(min(min_stack.top(), x));
    }
    
    void pop() {
        x_stack.pop();
        min_stack.pop();
    }
    
    int top() {
        return x_stack.top();
    }
    
    int getMin() {
        return min_stack.top();
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    MinStack minStack;
    minStack.push(-2);
    minStack.push(0);
    minStack.push(-3);
    cout << minStack.getMin() << "\n"; // -3
    minStack.pop();
    cout << minStack.top() << "\n";    // 0
    cout << minStack.getMin() << "\n"; // -2

    return 0;
}

✅ 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(){
    if(this.stack.length === 0) return;
    const popped = this.stack.pop();
    // 如果弹出的是当前最小值,辅助栈也要弹出
    if(popped === 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 与 C++ 版本差异说明

  • C++ 版:全同步辅助栈,代码简洁,空间 O(n),适合快速实现;
  • JS 版:条件入栈,空间更优(最坏仍是 O(n),但平均更省),适合强调空间效率的场景;
  • 面试时两种都正确,可主动说明 trade-off!

🌟 总结 & 面试要点

  • 核心思想:用辅助栈预存状态,实现 O(1) 查询;

  • 适用场景:任何需要“动态维护极值 + LIFO 结构”的问题;

  • 易错点

    • 忘记同步 pop;
    • 初始化未处理空栈;
    • 比较时用 < 而非 <=(导致重复最小值丢失);
  • 进阶变体

    • 最大栈;
    • 支持 getAverage() 的栈(需维护 sum);
    • 支持 undo 操作的栈(需历史快照)。

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!