1. 最小栈问题概述
最小栈(Min Stack)是LeetCode上经典的算法设计题(#155题),要求设计一个栈数据结构,除了支持常规的push、pop、top操作外,还能在常数时间内检索到栈中的最小元素。这个问题看似简单,却考察了数据结构设计的核心思想——如何在空间和时间复杂度之间寻找平衡点。
我在面试候选人时发现,约70%的初级开发者会直接回答"遍历栈找最小值",这显然不符合O(1)时间复杂度要求。正确的解法需要巧妙利用辅助数据结构,这也是该题被列为高频面试题的原因。下面我将通过Java实现,拆解三种典型解法及其适用场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解法一:双栈同步法
2.1 核心思路
最直观的解法是维护两个栈:
- 主栈(stack):存储所有元素
- 最小栈(minStack):存储当前主栈对应位置的最小值
java复制class MinStack {
private Stack<Integer> stack;
private Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
}
2.2 操作实现细节
push操作:
java复制public void push(int val) {
stack.push(val);
if(minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
} else {
minStack.push(minStack.peek()); // 重复当前最小值
}
}
pop操作:
java复制public void pop() {
stack.pop();
minStack.pop(); // 同步弹出
}
getMin操作:
java复制public int getMin() {
return minStack.peek();
}
关键点:每次push/pop时两个栈必须同步操作,确保minStack栈顶始终对应主栈当前范围的最小值
2.3 复杂度分析
- 时间复杂度:所有操作O(1)
- 空间复杂度:O(n)(需要额外n大小的minStack)
2.4 适用场景
适合对时间复杂度要求严格但空间充足的场景。在实际工程中,当栈操作频繁但内存不是瓶颈时,这是最稳妥的选择。
3. 解法二:差值存储法
3.1 核心思路
通过存储元素与当前最小值的差值来压缩空间:
- 使用单个栈
- 用变量min记录当前最小值
- 栈中存储实际值与min的差值
java复制class MinStack {
private Stack<Long> stack;
private long min;
}
3.2 关键操作实现
push操作:
java复制public void push(int val) {
if(stack.isEmpty()) {
stack.push(0L);
min = val;
} else {
stack.push((long)val - min); // 存储差值
min = Math.min(min, val);
}
}
pop操作:
java复制public void pop() {
long diff = stack.pop();
if(diff < 0) { // 说明弹出的是当前最小值
min = min - diff; // 还原前一个最小值
}
}
getMin操作:
java复制public int getMin() {
return (int)min;
}
3.3 注意事项
- 需要使用long类型防止整数溢出
- 当差值为负数时,表示该元素是当时的最小值
- 实际工程中要考虑数值范围限制
3.4 复杂度分析
- 时间复杂度:所有操作O(1)
- 空间复杂度:O(1)(理论上最优)
4. 解法三:链表节点法
4.1 数据结构设计
每个节点除了存储值,还记录当前最小值:
java复制class Node {
int val;
int min;
Node next;
}
class MinStack {
private Node head;
}
4.2 操作实现
push操作:
java复制public void push(int val) {
Node newNode = new Node();
newNode.val = val;
newNode.min = (head == null) ? val : Math.min(val, head.min);
newNode.next = head;
head = newNode;
}
pop操作:
java复制public void pop() {
head = head.next;
}
getMin操作:
java复制public int getMin() {
return head.min;
}
4.3 方案对比
| 方案 | 空间复杂度 | 实现难度 | 适用场景 |
|---|---|---|---|
| 双栈同步法 | O(n) | 简单 | 通用场景 |
| 差值存储法 | O(1) | 中等 | 内存敏感场景 |
| 链表节点法 | O(n) | 中等 | 需要频繁增删的场景 |
5. 实战应用场景
5.1 浏览器前进后退
浏览器历史记录栈需要快速获取最近访问时间最早的页面,类似最小栈的思想。
5.2 交易系统监控
在金融交易系统中,需要实时获取当前交易周期内的最低报价,最小栈结构可以高效支持这种需求。
5.3 游戏状态管理
游戏中的undo/redo操作需要记录操作序列的同时,快速获取某些关键状态的最小/最大值。
6. 常见问题排查
-
空栈处理:
java复制public int top() { if(stack.isEmpty()) throw new RuntimeException("Stack is empty"); return stack.peek(); } -
整数溢出问题:
在差值法中,当val=Integer.MAX_VALUE,min=Integer.MIN_VALUE时,差值会溢出。解决方案是使用long类型存储。 -
并发安全问题:
在多线程环境下,需要加锁或使用ConcurrentStack等线程安全结构:java复制private Lock lock = new ReentrantLock(); public void push(int val) { lock.lock(); try { // 操作代码 } finally { lock.unlock(); } }
7. 算法扩展思考
最小栈问题可以延伸出许多变种:
- 最大栈(O(1)获取最大值)
- 中间值栈(O(1)获取中位数)
- 平均值栈(O(1)获取平均值)
这些变种都可以通过类似的辅助数据结构思想来解决。比如最大栈只需将minStack改为maxStack,其他逻辑完全对称。
在实际编码面试中,面试官可能会要求在不使用额外空间的情况下实现最小栈,这就需要更巧妙的数学方法,比如利用原数据的特定位来存储差值信息。不过这种解法通常只适用于特定场景,通用性较差。
