力扣解题-155. 最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
- MinStack() 初始化堆栈对象。
- void push(int val) 将元素val推入堆栈。
- void pop() 删除堆栈顶部的元素。
- int top() 获取堆栈顶部的元素。
- int getMin() 获取堆栈中的最小元素。
示例 1: 输入: ["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]]
输出: [null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
提示:
-2³¹ <= val <= 2³¹ - 1
pop、top 和 getMin 操作总是在 非空栈 上调用
push, pop, top, and getMin最多被调用 3 * 10⁴ 次
Related Topics
栈、设计
第一次解答
解题思路
核心方法:双栈法(数据栈+最小栈),通过两个栈分别存储“所有元素”和“当前栈内最小元素”,保证getMin()操作的时间复杂度为O(1),所有核心操作均满足常数时间要求,是本题的经典最优解法。
核心原理铺垫(双栈设计的合理性)
要实现“常数时间获取最小值”,需在元素入栈/出栈时同步维护最小值状态:
- 数据栈(stack):存储所有入栈的元素,支持常规的push/pop/top操作;
- 最小栈(minStack):存储“当前数据栈中的最小元素”,栈顶始终是当前数据栈的最小值;
- 核心规则:入栈时同步更新最小栈,出栈时若弹出的是最小值则同步弹出最小栈的栈顶,保证最小栈与数据栈的状态一致。
核心逻辑拆解
- 初始化:创建两个空栈(
stack存储数据,minStack存储最小值); - push操作:
- 元素
val压入数据栈; - 若最小栈为空,或
val ≤ 最小栈栈顶元素(新元素更小/相等),将val压入最小栈; - (注:用
≤而非<,避免相同最小值被重复弹出导致错误,如连续push多个-3时,minStack需存储多个-3);
- 元素
- pop操作:
- 弹出数据栈栈顶元素
val; - 若
val等于最小栈栈顶元素(说明弹出的是当前最小值),同步弹出最小栈栈顶; - 题目保证pop操作在非空栈调用,仍保留空栈异常处理增强健壮性;
- 弹出数据栈栈顶元素
- top操作:返回数据栈栈顶元素(非空校验+异常抛出);
- getMin操作:返回最小栈栈顶元素(非空校验+异常抛出)。
具体步骤(以示例1为例)
| 操作 | 数据栈(stack) | 最小栈(minStack) | getMin结果 | top结果 |
|---|---|---|---|---|
| push(-2) | [-2] | [-2] | - | - |
| push(0) | [-2, 0] | [-2] | - | - |
| push(-3) | [-2, 0, -3] | [-2, -3] | - | - |
| getMin() | [-2, 0, -3] | [-2, -3] | -3 | - |
| pop() | [-2, 0] | [-2] | - | - |
| top() | [-2, 0] | [-2] | - | 0 |
| getMin() | [-2, 0] | [-2] | -2 | - |
性能说明
- 时间复杂度:所有操作(push/pop/top/getMin)均为O(1)(栈的入栈/出栈/取栈顶都是常数时间),执行用时4ms击败100%用户;
- 空间复杂度:O(n)(最坏情况所有元素都存入两个栈,如严格递减序列),内存消耗46.18MB击败66.45%用户;
- 优势:
- 逻辑清晰,双栈分工明确,易于理解和维护;
- 异常处理增强代码健壮性,符合工业级开发规范;
- 最小栈仅存储最小值相关元素,空间开销可控。
private Deque<Integer> stack;
private Deque<Integer> minStack;
public MinStack() {
stack = new ArrayDeque<>();
minStack = new ArrayDeque<>();
}
public void push(int val) {
stack.push(val);
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}
public void pop() {
if(stack.isEmpty()){
throw new RuntimeException("栈为空");
}else {
int val=stack.pop();
if(val==minStack.peek()) {
minStack.pop();
}
}
}
public int top() {
if(!stack.isEmpty()) {
return stack.peek();
}else {
throw new RuntimeException("栈为空");
}
}
public int getMin() {
if(!minStack.isEmpty()) {
return minStack.peek();
}else {
throw new RuntimeException("栈为空");
}
}
示例解答
解题思路
解法1:单栈优化版(空间优化)
核心方法:单栈存储“元素+当前最小值”对,用一个栈替代两个栈,每个栈元素存储[当前值, 入栈时的最小值],在保证O(1)时间复杂度的同时,逻辑更紧凑(空间复杂度仍为O(n),但减少了一个栈的对象开销)。
核心设计思路
- 栈中每个元素是长度为2的数组:
[val, currentMin],其中currentMin表示将val入栈后,栈内的最小值; - push时计算当前最小值(对比
val和栈顶的currentMin),将[val, currentMin]压入栈; - pop/top/getMin时直接操作栈顶数组的对应位置。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class MinStack {
private Deque<int[]> stack; // 每个元素为 {当前值, 当前最小值}
public MinStack() {
stack = new ArrayDeque<>();
}
public void push(int val) {
if (stack.isEmpty()) {
// 栈空时,当前最小值就是val
stack.push(new int[]{val, val});
} else {
// 栈非空时,当前最小值 = min(栈顶最小值, val)
int currentMin = Math.min(stack.peek()[1], val);
stack.push(new int[]{val, currentMin});
}
}
public void pop() {
if (stack.isEmpty()) {
throw new RuntimeException("栈为空");
}
stack.pop();
}
public int top() {
if (stack.isEmpty()) {
throw new RuntimeException("栈为空");
}
return stack.peek()[0];
}
public int getMin() {
if (stack.isEmpty()) {
throw new RuntimeException("栈为空");
}
return stack.peek()[1];
}
}
优势说明
- 时间复杂度:仍为O(1),与双栈法性能一致;
- 空间复杂度:O(n)(每个元素存储两个整数,实际内存开销与双栈法接近);
- 核心优势:
- 仅用一个栈,减少对象创建和维护的开销;
- 无需在pop时判断是否弹出最小值,逻辑更简洁;
- 天然支持重复最小值场景,无需处理
≤的边界问题。
解法2:差值法(极致空间优化,进阶思路)
核心方法:单栈存储“当前值与最小值的差值”,用一个变量记录当前最小值,栈中存储val - min的差值,通过差值反向推导原值和新最小值,实现O(1)额外空间(栈本身的存储不计入)。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class MinStack {
private Deque<Long> stack; // 用Long避免差值溢出
private long minVal; // 当前最小值
public MinStack() {
stack = new ArrayDeque<>();
}
public void push(int val) {
if (stack.isEmpty()) {
minVal = val;
stack.push(0L); // 差值为0
} else {
long diff = (long) val - minVal;
stack.push(diff);
// 差值<0说明val更小,更新最小值
if (diff < 0) {
minVal = val;
}
}
}
public void pop() {
if (stack.isEmpty()) {
throw new RuntimeException("栈为空");
}
long diff = stack.pop();
// 差值<0说明弹出的是最小值,恢复上一个最小值
if (diff < 0) {
minVal = minVal - diff;
}
}
public int top() {
if (stack.isEmpty()) {
throw new RuntimeException("栈为空");
}
long diff = stack.peek();
// 差值<0:栈顶值就是当前minVal;否则:minVal + diff
return diff < 0 ? (int) minVal : (int) (minVal + diff);
}
public int getMin() {
if (stack.isEmpty()) {
throw new RuntimeException("栈为空");
}
return (int) minVal;
}
}
核心原理与适用场景
- 空间优势:仅用一个变量存储最小值,栈存储差值,额外空间复杂度O(1)(栈本身的存储是必要开销);
- 注意事项:
- 用
Long存储差值,避免val=Integer.MIN_VALUE、minVal=Integer.MAX_VALUE时差值溢出; - 逻辑较复杂,需要反向推导原值和最小值,可读性低于双栈/单栈对法;
- 用
- 适用场景:对内存要求极高的场景,如嵌入式开发或大数据量处理。
总结
- 双栈法(第一次解答):逻辑清晰、易于维护,所有操作O(1)时间复杂度,是工程首选的经典解法;
- 单栈对法:单栈存储“值+最小值”,逻辑紧凑,性能与双栈法持平,减少了一个栈的对象开销;
- 差值法:极致空间优化(O(1)额外空间),但逻辑复杂、有溢出风险,适合对内存要求极高的场景;
- 关键技巧:
- 核心目标:通过“空间换时间”保证
getMin()的O(1)时间复杂度; - 边界处理:重复最小值需用
≤判断入栈,避免弹出时丢失最小值; - 健壮性:保留空栈异常处理,符合工业级代码规范。
- 核心目标:通过“空间换时间”保证