(huawei)最小栈

最小栈:O(1)时间获取栈最小值的双栈解法详解

引言:为什么需要"最小栈"?

在常规栈(Stack)数据结构中,我们可以轻松实现push(入栈)、pop(出栈)、top(获取栈顶元素)三个核心操作,且时间复杂度均为O(1) 。但在实际开发中,我们经常会遇到一个需求------快速获取当前栈中的最小值,比如在表达式计算、单调栈相关算法中。

如果直接使用常规栈实现"获取最小值"功能,最直接的思路是遍历整个栈查找最小值,此时时间复杂度会退化为O(n) (n为栈中元素个数),在数据量较大时效率极低。因此,我们需要设计一种特殊的栈结构(即"最小栈"),让getMin(获取最小值)操作也能达到O(1) 时间复杂度。

核心思路:双栈协同实现最小栈

要让getMin操作达到O(1),关键在于"提前记录最小值 "------用一个辅助栈同步存储主栈中的"当前最小值",通过双栈协同确保辅助栈的栈顶始终是主栈的最小值。

双栈分工

  • 主栈(min_stack1) :存储所有入栈元素,承担常规栈的pushpoptop功能。
  • 辅助栈(min_stack2):仅存储主栈中的"阶段性最小值",栈顶元素始终是当前主栈的最小值。

核心规则(重点!)

辅助栈的操作需要严格遵循以下规则,才能保证栈顶始终是最小值:

  1. 入栈(push)

    • 先将元素压入主栈;
    • 若辅助栈为空,或当前元素小于等于辅助栈顶元素,则将该元素也压入辅助栈("小于等于"是为了处理重复最小值的情况,避免漏记)。
  2. 出栈(pop)

    • 先判断主栈顶元素是否与辅助栈顶元素相等(若相等,说明当前最小值即将被弹出,辅助栈需同步弹出);
    • 再将主栈顶元素弹出。
  3. 获取最小值(getMin):直接返回辅助栈顶元素(此时栈顶已保证是主栈当前最小值)。

  4. 获取栈顶(top):直接返回主栈顶元素(与常规栈一致)。

代码实现与逐行解析(C++版)

下面基于题目中的代码,逐函数拆解实现逻辑,并补充关键注释。

完整代码

cpp 复制代码
#include <stack>
using namespace std;

class MinStack {
    // 主栈:存储所有元素
    stack<int> min_stack1;
    // 辅助栈:存储当前最小值,栈顶始终是主栈的最小值
    stack<int> min_stack2;
public:
    /** 构造函数:初始化栈(默认构造即可,无需额外操作) */
    MinStack() {
        // stack容器的默认构造函数已完成初始化,无需手动清空
    }
    
    /** 入栈操作:主栈必压,辅助栈按需压 */
    void push(int x) {
        // 1. 元素先压入主栈
        min_stack1.push(x);
        
        // 2. 辅助栈为空 或 当前元素<=辅助栈顶(保证最小值不丢失),则压入辅助栈
        if (min_stack2.empty() || min_stack2.top() >= x) {
            min_stack2.push(x);
        }
    }
    
    /** 出栈操作:主栈必弹,辅助栈按需弹 */
    void pop() {
        // 1. 若主栈顶等于辅助栈顶(当前最小值要被弹出),辅助栈同步弹出
        if (min_stack1.top() == min_stack2.top()) {
            min_stack2.pop();
        }
        
        // 2. 主栈弹出顶部元素
        min_stack1.pop();
    }
    
    /** 获取主栈顶元素 */
    int top() {
        return min_stack1.top();
    }
    
    /** 获取当前栈的最小值(直接返回辅助栈顶) */
    int getMin() {
        return min_stack2.top();
    }
};

/** 实例化与调用示例 */
// int main() {
//     MinStack* obj = new MinStack();
//     obj->push(-2);  // 主栈:[-2],辅助栈:[-2]
//     obj->push(0);   // 主栈:[-2,0],辅助栈:[-2](0 > -2,不压辅助栈)
//     obj->push(-3);  // 主栈:[-2,0,-3],辅助栈:[-2,-3](-3 <= -2,压辅助栈)
//     cout << "当前最小值:" << obj->getMin() << endl;  // 输出 -3
//     obj->pop();     // 主栈弹出-3,辅助栈同步弹出-3 → 主栈:[-2,0],辅助栈:[-2]
//     cout << "栈顶元素:" << obj->top() << endl;       // 输出 0
//     cout << "当前最小值:" << obj->getMin() << endl;  // 输出 -2
//     delete obj;     // 释放内存
//     return 0;
// }

