在算法面试和日常编程中,单调栈是一种高效解决特定问题的数据结构。它能在O(n)时间复杂度内处理"寻找数组中每个元素前后第一个比它大/小的元素"这类问题。本文将从底层原理出发,通过大量代码示例和实际应用场景,带你彻底掌握这一重要算法技巧。
单调栈的本质是维护一个具有单调性的栈结构(递增或递减),通过巧妙的元素入栈和出栈操作,在遍历数组时快速找到满足特定条件的元素。其核心优势在于:
关键理解:栈的单调性不是预先设定的,而是由问题需求和比较逻辑自然形成的。这是很多初学者容易混淆的点。
这是最经典的单调栈应用场景。我们需要为数组中每个元素找到它右边第一个比它大的元素。
cpp复制vector<int> nextGreater(vector<int>& nums) {
int n = nums.size();
vector<int> res(n, -1); // 默认-1表示没有更大的元素
stack<int> st; // 存储元素索引
for (int i = 0; i < n; i++) {
// 当前元素比栈顶大时,说明找到了栈顶元素的"下一个更大元素"
while (!st.empty() && nums[i] > nums[st.top()]) {
res[st.top()] = nums[i]; // 存储实际值
// 也可以存储索引差:i - st.top()
st.pop();
}
st.push(i);
}
return res;
}
工作原理分析:
时间复杂度:每个元素最多入栈和出栈一次,因此是O(n)
与寻找更大元素对称,只需改变比较方向:
cpp复制vector<int> nextSmaller(vector<int>& nums) {
int n = nums.size();
vector<int> res(n, -1);
stack<int> st; // 单调递增栈
for (int i = 0; i < n; i++) {
while (!st.empty() && nums[i] < nums[st.top()]) {
res[st.top()] = nums[i];
st.pop();
}
st.push(i);
}
return res;
}
关键区别:
>变为<通过改变遍历方向,可以寻找左侧的更大元素:
cpp复制vector<int> prevGreater(vector<int>& nums) {
int n = nums.size();
vector<int> res(n, -1);
stack<int> st; // 仍然保持递减栈特性
// 关键变化:从右向左遍历
for (int i = n - 1; i >= 0; i--) {
while (!st.empty() && nums[i] > nums[st.top()]) {
res[st.top()] = nums[i];
st.pop();
}
st.push(i);
}
return res;
}
逆向遍历的奥秘:
同样通过改变遍历方向和比较符号实现:
cpp复制vector<int> prevSmaller(vector<int>& nums) {
int n = nums.size();
vector<int> res(n, -1);
stack<int> st; // 单调递增栈
for (int i = n - 1; i >= 0; i--) {
while (!st.empty() && nums[i] < nums[st.top()]) {
res[st.top()] = nums[i];
st.pop();
}
st.push(i);
}
return res;
}
通过上述四种模式,我们可以提炼出一个通用思维框架:
nums[i] > nums[st.top()]nums[i] < nums[st.top()]通用代码模板:
cpp复制vector<int> monotonicStackTemplate(vector<int>& nums, bool findGreater, bool findPrevious) {
int n = nums.size();
vector<int> res(n, -1);
stack<int> st;
// 确定遍历方向
int start = findPrevious ? n - 1 : 0;
int end = findPrevious ? -1 : n;
int step = findPrevious ? -1 : 1;
for (int i = start; i != end; i += step) {
// 确定比较条件
bool condition = findGreater
? (nums[i] > nums[st.top()])
: (nums[i] < nums[st.top()]);
while (!st.empty() && condition) {
res[st.top()] = nums[i]; // 或索引i
st.pop();
// 需要更新condition以防栈变化
if (!st.empty()) {
condition = findGreater
? (nums[i] > nums[st.top()])
: (nums[i] < nums[st.top()]);
}
}
st.push(i);
}
return res;
}
问题描述:给定每日温度列表,返回一个列表表示需要等待多少天才能遇到更暖和的温度。
cpp复制class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> ans(n, 0);
stack<int> st;
for (int i = 0; i < n; i++) {
// 当前温度比栈顶高时,计算天数差
while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
ans[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
return ans;
}
};
关键点:
时间复杂度分析:O(n),每个索引最多入栈和出栈一次
问题描述:在柱状图中找出能勾勒出的最大矩形面积。
cpp复制class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
stack<int> st;
vector<int> left(n, -1); // 左边第一个比当前小的索引
vector<int> right(n, n); // 右边第一个比当前小的索引
// 找右边第一个更小的元素
for (int i = 0; i < n; i++) {
while (!st.empty() && heights[i] < heights[st.top()]) {
right[st.top()] = i;
st.pop();
}
st.push(i);
}
st = stack<int>(); // 清空栈
// 找左边第一个更小的元素
for (int i = n - 1; i >= 0; i--) {
while (!st.empty() && heights[i] < heights[st.top()]) {
left[st.top()] = i;
st.pop();
}
st.push(i);
}
// 计算最大面积
int maxArea = 0;
for (int i = 0; i < n; i++) {
maxArea = max(maxArea, heights[i] * (right[i] - left[i] - 1));
}
return maxArea;
}
};
算法思路:
优化技巧:
问题描述:计算柱状图能接多少雨水。
cpp复制class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
if (n == 0) return 0;
vector<int> leftMax(n), rightMax(n);
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = max(leftMax[i-1], height[i]);
}
rightMax[n-1] = height[n-1];
for (int i = n-2; i >= 0; i--) {
rightMax[i] = max(rightMax[i+1], height[i]);
}
int ans = 0;
for (int i = 0; i < n; i++) {
ans += min(leftMax[i], rightMax[i]) - height[i];
}
return ans;
}
};
cpp复制class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
int ans = 0;
stack<int> st;
for (int right = 0; right < n; right++) {
while (!st.empty() && height[right] > height[st.top()]) {
int bottom = st.top();
st.pop();
if (st.empty()) break;
int left = st.top();
int h = min(height[left], height[right]) - height[bottom];
int w = right - left - 1;
ans += h * w;
}
st.push(right);
}
return ans;
}
};
单调栈解法解析:
单调栈问题中常见的边界问题包括:
防御性编程建议:
cpp复制while (!st.empty() && nums[i] > nums[st.top()]) {
// 先检查栈是否为空再访问top()
}
根据问题需求,栈中可以存储:
选择建议:
单调栈不仅可以解决"下一个更大/小元素"问题,还可以应用于:
常见错误:
调试技巧:
某些情况下可以通过复用数组或更巧妙的设计减少空间使用:
单调栈本质上是顺序处理的,难以并行化。但在某些特殊情况下:
虽然单调栈是解决这类问题的最佳方案,但了解替代方法有助于深入理解:
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力法 | O(n²) | O(1) | 小规模数据 |
| 单调栈 | O(n) | O(n) | 通用场景 |
| 动态规划 | O(n) | O(n) | 特定问题如接雨水 |
| 双指针 | O(n) | O(1) | 特殊约束条件 |
单调栈在以下场景中有实际应用:
掌握单调栈不仅有助于算法面试,更能提升解决实际工程问题的能力。建议通过LeetCode和其他编程平台的类似题目进行大量练习,直到能够不假思索地写出正确的单调栈实现。