1. 单调队列与单调栈概述
单调队列和单调栈是两种基于线性数据结构的高级优化技术,广泛应用于算法竞赛和工程实践中。它们通过维护数据的单调性,能够在O(1)或O(n)时间复杂度内解决特定类型的区间极值问题。
单调队列(Monotonic Queue)主要用于维护滑动窗口内的极值,典型应用包括:
- 滑动窗口最大值/最小值
- 多重背包问题优化
- 动态规划状态转移优化
单调栈(Monotonic Stack)则擅长处理前驱/后继关系问题,常见场景有:
- 寻找下一个更大/更小元素
- 柱状图最大矩形面积
- 接雨水问题
这两种数据结构之所以高效,是因为它们通过主动淘汰无效数据,将原本O(n²)的暴力算法优化到O(n)级别。理解它们的运作机制对提升算法能力至关重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 单调队列深度解析
2.1 基本实现原理
单调队列通常基于双端队列(deque)实现,核心是维护队列元素的单调性。以维护递减队列为例:
cpp复制deque<int> q; // 存储的是元素下标而非值本身
// 维护递减队列的插入操作
void push(int val, int index) {
while (!q.empty() && arr[q.back()] <= val) {
q.pop_back(); // 淘汰比当前元素小的值
}
q.push_back(index);
}
关键特性:
- 队列中的元素保持严格的单调递减/递增
- 每个元素最多入队和出队各一次
- 时间复杂度均摊O(1)
2.2 滑动窗口经典应用
以LeetCode 239题为例,求滑动窗口最大值:
cpp复制vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> q;
vector<int> res;
for(int i=0; i<nums.size(); ++i) {
// 移除超出窗口范围的元素
while(!q.empty() && q.front() <= i-k) q.pop_front();
// 维护单调递减性
while(!q.empty() && nums[q.back()] < nums[i]) q.pop_back();
q.push_back(i);
if(i >= k-1) res.push_back(nums[q.front()]);
}
return res;
}
关键点:队列存储的是下标而非值,便于判断元素是否在窗口内
2.3 多重背包优化
单调队列在动态规划中的经典应用是多重背包问题优化。传统多重背包时间复杂度为O(NVK),使用单调队列可优化到O(N*V):
cpp复制for(int i=1; i<=n; ++i) {
for(int j=0; j<w[i]; ++j) {
deque<int> q;
for(int k=j; k<=W; k+=w[i]) {
// 维护单调队列
while(!q.empty() && q.front() < k - c[i]*w[i]) q.pop_front();
while(!q.empty() && dp[i-1][q.back()] + (k-q.back())/w[i]*v[i]
<= dp[i-1][k]) q.pop_back();
q.push_back(k);
dp[i][k] = dp[i-1][q.front()] + (k-q.front())/w[i]*v[i];
}
}
}
3. 单调栈技术详解
3.1 基本操作与实现
单调栈维护栈内元素的单调性,以递增栈为例:
cpp复制stack<int> st;
void push(int val) {
while(!st.empty() && st.top() >= val) {
st.pop(); // 破坏单调性的元素出栈
}
st.push(val);
}
典型应用场景:
- 寻找下一个更大元素(Next Greater Element)
- 计算柱状图中最大矩形
- 接雨水问题
3.2 柱状图最大矩形
LeetCode 84题解法:
cpp复制int largestRectangleArea(vector<int>& heights) {
stack<int> st;
heights.push_back(0); // 哨兵
int maxArea = 0;
for(int i=0; i<heights.size(); ++i) {
while(!st.empty() && heights[st.top()] > heights[i]) {
int h = heights[st.top()]; st.pop();
int w = st.empty() ? i : i - st.top() - 1;
maxArea = max(maxArea, h * w);
}
st.push(i);
}
return maxArea;
}
3.3 接雨水问题
LeetCode 42题单调栈解法:
cpp复制int trap(vector<int>& height) {
stack<int> st;
int res = 0;
for(int i=0; i<height.size(); ++i) {
while(!st.empty() && height[st.top()] < height[i]) {
int base = height[st.top()]; st.pop();
if(st.empty()) break;
int h = min(height[st.top()], height[i]) - base;
int w = i - st.top() - 1;
res += h * w;
}
st.push(i);
}
return res;
}
4. 实战技巧与常见问题
4.1 边界处理技巧
- 哨兵技巧:在数组首尾添加极值(如INT_MIN/INT_MAX)简化边界判断
- 下标存储:通常存储元素下标而非值本身,便于计算宽度和判断位置
- 空栈处理:出栈后检查栈是否为空,避免非法访问
4.2 常见错误排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 结果偏小 | 单调性维护方向错误 | 检查比较符号(>或<)是否与需求一致 |
| 数组 |
