单调栈
作用:查找每个数的左侧第一个比它小或者比它大的数。
单调递减栈:查找数的左侧第一个比它大的数
单调递增栈:查找数的右侧第一个比它小的数
84.柱状图中最大矩形 hard
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
![]()
分析:
枚举每个矩形,作为它的上边界。找出左边离它最近的比它小的柱形,找出右边离它最近的比它小的柱形,作为它的左右边界。
暴力解法为,对于每个矩形都向左向右遍历一遍,此时时间复杂度为O(n^2)
此时,可以用可以用单调递增栈进行优化,栈内的元素单调递增,当遇到第i个元素,小于栈顶元素时,此时栈顶元素的左边界就是栈内的第二个元素,栈顶元素的右边界就是第i个元素,计算以栈顶元素作为上边界的面积即可。
代码:
int LargestRectangleArea(vector<int>& heights)
{
int res = 0;
stack<int> s;
heights.push_back(0); //加入右边界
for(int i = 0; i < height.size(); i++)
{
if(s.empty() || height[i] >= height[s.top()])
s.push(i);
else
{
int h = height[s.top()]; s.pop();
// int area = h * (i - s.top() - 1); 不要忘记考虑栈为空的情况
int area = h * (s.empty() ? i : i - s.top() - 1);
res = max(res, area);
i--;
}
}
}
42.接雨水 hard
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。
分析:此题采用的是单调递减栈,寻找该数左侧第一个大于它的数。然后计算所能接到的雨水数。
代码:
int trap(vector<int>& height)
{
if(height.size() < 2) return 0;
int area = 0, hs = height.size();
stack<int> q;
for(int i = 0; i < hs; i++)
{
if(q.empty() || height[q.top] > height[i]) q.push(i);
else
{
int t = 0;
while(!q.empty() && height[q.top()] <= height[i])
{
area += (height[q.top()] - t) * (i - q.top() - 1);
}
if(!q.empty()) area = (height[i] - t) * (i - q.top() - 1);
q.push(i);
}
}
return area;
}
单调队列
单调队列我见过的使用最经典的就是滑动窗口最大值的问题.
239.滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
分析:
暴力解法:遍历数组中的所有值,查找其对应滑动窗口中的最大值。时间复杂度是O(N*K)。
进阶:如果希望采用线性时间复杂度,则使用单调队列记录滑动窗口当前的最大值。
维护一个单调递减队列,如果前一个数小于后一个数,则前一个数不可能是该滑动窗口的最大值,可直接删掉。
deque<int> q;
//目标:维护一个单调递减队列,其中队列一直是一个滑动窗口的长度
for(int i = 0; i < nums.size(); i++)
{
//控制滑动窗口的大小
if(q.size() && i - k + 1 > q.front()) q.pop_front();
//维护这个单调递减队列
while(q.size() && nums[q.back()] < nums[i]) q.pop_back();
q.push_back(i);
//记录滑动窗口的最大值
if(i >= k - 1) res.push_back(nums[q.front()]);
}
总结:
思路都很巧妙,但是我觉得还挺难的。面试中似乎很难考到(我是没有过)
主要有一些容易忘记的点:
1. 存储的是序号,目的是为了容易计算出横坐标。
2. 注意边界情况,比如栈,队列为空,或者遍历到了最后的情况。