Leetcode 155 Min Stack

本文介绍了一种特殊栈的设计方法,该栈支持在常数时间内进行push、pop、top操作及获取栈内最小元素。通过使用两个栈结构,能够高效地维护栈内元素并快速检索最小值。

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.

Example:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> Returns -3.
minStack.pop();
minStack.top();      --> Returns 0.
minStack.getMin();   --> Returns -2.
模拟实现栈,求取栈中的最小值的方法非常巧妙,可以仔细琢磨琢磨。

class MinStack {
public:
    /** initialize your data structure here. */
    stack<int> s1,s2;
    MinStack() {
        
    }
    
    void push(int x) {
        s1.push(x);
        if(s2.empty() || x <= s2.top()) s2.push(x);
    }
    
    void pop() {
        if(s1.top() == s2.top()) s2.pop();
        s1.pop();
    }
    
    int top() {
        return s1.top();
    }
    
    int getMin() {
        return s2.top();
    }
};

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(x);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */


<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]: 重复最小值需特殊处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值