LeetCode(155) Min Stack

本文介绍了一种支持常数时间内进行push、pop、top及获取最小元素操作的栈设计方法。通过使用两个栈,一个用于常规操作,另一个用于维护最小值。在进行push或pop操作时,同步更新最小值栈,确保其始终保存当前最小值。

题目如下:

Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.
push(x) -- Push element x onto stack.
pop() -- Removes the element on top of the stack.
top() -- Get the top element.
getMin() -- Retrieve the minimum element in the stack.


分析如下:

1 想到用两个stack来维护这个结构。1个stack用来正常进行stack的push pop等操作。另外1个stack用来维护min.每次对stack进行pop或者push时,也对min_stack进行相应操作。

2 第2个stack的大小是可以进行优化的。不一定每个min都要入栈min_stack,push的时候,只入栈比当前min小或者相等的值就可以了。pop的时候,比较待pop元素和min_stack的top的大小。如果待pop元素和min_stack top相等,则将min stack进行pop。

3 online judge做得真心好,如果用了两个栈但是没有空间优化,是没法ac的。

直接把官网上的unlocked之后的解答抄过来:

O(1) runtime, O(n) space – Extra stack:

Use an extra stack to keep track of the current minimum value. During the push operation we choose the new element or the current minimum, whichever that is smaller to push onto the min stack.


O(1) runtime, O(n) space – Minor space optimization:

If a new element is larger than the current minimum, we do not need to push it on to the min stack. When we perform the pop operation, check if the popped element is the same as the current minimum. If it is, pop it off the min stack too.


我的代码:

class MinStack {
private:
    std::stack<int> stack;
    std::stack<int> min_stack;
public:
    void push(int x) {
        stack.push(x);
        if (min_stack.empty() || ((!min_stack.empty()) && x <= min_stack.top())) {  //NOTE: 是“小于等于”,不是“小于”
            min_stack.push(x);
        }
    }
    
    void pop() {
        if (!stack.empty()) {
            if (stack.top() == min_stack.top())
                min_stack.pop();
            stack.pop();
        }
    }
    
    int top() {
        if (!stack.empty())
            return stack.top();
    }
    
    int getMin() {
        if (!min_stack.empty())
            return min_stack.top();
    }
};



