(huawei)最小栈
最小栈:O(1)时间获取栈最小值的双栈解法详解
引言:为什么需要“最小栈”?
在常规栈(Stack)数据结构中,我们可以轻松实现push(入栈)、pop(出栈)、top(获取栈顶元素)三个核心操作,且时间复杂度均为O(1)。但在实际开发中,我们经常会遇到一个需求——快速获取当前栈中的最小值,比如在表达式计算、单调栈相关算法中。
如果直接使用常规栈实现“获取最小值”功能,最直接的思路是遍历整个栈查找最小值,此时时间复杂度会退化为O(n)(n为栈中元素个数),在数据量较大时效率极低。因此,我们需要设计一种特殊的栈结构(即“最小栈”),让getMin(获取最小值)操作也能达到O(1) 时间复杂度。
核心思路:双栈协同实现最小栈
要让getMin操作达到O(1),关键在于“提前记录最小值”——用一个辅助栈同步存储主栈中的“当前最小值”,通过双栈协同确保辅助栈的栈顶始终是主栈的最小值。
双栈分工
- 主栈(min_stack1):存储所有入栈元素,承担常规栈的
push、pop、top功能。 - 辅助栈(min_stack2):仅存储主栈中的“阶段性最小值”,栈顶元素始终是当前主栈的最小值。
核心规则(重点!)
辅助栈的操作需要严格遵循以下规则,才能保证栈顶始终是最小值:
-
入栈(push):
- 先将元素压入主栈;
- 若辅助栈为空,或当前元素小于等于辅助栈顶元素,则将该元素也压入辅助栈(“小于等于”是为了处理重复最小值的情况,避免漏记)。
-
出栈(pop):
- 先判断主栈顶元素是否与辅助栈顶元素相等(若相等,说明当前最小值即将被弹出,辅助栈需同步弹出);
- 再将主栈顶元素弹出。
-
获取最小值(getMin):直接返回辅助栈顶元素(此时栈顶已保证是主栈当前最小值)。
-
获取栈顶(top):直接返回主栈顶元素(与常规栈一致)。
代码实现与逐行解析(C++版)
下面基于题目中的代码,逐函数拆解实现逻辑,并补充关键注释。
完整代码
#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;
// }
关键细节答疑
-
为什么辅助栈要存“小于等于”的元素,而不是“小于”?
假设主栈依次入栈2 → 2:- 若辅助栈只存“小于”的元素:第一次入栈2(辅助栈[2]),第二次入栈2(2不小于辅助栈顶,不存);
- 当主栈弹出第一个2时,主栈剩余[2],但辅助栈已空,此时
getMin会报错。
因此,“小于等于”能确保重复最小值被同步记录,避免最小值丢失。
-
出栈时为什么要先判断辅助栈,再弹主栈?
若先弹主栈,主栈顶元素已被删除,无法再与辅助栈顶对比,会导致辅助栈的最小值无法同步更新。
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
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>),每次入栈时计算新的最小值(当前元素与栈顶最小值的较小者)。
单栈解法示例代码
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),同时保证push和pop操作仍为O(1)。
双栈解法的关键在于辅助栈的“按需入栈/出栈”规则(小于等于入栈、相等出栈),只要掌握这一规则,就能轻松实现高效的最小栈。建议结合文中的main函数测试用例,手动模拟栈的变化过程,加深对逻辑的理解!
