C++单调栈和单调队列
1.单调栈定义
-
定义:单调栈即栈中的元素是有序的
-
类别:
-
单调递增栈:单调递增栈就是从栈底到栈顶数据是从小到大
-
只有比栈顶元素大的元素才能直接进栈,否则需要先将栈中比当前元素大的元素出栈,再将当前元素入栈。
-
这样就保证了:栈中保留的都是比当前入栈元素大的值,并且从栈底到栈顶的元素值是单调递增的
-
-
单调递减栈:单调递减栈就是从栈底到栈顶数据是从大到小
-
只有比栈顶元素小的元素才能直接进栈,否则需要先将栈中比当前元素小的元素出栈,再将当前元素入栈。
-
这样就保证了:栈中保留的都是比当前入栈元素小的值,并且从栈底到栈顶的元素值是单调递减的
-
-
2.如何利用单调栈
-
单调栈何时用:为任意一个元素找左边和右边第一个比自己大/小的位置用单调栈,由于每个元素最多个自进出栈一次,复杂度是O(n)。
-
例题:
【力扣】队列中可以看到的人数
-
有 n 个人排成一个队列,从左到右编号为 0 到 n - 1 。给你以一个整数数组 heights ,每个整数 互不相同,heights[i] 表示第 i 个人的高度。 一个人能 看到 他右边另一个人的条件是这两人之间的所有人都比他们两人 矮 。更正式的,第 i 个人能看到第 j 个人的条件是 i < j 且 min(heights[i], heights[j]) > max(heights[i+1], heights[i+2], ..., heights[j-1]) 。 请你返回一个长度为 n 的数组answer,其中 answer[i] 是第 i 个人在他右侧队列中能看到的人数 。
-
做法一:暴力遍历
对每一个人从右往左进行遍历,再对每一个人右边的所有人对进行遍历找出满足条件的人数,然后输出。空间复杂度为O(n)
-
做法二:单调栈法
对于题目,我们可以对问题进行简化。当我们每从右往左遍历一个数(从右往左遍历是为了在遍历每一个元素时,可以记录到元素右边的情况),我们要求的是当前所遍历的数a[i],从右边第一个数开始挨个递增的元素的总个数(即求以a[i+1]为首项向右延展的最大递增数列的元素个数)
因此,我们需要的是每个元素a[i]右边以a[i+1]为首项的一个最大递增序列。分析到此,我们便可以考虑用单调栈来获取每个元素的最大递增序列,即使用单调递增栈。
-
代码如下:
vector<int> canSeePersonsCount(vector<int>& heights) { int n = heights.size(); vector<int> ans(n); stack<int> st;//单调递增 for (int i = n - 1; i >= 0; i--) { while(!st.empty() && heights[i] > st.top()) { ans[i]++; st.pop(); } if (!st.empty()) ans[i]++;//第一个大于h[i]的 st.push(heights[i]); } return ans; }
-
3.单调队列定义
-
定义:
单调队列 是 队列中元素之间的关系具有单调性的一中队列。
-
类别:
-
单调递增队列:队列中的元素从队头到队尾单调递增(队头为数组的第一个元素,查看队头用q.front())
-
单调递减队列:队列中的元素从队头到队尾单调递减
-
-
构建单调队列:
给定一个数组如7,6,8,12,9,10,3,接着构建一个单调递减的队列
根据单调递减队列的规则,即大的元素在队头,即可以得到以下结果
【12,10,3】
4.如何利用单调队列
-
例题:滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值。
-
思考:
-
我们先对题目进行分析,题目要求的是每个滑动区间的最大值,首先一个最为简单的思路就是暴力遍历,对每个区间进行遍历求出最大值,再滑动到下一个区间求最大值。这种方法的时间复杂度为O(nk)。
-
因此我们可以尝试对该方法进行优化。我们不难发现,在遍历每个区间的时候,时常有一些数据我们已经遍历过了,或者说有一些数据前面的一个区间判断过了,在下一个区间不可能会成为最大值;像这样的数据我们就可以不进行遍历直接跳过,减少时间消耗。
-
有了这样的一个优化思路,我们来看看如何跳过这些数据的重复遍历。对于区间
[1 3 4 5 -1]
我们挨个遍历数据,先读入1,然而对于下一个区间来说1不可能重复遍历,所以可以不维护1,接着往下遍历,当读取到5时,我们发现5大于3,也大于4,对于下一个区间来说3,4
就不可能成为最大的值,所以可以减少对这类情况的遍历。对于-1来说,虽然它比5小,但由于-1在5的前面,在滑动窗口的时候,-1有可能成为下一个区间的最大值,所以可以将-1读入容器。 -
我们从个别例子找到优化的思路,便要从普遍例子找到优化的规律。即我们可以先遍历第一个区间的数值,将可能成为下一个区间的最大值读入到容器中;接着就是滑动窗口,每遍历下一个元素,就将容器中不在区间内的数值弹出容器,并且对遍历进来的元素进行判断,判断其预进容器的元素是否比容器的最小值还要大,如果大的话,将最小值弹出再继续进行判断,直到容器为空或者预处理的元素比最小值小,停止判断并将预处理元素读入容器。
-
因此我们在每遍历一个元素,都可以得到一个严格单调的区间,区间的第一个元素便是每个区间的最大值。考虑到该过程要对容器进行队头或者队尾元素的删除,所以用双端队列deque
-
-
以上的优化思路为单调队列,该方法的空间复杂度O(n),求滑动区间的最值问题就可以用单调队列
-
代码如下:
class Solution { public: vector<int> maxSlidingWindow(vector<int>& nums, int k) { int n = nums.size(); deque<int> q; //使用第一个窗口去初始化单调队列 for (int i = 0; i < k; ++i) { while (!q.empty() && nums[i] >= nums[q.back()]) { q.pop_back(); } q.push_back(i);//队列存储的是下标 } //ans存储每个窗口的最大值 vector<int> ans = {nums[q.front()]}; for (int i = k; i < n; ++i) { while (!q.empty() && nums[i] >= nums[q.back()]) { q.pop_back(); } q.push_back(i); //检查最大元素是否在窗口区间里 if (q.front() <= i - k) { q.pop_front(); } ans.push_back(nums[q.front()]); } return ans; } };
刷题
-
法一:暴力循环
思路:先对nums1进行遍历,获取nums1中的每一个元素,接着对nums2进行遍历,直到找到和nums1所获取到的元素相等的元素,接着从该元素开始接着遍历,直到找到比nums1所获取的元素大的元素并返回该元素,否则返回-1.该方法的时间复杂度为O(n1*n2).
代码如下:
class Solution { public: vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) { int n1=nums1.size(); int n2=nums2.size(); vector<int> ans(n1); for(int i =0;i<n1;i++) { int j =0,k=0; while(nums1[i]!=nums2[j]) j++; k=j+1; while(k<n2&&nums1[i]>nums2[k]) k++; ans[i]=k<n2?nums2[k]:-1; } return ans; };
-
法二:单调栈法
-
思路:由于暴力遍历要多次对nums2 进行循环遍历,导致时间消耗太多,因此我们考虑更优的解法。重新分析一下题目:即求nums2每个元素右边第一个比它大的元素,有则返回该数,无则返回-1.
-
因为每遍历nums2中每一个元素都要考虑其右边所有元素,所有我们可以直接从右往左遍历,接着可以用一个栈对每个元素右边的所有元素进行维护。将当前所遍历的元素和栈顶元素进行比较,若当前元素大于栈顶元素,将栈顶元素弹出,将当前元素压入栈内。重复以上的判断和操作,当栈为空或者当前元素比栈顶元素小,返回栈顶元素并将当前元素压进栈。
-
这样我们一次遍厉nums2便找到了所有元素右边的第一个更大值,接着再对nums1进行遍历找到nums2中对应的值返回对应的第一个更大值。
-
该方法的时间复杂度为O(n1+n2)
-
代码如下:
class Solution { public: vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) { int n1=nums1.size(); int n2=nums2.size(); vector<int> ans(n1);//声明答案数组 int list[10010];//初始化一个数组用于记录nums2每个元素对应的下一个最大值 stack<int> s;//构建一个单调递减栈,储存nums2中每个元素下一个可能更大值的单调递减序列 for(int i =n2-1;i>=0;i--) { while(!s.empty()&&nums2[i]>s.top())//栈顶元素为栈内最小值,若当前元素大于栈顶元素,栈顶元素出栈,继续判断 s.pop(); if(s.empty()) list[nums2[i]]=-1;//栈为空,说明栈内元素没有比当前元素大,即没有下一个更大值,对当前元素进行标记-1 else list[nums2[i]]=s.top();//栈不为空,说明有下一个更大值,对当前元素进行标记。(因为题目nums2中说明没有重复值,所以该标记法可取) s.push(nums2[i]); } for(int i = 0;i<n1;i++) { ans[i]=list[nums1[i]]; } return ans; } };
-
给定一个循环数组
nums
( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。
-
分析:题目意思,是对循环序列每个元素进行判断,寻找每个元素右边第一个更大的数,若有返回该数;若无返回-1。我们不难发现在循环序列中,只需循环遍历二遍,就能每次都将nums的所有元素都遍历一遍。所有我们可以直接将nums【1,2,1】变为nums【1,2,1,1,2,1】
-
法一:暴力遍历
每遍历nums中的一个元素,就往nums尾部填入该元素,接着从当前所遍历的元素的下一个元素开始遍历,判断是否出现第一个更大的数;一遍一遍地对每个元素进行上述操作,直到遍历到nums最后一个元素。该方法的时间复杂度为O(n^2)
代码如下:
class Solution { public: vector<int> nextGreaterElements(vector<int>& nums) { int n=nums.size(); vector<int> ans(n); for(int i =0;i<n;i++) { int j=i+1; nums.push_back(nums[i]); while(j<=nums.size()-1&&nums[i]>=nums[j]) j++; int a=j==nums.size()?(-1):nums[j]; ans[i]=a; } return ans; } };
-
法二:单调栈法
-
思路:考虑到题目要求的是循环数组nums每个元素右边是否有比它更大的值,有则返回第一个最大值,我们可以从右往左遍历,在遍历每个元素的同时也对其右边的元素进行分析、判断。
-
所以我们可以用单调递减栈对每个元素右边的元素进行维护,当所遍历元素大于栈顶元素,将栈顶元素弹出,接着上述的判断操作,如果栈中的元素全部弹出(说明栈中没有比它更大的值),返回-1;如果栈不为空(说明栈顶元素就是第一个更大值),返回栈顶元素。最后将所遍历的元素压进栈中,遍历到nums下一个元素,重复以上的操作。
-
代码如下:
class Solution { public: vector<int> nextGreaterElements(vector<int>& nums) { int n=nums.size(); vector<int> ans(n); stack<int> s; //初始化单调栈 for(int i =n-1;i>=0;i--) { while(!s.empty()&&nums[i]>=s.top()) s.pop(); s.push(nums[i]); } for(int i =n-1;i>=0;i--) { while(!s.empty()&&nums[i]>=s.top()) s.pop(); int a=s.empty()?-1:s.top(); ans[i] = a; s.push(nums[i]); } return ans; } };
-
-
法三:官方解法
-
代码如下:
class Solution { public: vector<int> nextGreaterElements(vector<int>& nums) { int n = nums.size(); vector<int> ret(n, -1); stack<int> stk; for (int i = 0; i < n * 2 - 1; i++) { while (!stk.empty() && nums[stk.top()] < nums[i % n]) { ret[stk.top()] = nums[i % n]; stk.pop(); } stk.push(i % n); } return ret; } };
-
官方解法妙处:
1.初始化答案数组全为-1,可以只对有更大值的进行操作,无需对每个元素都进行操作,减少时间。
2.对于循环这一问题,用下标对原数组个数取余
3.将元素的下标压进栈中。(不是将元素压进栈中,将元素压进栈中相当于将元素值和其位置一同储存起来,将元素压进栈中仅仅是将元素的大小储存起来)
4.从左往右遍历,若遇到出现比栈顶值大的元素 j,对数组ret下标为栈顶下标的元素赋值为 j,并弹出栈顶值的下标,循环上述操作,直到遇见比 j小的元素跳出循环。(这样做可以在每次遍历查找是否有满足条件的,若有直接进行赋值操作,该方法效率高,不像鄙人从右往左遍历来求出一个元素右边的栈,再对栈中的元素逐个弹出,最后通过栈是否为空来对答案数组对应位置赋值。标答妙在将元素弹出的同时又对答案数组对应位置赋值。
-
-
分析:
-
暴力遍历法:先初始化第一个区间,遍历该区间并求出该区间的最大,最小值;将区间的队头弹出,将下一个元素压进队尾,求此时区间的最大最小。这种办法的复杂度为O(n*k)
-
单调队列法:对上述暴力法我们可以进行优化,即在一个区间内将不可能为最大值或者最小值的元素去掉,减少对这一部分数据的变遍历从而减少时间。
假如现在要求每个区间的最小值,每滑动到下一个区间我们会将下一个元素“i"压进队列里,将队头元素弹出;在此我们可以对上述的进行遍历求最值的那一步进行优化:假如元素 ”i“ 比队尾元素小,我们可以肯定队尾元素肯定不是最小值,故可以弹出该队尾元素;重复以上操作,直到遇到比 ”i“ 小的停止,并将 ”i“ 压进对列,这样就能构建一个单调递增的队列,求最小值只需取队头就行,该操作时间复杂度为O(1),该方法的时间复杂度为O(n)。
-
-
鄙人做法:
思路:利用双端队列deque来构建单调队列
#include <iostream> #include <vector> #include <deque> using namespace std; int main() { int n,k;cin>>n>>k; //定义da、xiao数组用于输出,yuan数组是为了储存输入的数据 vector<int> da(n),xiao(n),yuan(n); deque<int> d,x;//d为求最大值时的对列 for(int i =0;i<n;i++) { int a;cin>>a; yuan[i]=a; } for(int i =0;i<n;i++) { int j=i-k+1;//j为每个区间头的下标 //如果j大于队列队头的下标,将队头下标弹出 if(!d.empty()&&j>d.front()) d.pop_front(); if(!x.empty()&&j>x.front()) x.pop_front(); //构建单调递减的队列 while(!d.empty()&&yuan[i]>yuan[d.back()]) d.pop_back(); //构建单调递增的队列 while(!x.empty()&&yuan[i]<yuan[x.back()]) x.pop_back(); d.push_back(i);x.push_back(i); //将队列的队头取出放进对应的da或xiao数组里面 if(i>=k-1) da[j]=yuan[d.front()]; if(i>=k-1) xiao[j]=yuan[x.front()]; } int t=n-k+1;//t代表区间的总数 for(int i =0;i<t;i++) { cout<<xiao[i]; if(i!=t-1) cout<<" "; } cout << endl; for(int i =0;i<t;i++) { cout <<da[i]; if(i!=t-1) cout <<" "; } return 0; }
-
y总妙解
思路:和上述一样,不过不是用双端队列,而是用普通数组,在定义队头队尾的下标来代替双端队列
#include<iostream> using namespace std; const int N = 1e6 + 10; int n, k, q[N], a[N];//q[N]存的是数组下标 int main() { int tt = -1, hh=0;//hh队列头 tt队列尾 cin.tie(0); ios::sync_with_stdio(false); cin>>n>>k; for(int i = 0; i <n; i ++) cin>>a[i]; for(int i = 0; i < n; i ++) { //维持滑动窗口的大小 //当队列不为空(hh <= tt) 且 当当前滑动窗口的大小(i - q[hh] + 1)>我们设定的 //滑动窗口的大小(k),队列弹出队列头元素以维持滑动窗口的大小 if(hh <= tt && k < i - q[hh] + 1) hh ++; //构造单调递增队列 //当队列不为空(hh <= tt) 且 当队列队尾元素>=当前元素(a[i])时,那么队尾元素 //就一定不是当前窗口最小值,删去队尾元素,加入当前元素(q[ ++ tt] = i) while(hh <= tt && a[q[tt]] >= a[i]) tt --; q[ ++ tt] = i;、//将当前元素下标压进队列 //当i读到第一个区间的队尾时,开始打印最小值 if(i + 1 >= k) printf("%d ", a[q[hh]]); } puts(""); //重新设置队头和队尾的下标,将上述的操作加以修改,求最大值 hh = 0,tt = -1; for(int i = 0; i < n; i ++) { if(hh <= tt && k < i - q[hh] + 1) hh ++; while(hh <= tt && a[q[tt]] <= a[i]) tt --; q[ ++ tt] = i; if(i + 1 >= k ) printf("%d ", a[q[hh]]); } return 0; }
-
给你一个字符串
s
,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。 -
分析:
-
本题问题关键在于要如何删去重复值可以使返回的结果字典序最小,如何做呢?我们从字符串第一个字母开始遍历,将第一个字母压进栈中,然后遍历下一个字母判断是否大于栈顶元素:若大于,将元素压进栈中继续遍历;若小于,判断栈顶元素个数是否为1,若为1,压进栈中;若不为1,弹出栈并一直该判断操作直到栈栈顶的元素的个数为1,停止弹出。重复以上遍历的操作,直到结束,返回该栈的元素。
-
对于示例2的数据,我们解释以上的做法的原因。先将 ’c‘压进栈中,接着遍历到
b
,由于b
比c
的字典序小,我们判断c
是否是唯一的数,发现b
后面有3个c
,以c
开头的字符串不如以b
开头的字符串的字典序小且b
的背后还有c
,所以将c
弹出,b
压进栈;对于a
重复上述的操作,将bc
都弹出,将a
压进;接着就遍历到下一个c
,由于c
大于a
,压进栈;对于d
,无论是否大于c
,由于d
此时是唯一的,所以直接压进栈中;对于接下来的c
由于栈中已存在,跳过即可(若c
在栈中不存在,重复上述进栈的操作);对于唯一的b
直接压进栈内即可。
-
-
思路:
根据上述的分析,我们先构建一个字母表数组
s[26]
用于储存每个字母的个数,每遍历一个字母,对应的字母表数组中的个数减一,接着构建一个栈,进行上述的进栈操作。class Solution { public: string removeDuplicateLetters(string s) { //定义一个哈希表用于储存字母的个数 unordered_map<char,int> biao; //定义string的栈 string str=""; //计算每个字母的个数 for(auto i : s) biao[i]++; for(auto i : s) { //每遍历一次,就将当前字母的个数减一 biao[i]--; //进行弹出判断操作 //若栈不为空,i<栈顶元素,且栈顶元素在i后还会出现,且i不在栈内则将栈顶元素弹出。 while(!str.empty()&&i<str.back()&&biao[str.back()]>0&&str.find(i)==string::npos) { str.pop_back(); } //只要栈内没有i,则将i压进栈内 if(str.find(i)==string::npos) str.push_back(i); } return str; } };
-
笛卡尔树 是由一系列不同数字构成的二叉树。
树满足堆的性质,中序遍历返回原始序列。 最小笛卡尔树表示满足小根堆性质的笛卡尔树。
例如,给定序列 {8,15,3,4,1,5,12,10,18,6},则生成的最小堆笛卡尔树如图所示。
现在,给定一个长度为 N 的原始序列,请你生成最小堆笛卡尔树,并输出其层序遍历序列。
输入格式 第一行包含整数 N。
第二行包含 N 个两不同的整数,表示原始序列。
输出格式 共一行,输出最小堆笛卡尔树的层序遍历序列。
数据范围
1≤N≤30
原始序列中元素的取值范围 [−2147483648,2147483647]。输入样例:
10 8 15 3 4 1 5 12 10 18 6
输出样例:1 3 5 8 4 6 15 10 12 18
-
分析:
-
法一:以下是参考y总的代码和思路
#include <iostream> #include <vector> using namespace std; const int N=40; vector<int> level[N];//定义一个向量数组用于存储每一层的元素 int w[N];//用于储存输入的数组数据 int n ; //定义一个取根节点的函数 int getmin(int l,int r){ int res=l; for(int i =l;i<=r;i++) { if(w[res]>w[i]) res=i; } return res; } //定义一个递归函数,将每一个根节点压进对应层的向量中 void dfs(int l,int r,int d){ if(l>r) return ; int root = getmin(l,r);//取根节点 level[d].push_back(w[root]); dfs(l,root-1,d+1); dfs(root+1,r,d+1); } int main() { cin>>n; for(int i = 0; i < n ; i++ ) cin>>w[i]; dfs(0,n-1,0); //进行层序遍历,level[i].size()为空时停止遍历 for(int i =0;level[i].size();i++) { for(auto x : level[i]) cout << x<<" "; } return 0; }