01最小栈实现原理
最小栈是一种特殊的栈结构,除了支持标准栈的所有操作外,还需要在O(1)时间内找到栈中的最小元素。
这是一个经典的数据结构设计问题,不通过遍历实现时间复杂度为O(1),考虑可以用空间换时间的思想。
问题分析
问题描述 LeetCode 155: https://leetcode.cn/problems/min-stack/
设计一个支持以下操作的栈:
- push(x) - 将元素x推入栈中
- pop() - 删除栈顶元素
- top() - 获取栈顶元素
- getMin() - 检索栈中的最小元素
所有操作的时间复杂度都必须是O(1)。
核心思路
使用辅助栈记录最小值:
- data栈:存储实际数据
- min栈:存储到当前位置的最小值
关键点:min栈data栈保持同步,每次push操作,min栈都要压入一个值(当前最小值)。
02基于Java内置Stack
方案一:基于Java内置Stack
实现代码
import java.util.Stack;
/**
* 基于Java内置Stack的最小栈实现
* 缺点是常数时间慢,有同步开销
*/
class MinStackWithBuiltIn {
private Stack<Integer> dataStack; // 存储实际数据
private Stack<Integer> minStack; // 存储最小值信息
public MinStackWithBuiltIn() {
dataStack = new Stack<>();
minStack = new Stack<>();
}
/**
* 压栈操作
* @param val 要压入的值
*/
public void push(int val) {
dataStack.push(val);
// 更新最小值的栈:如果新值更小或相等,或者是第一个元素,则压入栈内
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
} else {
// 保持与dataStack同步数量,所以重复压入当前最小值
minStack.push(minStack.peek());
}
}
/**
* 弹栈操作
*/
public void pop() {
dataStack.pop();
minStack.pop(); // 两个栈保持同步
}
/**
* 获取栈顶元素
*/
public int top() {
return dataStack.peek();
}
/**
* 获取当前栈中最小元素
*/
public int getMin() {
return minStack.peek();
}
时间复杂度:
- push():O(1) - 栈的基本操作
- pop():O(1) - 栈的基本操作
- top():O(1) - 直接访问栈顶
- getMin():O(1) - 直接访问最小值栈顶
缺点是常数时间慢,即便是O(1)常数时间。
空间复杂度:
- O(n) - 需要两个栈,每个栈最多存n个元素
03基于数组实现的优化
实现代码
/**
* 基于数组的最小栈实现
* 优点:常数时间快,无同步开销
* 缺点:需要预估容量上限
*/
class MinStackWithArray {
private static final int MAXN = 8001; // 根据面试题题干,确定的最大容量
private int[] dataArray; // 存实际数据的数组
private int[] minArray; // 存最小值的数组
private int size; // 当前栈的大小
public MinStackWithArray() {
dataArray = new int[MAXN];
minArray = new int[MAXN];
size = 0;
}
/**
* 压栈操作
*/
public void push(int val) {
dataArray[size] = val;
// 更新最小值数组
if (size == 0 || val <= minArray[size - 1]) {
minArray[size] = val; // 新的最小值
} else {
minArray[size] = minArray[size - 1]; // 沿用之前的最小值
}
size++;
}
/**
* 弹栈操作
*/
public void pop() {
size--; // 只需要减少size即可,无需清理数据!
}
/**
* 获取栈顶元素
*/
public int top() {
return dataArray[size - 1];
}
/**
* 获取当前栈中最小元素
*/
public int getMin() {
return minArray[size - 1];
}
}
时间复杂度:
- push():O(1) - 数组赋值操作
- pop():O(1) - 只需修改size
- top():O(1) - 直接数组访问
- getMin():O(1) - 直接数组访问
时间复杂度为O(1),常数操作时间很快。
空间复杂度:
- O(MAXN) - 预分配固定大小的数组空间
04算法细节分析
最小值栈的同步策略
// 压栈时的最小值更新逻辑
if (val <= currentMin) {
minStack.push(val); // 新的最小值
} else {
minStack.push(currentMin); // 保持同步,重复压入当前最小值
}
这种方式的原则是:
- 两个栈始终保持相同的大小
- 弹栈时无需额外判断,两栈直接同步弹出
- 任何时候最小栈顶都是当前的最小值
边界条件处理
// 第一个元素的特殊处理
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
性能优化要点
常数时间优化
- 数组访问 vs 栈操作:数组直接索引比栈的方法调用更快
- 避免同步开销:Java的Stack继承自Vector,有同步机制
- 减少对象创建:数组实现避免了栈节点对象的频繁创建
空间优化策略
// 空间优化版本:只在必要时压入最小值
if (val <= currentMin) {
minStack.push(currentMin); // 先压入旧的最小值
minStack.push(val); // 再压入新的最小值
}
适用场景分析
选择Stack实现的场景
- 快速原型开发
- 对性能要求不严格
- 栈大小变化范围大且不可预估
选择数组实现的场景
- 对性能要求严格的场合
- 可以预估栈大小上限的情况
Anyway,最小栈的核心思想是**空间换时间**,通过额外的数据结构,来实现O(1)的查询效率。
文章参考:bilibili左程云算法系列
青轴作响,叨叨有声。
欢迎留言,一起分享你的技术见闻。每一篇都聊技术人的真实选择与成长。IT之路,不孤单。
声明:文章仅代表作者本人观点,供学习和交流使用。
公众号:叨叨猿的青轴日记
更多往期内容请在公众号回复[算法那些事]
合作/投稿:daodaoyuanblue@163.com