1. 单调栈基础概念解析
单调栈(Monotonic Stack)是一种特殊的栈结构,它在算法题解中经常用于处理"下一个更大/更小元素"这类问题。我第一次接触这个概念是在解决LeetCode 496题"下一个更大元素I"时,当时暴力解法超时后才意识到需要更高效的解决方案。
单调栈的核心特性是栈内元素始终保持单调递增或单调递减的顺序。当新元素准备入栈时,会通过出栈操作来维持这种单调性。这种结构特别适合处理需要比较相邻元素大小关系的场景,比如:
- 柱状图中最大矩形(LeetCode 84)
- 每日温度(LeetCode 739)
- 接雨水(LeetCode 42)
关键理解:单调栈不是一种独立的数据结构,而是对标准栈的一种使用策略。就像排队时让矮个子站前面一样,通过特定规则维护元素的排列顺序。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C++实现单调栈的标准模板
2.1 基础实现框架
下面是一个标准的单调递增栈实现模板(处理"下一个更小元素"问题):
cpp复制vector<int> nextSmallerElement(vector<int>& nums) {
stack<int> st;
vector<int> res(nums.size(), -1); // 默认-1表示没有更小元素
for(int i = 0; i < nums.size(); ++i) {
while(!st.empty() && nums[st.top()] > nums[i]) {
res[st.top()] = nums[i]; // 当前元素是栈顶元素的下一个更小值
st.pop();
}
st.push(i); // 存储的是索引而非值,方便处理
}
return res;
}
2.2 模板关键点解析
- 存储索引而非值:存储索引可以同时访问原始数组,还能处理重复元素的情况
- 循环条件:
nums[st.top()] > nums[i]维护的是严格递增栈 - 结果记录时机:在元素出栈时记录结果,此时当前元素就是它的下一个目标值
2.3 变体模板示例
递减栈模板(处理"下一个更大元素"):
cpp复制vector<int> nextGreaterElement(vector<int>& nums) {
stack<int> st;
vector<int> res(nums.size(), -1);
for(int i = 0; i < nums.size(); ++i) {
while(!st.empty() && nums[st.top()] < nums[i]) {
res[st.top()] = nums[i];
st.pop();
}
st.push(i);
}
return res;
}
3. 典型问题实战解析
3.1 LeetCode 739. 每日温度
问题描述:给定每日温度列表,返回需要等待多少天才能观察到更高温度。
cpp复制vector<int> dailyTemperatures(vector<int>& T) {
stack<int> st;
vector<int> res(T.size(), 0);
for(int i = 0; i < T.size(); ++i) {
while(!st.empty() && T[st.top()] < T[i]) {
res[st.top()] = i - st.top(); // 计算天数差
st.pop();
}
st.push(i);
}
return res;
}
优化点:这里结果存储的是天数差而非温度值本身,体现了单调栈应用的灵活性。
3.2 LeetCode 84. 柱状图中最大矩形
这是单调栈的经典难题,需要同时处理左右边界:
cpp复制int largestRectangleArea(vector<int>& heights) {
stack<int> st;
int maxArea = 0;
heights.push_back(0); // 哨兵值,确保最后能触发计算
for(int i = 0; i < heights.size(); ) {
if(st.empty() || heights[st.top()] <= heights[i]) {
st.push(i++);
} else {
int h = heights[st.top()];
st.pop();
int width = st.empty() ? i : i - st.top() - 1;
maxArea = max(maxArea, h * width);
}
}
return maxArea;
}
关键技巧:添加哨兵值简化边界条件处理,这是单调栈解题的常用技巧。
4. 性能分析与优化策略
4.1 时间复杂度分析
单调栈的时间复杂度通常是O(n),因为:
- 每个元素最多入栈一次、出栈一次
- 即使有嵌套循环,总操作次数也不会超过2n
4.2 空间优化技巧
- 原地修改:如果允许修改输入数组,可以用它存储部分结果
- 预分配空间:提前reserve栈和结果数组的空间,避免动态扩容
- 索引压缩:对于某些问题,可以用位运算组合存储多个信息
4.3 常见性能陷阱
- 不必要的值拷贝:优先使用const引用传递大型容器
- 频繁的栈顶访问:将
st.top()存入局部变量减少重复调用 - 错误的比较条件:注意
<和<=的选择会影响结果正确性
5. 调试与验证技巧
5.1 可视化调试方法
我常用的调试打印模板:
cpp复制void printStack(stack<int> st, const vector<int>& nums) {
cout << "Current Stack: ";
while(!st.empty()) {
cout << nums[st.top()] << " ";
st.pop();
}
cout << endl;
}
// 在循环内调用
printStack(st, nums);
5.2 边界测试用例
必须测试的几种特殊情况:
- 空输入
- 全部元素相同
- 严格递增/递减序列
- 包含INT_MAX/MIN的值
- 超长序列(测试性能)
5.3 单元测试示例
使用Catch2测试框架的示例:
cpp复制TEST_CASE("Test nextGreaterElement") {
vector<int> input = {4,2,3,1,5};
vector<int> expected = {5,3,5,5,-1};
REQUIRE(nextGreaterElement(input) == expected);
SECTION("Empty input") {
REQUIRE(nextGreaterElement({}).empty());
}
}
6. 工程实践中的扩展应用
6.1 处理循环数组
对于类似LeetCode 503这样的循环数组问题,常用解法:
cpp复制vector<int> nextGreaterElements(vector<int>& nums) {
int n = nums.size();
vector<int> res(n, -1);
stack<int> st;
for(int i = 0; i < 2 * n; ++i) {
int idx = i % n;
while(!st.empty() && nums[st.top()] < nums[idx]) {
res[st.top()] = nums[idx];
st.pop();
}
if(i < n) st.push(idx);
}
return res;
}
6.2 多维扩展
单调栈也可以扩展到二维问题,如最大矩形问题(LeetCode 85),通常需要:
- 先构建高度直方图
- 对每行应用柱状图算法
6.3 与其他数据结构结合
我曾在项目中用单调栈+线段树解决过区间最值问题:
- 用单调栈预处理边界信息
- 用线段树快速查询区间信息
- 组合结果时可以达到O(nlogn)复杂度
7. 学习资源与进阶路径
7.1 推荐练习题目
按难度排序的刷题清单:
- 简单:496、739
- 中等:503、901、907
- 困难:84、85、42
7.2 调试技巧总结
- 总是先在小数据上手算验证
- 用纸笔画出栈的变化过程
- 对特殊测试用例要单独验证
- 使用
assert检查不变式
7.3 性能优化检查表
提交前检查:
- 所有容器是否预分配了足够空间
- 是否避免了不必要的拷贝
- 循环条件是否最简形式
- 是否有提前退出的机会
