1 单调栈
单调栈和单调队列作为线性结构,通过保持一定的序列性,从而能很好地适应寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置。
单调栈与单调队列的本质都是空间换时间,通过保存可能用到的所有极值,省去了过多的重复遍历。
对于单调栈而言,必须想清楚单调顺序,即栈底->栈顶是递增还是递减;想清楚处理逻辑,即当前遍历元素与栈顶元素大小比较时应该进行的操作。
2 每日温度
LeetCode:每日温度
使用单调栈有两种方法,其中一种更为通用,我将列在方法一。
从开始向后遍历,维持一个递减的单调栈,一旦出现递增元素,则将所有满足递增条件的元素弹出,并且进行赋值处理。
方法二则更为粗陋一些,从后向前遍历,用栈维持从后向前的单调递减的温度的索引。
方法一
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
vector<int> result(temperatures.size(),0);
stack<int> stk;
stk.push(0);
for(int i=1;i<temperatures.size();++i)
{
if(temperatures[stk.top()]>=temperatures[i])
{
stk.push(i);
}
else
{
while(!stk.empty() && temperatures[stk.top()]<temperatures[i])
{
result[stk.top()]=i-stk.top();
stk.pop();
}
stk.push(i);
}
}
return result;
}
};
方法一[精简版]
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
vector<int> result(temperatures.size(),0);
stack<int> stk;
stk.push(0);
for(int i=1;i<temperatures.size();++i)
{
while(!stk.empty() && temperatures[stk.top()]<temperatures[i])
{
result[stk.top()]=i-stk.top();
stk.pop();
}
stk.push(i);
}
return result;
}
};
方法二
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
vector<int> result(temperatures.size(),0);
stack<int> stk;
stk.push(temperatures.size()-1);
for(int i=temperatures.size()-2;i>=0;--i)
{
if(temperatures[i]<temperatures[stk.top()])
{
result[i]=1;
stk.push(i);
}
else
{
while((!stk.empty()) && temperatures[stk.top()]<=temperatures[i])
{
stk.pop();
}
if(stk.empty())
result[i]=0;
else
result[i]=stk.top()-i;
stk.push(i);
}
}
return result;
}
};
3 下一个更大元素I
LeetCode:下一个更大元素I
利用哈希+单调栈解决。这里同样给出详细版与精简版。
详细版
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
//哈希+单调栈
//哈希初始化
unordered_map<int,int> umap;
for(int i=0;i<nums1.size();++i)
{
umap[nums1[i]]=-1;
}
//单调栈
stack<int> stk;
stk.push(0);
for(int i=1;i<nums2.size();++i)
{
//nums2[i]不是栈顶元素的下一个更大元素,入栈+跳过
if(nums2[i]<=nums2[stk.top()])
{
stk.push(i);
}
//nums2[i]是栈顶元素的下一个更大元素,将所有符合条件的元素进行出栈
else
{
while(!stk.empty() && nums2[i]>nums2[stk.top()])
{
int top=stk.top();
//在nums1中也有栈顶元素
if(umap.find(nums2[top])!=umap.end())
{
umap[nums2[top]]=nums2[i];
}
stk.pop();
}
stk.push(i);
}
}
//结果转化
vector<int> result;
for(int i=0;i<nums1.size();++i)
result.push_back(umap[nums1[i]]);
return result;
}
};
精简版
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
//哈希+单调栈
//哈希初始化
unordered_map<int,int> umap;
for(int i=0;i<nums1.size();++i)
{
umap[nums1[i]]=-1;
}
//单调栈
stack<int> stk;
stk.push(0);
for(int i=1;i<nums2.size();++i)
{
//nums2[i]是栈顶元素的下一个更大元素,将所有符合条件的元素进行出栈
while(!stk.empty() && nums2[i]>nums2[stk.top()])
{
int top=stk.top();
//在nums1中也有栈顶元素
if(umap.find(nums2[top])!=umap.end())
{
umap[nums2[top]]=nums2[i];
}
stk.pop();
}
//无论有没有进行出栈,i必须入栈
stk.push(i);
}
//结果转化
vector<int> result;
for(int i=0;i<nums1.size();++i)
result.push_back(umap[nums1[i]]);
return result;
}
};
4 下一个更大元素II
LeetCode:下一个更大元素II
跑两遍几可以了,自然会保证循环。
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
//跑两遍以保证覆盖
vector<int> result(nums.size(),-1);
stack<int> stk;
stk.push(0);
for(int i=1;i<2*nums.size()-1;++i)
{
int index=i%(nums.size());
while(!stk.empty() && nums[stk.top()]<nums[index])
{
result[stk.top()]=nums[index];
stk.pop();
}
stk.push(index);
}
return result;
}
};
5 接雨水
LeetCode:接雨水
超级超级经典的题目,能用双指针+动态规划解决,也能用单调栈解决。
- 单调栈的思想是横着装满雨水,维持一个栈底到栈顶降序的单调栈,一旦出现递增元素,说明栈顶 < 遍历元素 && 栈顶 < 栈顶之下的元素,即栈顶为洼点。计算由两边与洼点所组成的谷地的面积。
- 双指针+动态规划的思想是竖着装满雨水,每一个列的雨水量,其宽度必然为1,其高度由【自身高度】与【左边最高值 和 右边最高值 中的最小值】的差决定,因此使用动态规划,事先记录每个列的【左最高值】和【右最高值】。
单调栈
class Solution {
public:
int trap(vector<int>& height) {
if(height.size()<=2)return 0;
//单调栈,从栈底->栈顶为降序
//一旦比栈顶大,说明top<i且bottom>top,top为一个洼点,可以承接雨水
stack<int> stk;
stk.push(0);
int count=0;
for(int i=1;i<height.size();++i)
{
//横着装水
while(!stk.empty() && height[stk.top()]<=height[i])
{
//洼点
int mid=stk.top();
stk.pop();
if(!stk.empty())
{
//左侧柱
int left=stk.top();
int h=min(height[left],height[i])-height[mid];
//此处使用i和left计算宽度
int w=i-left-1;
count+=h*w;
}
}
stk.push(i);
}
return count;
}
};
双指针+动态规划
class Solution {
public:
int trap(vector<int>& height) {
if(height.size()<=2)return 0;
//双指针,每一列的雨水量=max ( min(LeftHeight,RightHeight) - Height )
vector<int> leftHeight(height.size(),height[0]);
vector<int> rightHeight(height.size(),height.back());
//每一列i的左侧最高值
for(int i=1;i<height.size();++i)
leftHeight[i] = max(leftHeight[i-1],height[i]);
//右侧最高值
for(int i=height.size()-2;i>=0;--i)
rightHeight[i] = max(rightHeight[i+1],height[i]);
//求雨水量
int rain_count=0;
//显然头尾不会有雨水
for(int i=1;i<height.size()-1;i++)
{
int diff_h=min(leftHeight[i],rightHeight[i])-height[i];
rain_count+=max(0,diff_h);
}
return rain_count;
}
};
6 柱状图中最大的矩形
LeetCode:柱状图中最大的矩形
与接雨水没有太多的区别,比较关键地在于需要记录的是索引而不是值。
- 在单调栈的使用中,必须明确遇到递减元素时,去计算从栈顶(此时遍历元素尚未入栈)到(大于遍历元素)之间[ 所有元素 ] 到达 [ 遍历元素 ]位置所形成的矩形。
为了保证最后一个元素向前构造矩形和一定能找到左边界,分别要在末尾和头部添加0,扩充数组。 - 当然使用双指针+动态规划会更清晰一些,但事实上也隐晦地蕴含了对于头尾的处理,并且类似于KMP算法,使用了启发式搜索,跳过了大量无意义的遍历。
单调栈
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
//单调栈,维护一个底->顶的升序单调栈
//heights头尾都加入0
heights.insert(heights.begin(),0);
heights.push_back(0);
//进行单调栈维护
int size=heights.size();
stack<int> stk;
stk.push(0);
int max_area=0;
for(int i=1;i<size;++i)
{
if(heights[i]>heights[stk.top()])
stk.push(i);
else if(heights[i]==heights[stk.top()])
{
stk.push(i);
}
else
{
while(!stk.empty() && heights[i]<heights[stk.top()])
{
int mid=stk.top();
stk.pop();
if(!stk.empty())
{
int left=stk.top();
int right=i;
int h=heights[mid];
int w=right-left-1;
max_area=max(max_area,h*w);
}
}
stk.push(i);
}
}
return max_area;
}
};
双指针+动态规划
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
//双指针,记录自己【左边第一个比自己小的】和【右边第一个比自己小的】
int size=heights.size();
vector<int> leftIndex(size);
vector<int> rightIndex(size);
//初始化左边第一个比自己小的坐标
leftIndex[0]=-1;
for(int i=1;i<size;++i)
{
int t=i-1;
//height[t] >= heights[i],说明t不满足条件,需要向前寻找
//必然地,在leftIndex[t]+1 ~ t之间的数index,由于必然height[index]>=height[t]>=heights[i]
//所以必须从leftIndex[t]开始向前寻找,以此类推
while(t>=0 && heights[t]>=heights[i])
t=leftIndex[t];
leftIndex[i]=t;
}
//同理处理右侧
rightIndex[size-1]=size;
for(int i=size-2;i>=0;--i)
{
int t=i+1;
while(t<size && heights[t]>=heights[i])
t=rightIndex[t];
rightIndex[i]=t;
}
int max_area=0;
//开始处理数组
for(int i=0;i<size;++i)
{
int length = rightIndex[i] - leftIndex[i] - 1;
max_area = max(max_area, length*heights[i]);
}
//返回结果
return max_area;
}
};
7 总结
Leetcode基础部分就到此为止了,可以说一句完结撒花了。
整整23篇,至少也是20天的努力,写Markdown对我而言既是一种负担,也是一种督促,但事实上,决不能沉溺于Leetcode的舒适圈,明天开始要好好地恶补计算机基础知识了。
之后这个系列估计只会偶尔更新了,可能明天会写一点背包问题的内容,主要是记录昨天看背包九讲的收获吧,引用一句其中很喜欢的一句话:失败从不是什么丢人的事,从失败中全无收获才是,前者是pratice,而后者才是failure。
——2023.3.7