关键细节答疑

  1. 为什么辅助栈要存"小于等于"的元素,而不是"小于"?

    假设主栈依次入栈 2 → 2

    • 若辅助栈只存"小于"的元素:第一次入栈2(辅助栈2),第二次入栈2(2不小于辅助栈顶,不存);
    • 当主栈弹出第一个2时,主栈剩余2,但辅助栈已空,此时getMin会报错。
      因此,"小于等于"能确保重复最小值被同步记录,避免最小值丢失。
  2. 出栈时为什么要先判断辅助栈,再弹主栈?

    若先弹主栈,主栈顶元素已被删除,无法再与辅助栈顶对比,会导致辅助栈的最小值无法同步更新。

复杂度分析

操作 时间复杂度 空间复杂度
push(x) O(1) O(n)
pop() O(1) O(n)
top() O(1) O(n)
getMin() O(1) O(n)
  • 时间复杂度 :所有操作均为栈的基础操作(push/pop/top),均为常数时间O(1)。
  • 空间复杂度 :最坏情况下(主栈元素严格递减,如5→4→3→2→1),辅助栈需存储所有元素,空间复杂度为O(n)(n为主栈元素个数)。

拓展思考:单栈解法 vs 双栈解法

除了双栈法,还有一种常见的"单栈解法"------用栈存储"元素值+当前最小值"的键值对(pair<int, int>),每次入栈时计算新的最小值(当前元素与栈顶最小值的较小者)。

单栈解法示例代码

cpp 复制代码
class MinStack {
    stack<pair<int, int>> st;  // pair<当前元素, 当前栈最小值>
public:
    MinStack() {}
    
    void push(int x) {
        if (st.empty()) {
            st.push({x, x});  // 空栈时,元素本身就是最小值
        } else {
            // 新最小值 = min(当前元素, 栈顶最小值)
            int new_min = min(x, st.top().second);
            st.push({x, new_min});
        }
    }
    
    void pop() {
        st.pop();  // 弹出键值对,最小值同步更新
    }
    
    int top() {
        return st.top().first;  // 返回当前元素
    }
    
    int getMin() {
        return st.top().second;  // 返回当前最小值
    }
};

两种解法对比

维度 双栈解法 单栈解法
逻辑直观性 稍复杂(需理解双栈协同) 更直观(键值对绑定关系)
空间开销 最坏O(n),最好O(1)(主栈递增时) 固定O(n)(每个元素都带最小值)
代码简洁度 略繁琐(两个栈操作) 更简洁(单个栈操作)

实际开发中可根据需求选择:若追求空间最优(主栈递增场景多),选双栈法;若追求代码简洁和直观,选单栈法。

总结

最小栈的核心是"用辅助空间换时间 "------通过额外的辅助栈(或键值对)提前记录最小值,将getMin操作从O(n)优化到O(1),同时保证pushpop操作仍为O(1)。

双栈解法的关键在于辅助栈的"按需入栈/出栈"规则(小于等于入栈、相等出栈),只要掌握这一规则,就能轻松实现高效的最小栈。建议结合文中的main函数测试用例,手动模拟栈的变化过程,加深对逻辑的理解!

相关推荐
艾莉丝努力练剑4 小时前
【AI大模型接入SDK】WebSocket & SSE 协议
网络·c++·websocket·网络协议·学习·面试
码匠许师傅4 小时前
【设计模式精讲】6.抽象工厂(Abstract Factory)
c++·设计模式
GKxx4 小时前
在 HarmonyOS 上从源码构建 GCC 16(gcc/g++ + libstdc++ + libsanitizer):完整记录
c++·华为·harmonyos·鸿蒙·gcc
晴天的雨.9924 小时前
类和对象下(内部类,匿名对象,对象拷贝时的编译器优化)
开发语言·c++·算法
小义_4 小时前
JDK 深度解析
java·linux·开发语言·python·面试
旖旎夜光4 小时前
LeetCode 238:除自身以外数组的乘积(前缀和) —— 题解
数据结构·c++·算法·leetcode·前缀和
数智启示录4 小时前
Doris精讲篇(六) 从第一批小文件到 -235:Doris 是怎样被正常写入拖死的
大数据·数据库·经验分享·面试·flink
黑臂麒麟4 小时前
HarmonyOS随手账本App——从项目搭建到数据模型设计
华为·app·鸿蒙
RisunJan4 小时前
Android 面试知识点整理
android·面试·职场和发展
黑臂麒麟4 小时前
Harmony鸿蒙实战应用4:随手账本——编辑与删除账单
ubuntu·华为·鸿蒙