单调栈
单调栈的建立:
单调栈在原先栈先进后出的特性上面在加一个 从栈顶到栈底是严格递增或递减 \textcolor{red}{从栈顶到栈底是严格递增或递减} 从栈顶到栈底是严格递增或递减的特性。
当我们要插入一个元素时
我们假设此时元素是从栈顶到栈底严格递增的,所以 对栈顶元素 v 和 e 进行判断如果 e < = v ,将 a 弹出,直到 e > v \textcolor{red}{对栈顶元素v和e进行判断如果e<=v,将a弹出,直到e>v} 对栈顶元素v和e进行判断如果e<=v,将a弹出,直到e>v
stack<int> stk;
vector<int> l(n);
for(int i=0;i<n;i++){
while(stk.size()){
//当栈顶元素比e大时弹出栈顶元素
if(e<=stk.top()){
stk.pop();
}
}
l[idx]=stk.empty()? -1 : stk.top();
stk.push(e);
}
这样可以就保证满足元素是从栈顶到栈底严格递增的
同理对于严格递减的处理也可类推出来
单调栈的作用:
用于高效的求出某一个下标index左右两侧比该元素大,或者小的位置
可理解为在该位置形成了一个低谷或者高峰,然后高效求出该低谷或者高峰的 管辖范围 ( l , r ) \textcolor{red} {管辖范围(l,r)} 管辖范围(l,r)
如图所示,在位置为idx的地方中形成了一个管辖范围(-1,6)的一个低谷,该低谷的长度为6-(-1)-1=6
单调栈的用途:
一般典型的有如下几个问题:
1.求柱状图的最大矩形面积
思路
该题让我们求出若干个矩形堆砌而成的图形当中所能构成的最大矩形面积是多少
首先应该想明白如何才能形成一个矩形(因为上图参差不及,高度不一),所以我们可以采取 固定最低高度 \textcolor{red}{固定最低高度} 固定最低高度的思路来求解
那要使当前高度为最小值,我们需要找出它的
管辖区间
(
l
,
r
)
\textcolor{red}{管辖区间(l,r)}
管辖区间(l,r),进而我们得到它的管辖长度r-l-1,这时我们求出以当前节点作为最小值形成的最大矩形面积为:
(
r
−
l
−
1
)
∗
h
[
i
d
x
]
(r-l-1)*h[idx]
(r−l−1)∗h[idx]
所以只需要遍历每个下标就可以求出整个数组的最大矩形
代码
class Soultion{
public:
int largetstArea(vector<int>& height){
int n=height.size();
vector<int> l(n);//定义左管辖范围
vector<int> r(n);//定义右管辖范围
stack<int> stk;
//因为要形成一个”低谷“,我们在处理左管辖区间时,需要定义一个栈顶到栈底单调递减的单调栈
//在处理右管辖区间时,要定义一个栈顶到栈底单调递减的单调栈
//处理左管辖区间,从左到右
for(int i=0;i<n;i++){
while(stk.size()&&height[i]<=height[stk.top()]){
stk.pop();
}
l[i]=stk.empty()? -1 : stk.top();//如果栈为空,说明此时已经到达最左端,取-1
stk.push(i);
}
stk=stack<int>();//将栈置空
//处理右管辖区间,从右到左
for(int i=n-1;i>=0;i--){
while(stk.size()&&height[i]<=height[stk.top()]){
stk.pop();
}
r[i]=stk.empty()? n :stk.top();//如果栈为空,说明此时已经到达最右端,取n
stk.push(i);
}
int res=0;
//开始求每个下标所对应的最大面积
for(int i=0;i<n;i++){
res=max(res,(r[i]-l[i]-1)*height[i]);
}
return res;
}
}
细节
要注意从栈顶到栈底的增减性问题
如果要确保当前元素可以形成一个低谷,则要让idx小于栈顶元素
2.接雨水问题
思路
该题要我们求出一个高度不一的矩形块最大能容纳的雨水是多少
设当前元素为idx时,我们需要找到两个量,一是当前栈顶元素,二是栈顶元素的下一处
如果栈顶元素比idx小,我们可以认为此时可能存水,记录当前栈顶元素cur
然后弹出栈顶元素
若此时栈不为空,加之我们构造了一个从栈顶到栈底单调递增的栈
然后我们取出当前栈顶元素,并记为left
所以现在就可以确定,(left,idx)之间可以容纳雨水
可容纳雨水的量为:
(
m
i
n
(
h
[
l
e
f
t
]
,
h
[
i
d
x
]
)
−
h
[
c
u
r
]
)
∗
(
i
d
x
−
l
e
f
t
−
1
)
;
(min(h[left],h[idx])-h[cur])*(idx-left-1);
(min(h[left],h[idx])−h[cur])∗(idx−left−1);
最终答案就是对上述算式求累加和
代码
class Soultion{
public:
int trap(vector<int>& height) {
stack<int> stk;
int ans=0;
for(int i=0;i<n;i++){
while(stk.size()&&height[i]>=height[stk.top()]){
//此时的栈顶
int cur=stk.top();
stk.pop();
if(stk.empty()){
break;
}
int left=stk.top();
//求出可容纳水的高度
int sum=min(height[i],height[left])-height[cur];
ans+=sum*(i-left-1);
}
stk.push(i);
}
return ans;
}
}
细节
1.注意在当前栈内有两个元素时才可以确保可以接到雨水
2.此题内在隐含逻辑是要确保当前元素为最低,也就是高峰,所以需要当前元素大于等于栈顶元素
3.前序遍历构造二叉搜索树
引入
清楚了单调栈的性质之后,在一次做二叉搜索数的题时想到,可以直接根据它的中序遍历结果,来得到每个节点的根节点的位置
从而确定整个树
题意
给一个二叉搜索树先序遍历,构造一棵树
思路
由二叉搜索树的性质:左<根<右
由前序遍历的结果:根->左->右
所以利用单调栈我们可以求出左子树的范围(将根节点当作最大值),进而求出整棵树
代码
class Solution {
public:
TreeNode* build(vector<int>& preorder,vector<int>& r,int cur,int right){
if(right<=cur){
return nullptr;
}
TreeNode* root=new TreeNode(preorder[cur]);
root->left=build(preorder,r,cur+1,r[cur]);
root->right=build(preorder,r,r[cur],right);
return root;
}
TreeNode* bstFromPreorder(vector<int>& preorder) {
stack<int> stk;
int n=preorder.size();
vector<int> r(n);
for(int i=n-1;i>=0;i--){
while(stk.size()&&preorder[i]>=preorder[stk.top()]){
stk.pop();
}
r[i]=stk.empty()? n:stk.top();
stk.push(i);
}
TreeNode* root=bulid(preorder,r,0,n);
return root;
}
};
细节
r[i]数组的值为右子树的起始点
4.移除k位数字
题意
给定一个非负整数num,和一个整数k,在不改变num中数字的顺序下,删除k位数字,求可以得到的最大值
思路
先将这个数字当作字符串来看,设长度为n,因为最终的长度是一样的,均为n-k,所以只需要得到最小字符串即可
对字符串进行比较时,我们用来进行判断的就是两个字符串中第一个不相同的元素,该元素决定了两个字符串的大小
所以为了得到一个最小的字符串,我们应该把小的尽可能放在前面,大的尽可能放在后面
所以我们可以维护一个 从栈顶到栈底单调递增的一个单调栈,当新加入的元素比栈顶小,就将栈顶元素弹出,每弹出一次,给 k 减 1 \textcolor{red}{从栈顶到栈底单调递增的一个单调栈,当新加入的元素比栈顶小,就将栈顶元素弹出,每弹出一次,给k减1} 从栈顶到栈底单调递增的一个单调栈,当新加入的元素比栈顶小,就将栈顶元素弹出,每弹出一次,给k减1
代码
class Solution {
public:
string removeKdigits(string num, int k) {
int n=num.size();
if(n==k){
return "0";
}
stack<int> stk;
int g=k;
for(int i=0;i<n;i++){
while(k>0&&stk.size()&&num[i]<num[stk.top()]){
stk.pop();
k--;
}
if(num[i]=='0'&&stk.empty()){
g++;
continue;
}else{
stk.push(i);
}
}
string r;
while(stk.size()){
r.push_back(num[stk.top()]);
stk.pop();
}
reverse(r.begin(),r.end());
string res;
int i=0;
for(i=0;i<n-g;i++){
res.push_back(r[i]);
}
if(res==""){
return "0";
}
return res;
}
};
细节
因为有前导0的存在,所以实际的移除位数为g
5.车队问题
题意
在一条单向行使的路上,有n辆车驶向同一个目的地target,position数组给出了每个车辆的初始位置,speed数组给出了每个车的速度
规定如果后车追上前车,则两辆车一起以相同速度行使
思路
我们假设有两辆车在路上行使,a车在后,b车在前
设他们最后到达目的地所花费的时间为Ta和Tb
则他们会行成一辆车的充分必要条件为Ta<=Tb
所以我们定义一个time数组
t
i
m
e
[
i
]
=
(
t
a
r
g
e
t
−
p
o
s
i
t
i
o
n
[
i
]
)
/
s
p
e
e
d
[
i
]
;
time[i]=(target-position[i])/speed[i];
time[i]=(target−position[i])/speed[i];
然后我们可以使用哈希表将position和time数组结合起来(注意题目中说了position数组的值是唯一的),此时对position数组排序,然后通过position数组和哈希表调用time数组
当我们通过position遍历time数组的时候
想一种特殊情况:只有两辆车,如果遇到前值小于后值,则表明后面的车可以追上前面的车
对于一般情况下,我们可以通过维护一个从栈顶到栈底单调递减的单调栈,当新加入的元素e(后值)大于等于栈顶元素时,我们弹出栈顶元素(相当于当前元素被吃掉),所以我们最终只需返回单调栈的个数即可
代码
class Solution {
public:
int carFleet(int target, vector<int>& position, vector<int>& speed) {
int n=position.size();
vector<double> time(n);
for(int i=0;i<n;i++){
time[i]=(target-position[i])*1.0/speed[i];
}
unordered_map<int,int> map;
for(int i=0;i<n;i++){
map[position[i]]=i;
}
sort(position.begin(),position.end());
stack<int> stk;
for(int i=0;i<n;i++){
int idx=map[position[i]];
while(stk.size()&&time[idx]>=time[stk.top()]){
stk.pop();
}
stk.push(idx);
}
return stk.size();
}
};