<think>我们讨论的是LeetCode 155题“最小栈”。题目要求设计一个栈,除了常规的push、pop、top操作外,还能在常数时间内检索到栈中的最小元素。根据引用[1][2][3][4][5],我们看到了几种不同的实现方法,现在需要分析并给出最优解法。 ### 方法对比 1. **双栈法(两个栈)**:引用[1][3]使用两个栈,一个栈存储数据,另一个栈存储当前最小值。每次push时,如果新元素小于等于最小栈的栈顶,则压入最小栈;pop时,如果弹出的元素等于最小栈栈顶,则最小栈也弹出。但注意,引用[1]和[3]的实现中,最小栈每次push都会压入当前最小值(即使新元素比当前最小值大,也重复压入当前最小值)。这样,两个栈的大小始终保持一致,pop时同时弹出即可。 2. **单栈法(使用数组/元组)**:引用[2]使用一个栈,但栈中每个元素是一个数组(或元组),其中第一个元素是当前值,第二个元素是从栈底到该元素时的最小值。这样,每次push时,当前元素的最小值可以通过比较新元素和栈顶元素的最小值得到。这种方法只需要一个栈,但每个元素存储的信息更多。 3. **优化双栈法(最小栈非同步压入)**:引用[4]提到,最小栈只有在遇到小于等于当前最小值的元素时才压入。这样,最小栈中的元素不会重复,且数量会少于主栈。pop时,只有弹出的元素等于最小栈栈顶时,最小栈才弹出。 ### 最优解法分析 - **时间效率**:三种方法在push、pop、top、getMin操作的时间复杂度都是$O(1)$,满足题目要求。 - **空间效率**:方法1(双栈同步)在最坏情况下(所有元素递减)空间为$2n$。方法2(单栈)每个元素存储两个值,空间也是$2n$。方法3(最小栈非同步)在最好情况下(元素递增)最小栈只存储一个元素,空间为$n+1$;最坏情况(递减)空间为$2n$。因此,方法3在空间上更优。 - **实现复杂度**:方法1和方法2实现简单,且pop操作不需要判断,直接弹出。方法3在pop时需要判断主栈弹出的元素是否等于最小栈栈顶,如果相等则弹出最小栈,实现稍复杂,但节省空间。 ### 最优解法:优化双栈法(最小栈非同步压入) 参考引用[4],我们采用以下策略: - 使用两个栈:主栈`stack`和最小栈`min_stack`。 - `push(x)`操作: - 主栈直接压入`x`。 - 如果最小栈为空,或者`x`小于等于最小栈的栈顶元素,则将`x`压入最小栈。 - `pop()`操作: - 弹出主栈栈顶元素,记为`val`。 - 如果`val`等于最小栈的栈顶元素,则弹出最小栈的栈顶。 - `top()`:返回主栈栈顶。 - `getMin()`:返回最小栈栈顶。 **注意**:为什么使用小于等于?考虑连续压入相同最小值的情况,例如:push(0), push(1), push(0)。第一个0压入最小栈,然后压入1(不压入最小栈),再压入0,此时0等于最小栈栈顶0,所以压入。这样,当pop第一个0时,最小栈的0被弹出,而第二个0还在,保证了正确性。 ### 代码实现(C++) ```cpp class MinStack { private: stack<int> data_stack; stack<int> min_stack; public: MinStack() {} void push(int val) { data_stack.push(val); // 当最小栈为空,或者新元素小于等于最小栈栈顶时,压入最小栈 if (min_stack.empty() || val <= min_stack.top()) { min_stack.push(val); } } void pop() { if (data_stack.empty()) return; int val = data_stack.top(); data_stack.pop(); // 如果弹出的元素等于最小栈栈顶,则弹出最小栈 if (val == min_stack.top()) { min_stack.pop(); } } int top() { return data_stack.top(); } int getMin() { return min_stack.top(); } }; ``` ### 示例演算(以引用[5]的操作为例) 操作序列:`push(-2)`, `push(0)`, `push(-3)`, `getMin()`, `pop()`, `top()`, `getMin()` | 操作 | 主栈(栈底→栈顶) | 最小栈(栈底→栈顶) | 返回值 | 说明 | |----------------|-------------------|---------------------|--------|--------------------------------------| | 初始化 | [] | [] | | | | push(-2) | [-2] | [-2] | | 最小栈空,压入-2 | | push(0) | [-2,0] | [-2] | | 0>-2,最小栈不变 | | push(-3) | [-2,0,-3] | [-2,-3] | | -3<-2,压入-3 | | getMin() | | | -3 | 最小栈栈顶为-3 | | pop() | [-2,0] | [-2] | | 弹出主栈-3,等于最小栈顶-3,弹出 | | top() | | | 0 | 主栈栈顶为0 | | getMin() | | | -2 | 最小栈栈顶为-2 | ### 空间复杂度分析 - 最坏情况:元素递减,每个新元素都小于前一个,最小栈和主栈大小相同,空间复杂度$O(n)$。 - 最好情况:元素递增,最小栈只存储第一个元素,空间复杂度$O(1)$(最小栈大小1,主栈大小n)。 - 平均情况:空间复杂度$O(n)$,但实际使用空间小于等于同步双栈法。 ### 相关问题 1. 为什么最小栈的push条件要包含等于的情况? 2. 如果栈中存储的是对象(而非基本类型),如何修改比较逻辑? 3. 在并发环境下,如何保证最小栈的线程安全? [^1]: 双栈同步压入法,空间复杂度为$O(2n)$ [^2]: 单栈存储元组法,空间复杂度$O(2n)$ [^3]: 双栈非同步压入法,空间复杂度最坏$O(2n)$,最好$O(1)$ [^4]: 最小栈只在必要时压入,节省空间 [^5]: 操作序列示例</think>### LeetCode 155 最小栈最优解法详解 最小栈要求在常数时间内实现 `push`、`pop`、`top` 和 `getMin` 操作。最优解法采用**双栈结构 + 非同步压栈策略**,在保证 $O(1)$ 时间复杂度的同时优化空间效率。 #### 核心思想 1. **双栈结构**: - `data_stack`:存储常规数据 - `min_stack`:存储当前最小值 2. **非同步压栈**: - 仅当新元素 $\leq$ `min_stack` 栈顶时,才压入 `min_stack` - 避免存储冗余的最小值(如引用[4]的优化) #### 时间复杂度分析 | 操作 | 时间复杂度 | 说明 | |----------|------------|--------------------------| | `push()` | $O(1)$ | 栈操作 + 一次比较 | | `pop()` | $O(1)$ | 栈操作 + 一次比较 | | `top()` | $O(1)$ | 直接访问栈顶 | | `getMin()`| $O(1)$ | 直接访问 `min_stack` 栈顶| #### 空间复杂度分析 - **最坏情况**:$O(2n)$(元素严格递减) - **最佳情况**:$O(n)$(元素递增,`min_stack` 仅存首元素) - **平均情况**:显著优于同步压栈法(引用[1][3]) --- ### 最优解法代码实现(C++) ```cpp class MinStack { private: stack<int> data_stack; // 主数据栈 stack<int> min_stack; // 最小值辅助栈 public: MinStack() {} // 初始化 void push(int val) { data_stack.push(val); // 仅当 min_stack 为空或 val ≤ 当前最小值时压栈 if (min_stack.empty() || val <= min_stack.top()) { min_stack.push(val); } } void pop() { // 若弹出值等于当前最小值,则同步弹出 min_stack if (data_stack.top() == min_stack.top()) { min_stack.pop(); } data_stack.pop(); } int top() { return data_stack.top(); } int getMin() { return min_stack.top(); // 直接返回最小值栈顶 } }; ``` --- ### 演算示例(引用[5]的操作序列) | 操作 | `data_stack` (栈底→栈顶) | `min_stack` (栈底→栈顶) | 返回值 | 关键逻辑说明 | |------------------|--------------------------|--------------------------|--------|----------------------------------| | `MinStack()` | `[]` | `[]` | | 初始化空栈 | | `push(-2)` | `[-2]` | `[-2]` | | `min_stack` 压入首个元素 | | `push(0)` | `[-2, 0]` | `[-2]` | | 0 > -2,不压 `min_stack` | | `push(-3)` | `[-2, 0, -3]` | `[-2, -3]` | | -3 < -2,压入 `min_stack` | | `getMin()` | | | **-3** | 访问 `min_stack.top()` | | `pop()` | `[-2, 0]` | `[-2]` | | 弹出 -3 且等于 `min_stack.top()`| | `top()` | | | **0** | 访问 `data_stack.top()` | | `getMin()` | | | **-2** | `min_stack` 栈顶回退到 -2 | > **关键点**:`pop()` 时仅当弹出值等于 `min_stack.top()` 才同步弹出,避免最小值丢失(如 `push(0)` 后弹出不影响最小值)[^4]。 --- ### 对比其他解法 1. **同步双栈法(引用[1][3])**: - `min_stack` 始终与 `data_stack` 同大小 - 空间复杂度恒为 $O(2n)$,存在冗余存储 - 实现简单但空间效率低 2. **单栈元组法(引用[2])**: - 单栈存储 `(value, current_min)` - 空间复杂度 $O(2n)$,访问逻辑稍复杂 - 无栈同步问题,但扩展性受限 > **最优解优势**:空间效率显著提升(尤其对递增序列),代码简洁易维护[^4]。 --- ### 边界处理 1. **空栈操作**: - `pop()`/`top()`/`getMin()` 需检查栈空(LeetCode 测试用例保证非空) 2. **重复最小值**: ```cpp push(0); // data_stack=[0], min_stack=[0] push(1); // data_stack=[0,1], min_stack=[0] push(0); // data_stack=[0,1,0], min_stack=[0,0] ``` - 使用 `val <= min_stack.top()` 保证重复最小值被压栈 - `pop()` 时同步弹出避免过早丢失最小值 --- ### 相关问题 1. 如何处理自定义对象的最小栈? → 需实现比较运算符(如 `operator<=`)[^4]。 2. 为什么 `min_stack` 压栈条件是 $\leq$ 而非 $<$? → 防止重复最小值场景下 `getMin()` 错误(见边界处理示例)[^5]。 3. 如何扩展支持多维度最小值(如最大值、中位数)? → 设计多维辅助栈或使用红黑树结构[^2]。 [^1]: 双栈同步压入法空间效率低 [^2]: 单栈元组法扩展性受限 [^3]: 同步双栈法存在冗余存储 [^4]: 非同步压栈优化空间效率 [^5]: 重复最小值需特殊处理
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值