C++ 单调栈和单调队列的学习及刷题

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;
         }
     };

刷题

496.下一个更大元素 I【力扣】

  • 法一:暴力循环

    思路:先对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;
     }
 };

503. 下一个更大元素 II【力扣】

  • 给定一个循环数组 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小的元素跳出循环。(这样做可以在每次遍历查找是否有满足条件的,若有直接进行赋值操作,该方法效率高,不像鄙人从右往左遍历来求出一个元素右边的栈,再对栈中的元素逐个弹出,最后通过栈是否为空来对答案数组对应位置赋值。标答妙在将元素弹出的同时又对答案数组对应位置赋值。

154. 滑动窗口【AcWing】

  • 分析

    • 暴力遍历法:先初始化第一个区间,遍历该区间并求出该区间的最大,最小值;将区间的队头弹出,将下一个元素压进队尾,求此时区间的最大最小。这种办法的复杂度为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;
     }

316、去除重复字母【力扣】

  • 给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

  • 分析

    • 本题问题关键在于要如何删去重复值可以使返回的结果字典序最小,如何做呢?我们从字符串第一个字母开始遍历,将第一个字母压进栈中,然后遍历下一个字母判断是否大于栈顶元素:若大于,将元素压进栈中继续遍历;若小于,判断栈顶元素个数是否为1,若为1,压进栈中;若不为1,弹出栈并一直该判断操作直到栈栈顶的元素的个数为1,停止弹出。重复以上遍历的操作,直到结束,返回该栈的元素。

    • 对于示例2的数据,我们解释以上的做法的原因。先将 ’c‘压进栈中,接着遍历到b,由于bc的字典序小,我们判断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;
         }
     };

4279、笛卡尔树 【Awcing】

  • 笛卡尔树 是由一系列不同数字构成的二叉树。

    树满足堆的性质,中序遍历返回原始序列。 最小笛卡尔树表示满足小根堆性质的笛卡尔树。

    例如,给定序列 {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

  • 分析:

    • 要做出本题首先得了解题目的意思,得知道什么是二叉树,什么是中序遍历(中序遍历可以参考这篇大佬的文章中序遍历)

    • 根据题意,我们要将给定的数组按最小堆笛卡尔树先排列好再按层序遍历输出即可

    • 由该最小堆笛卡尔树的性质,每个根节点都为每个子树的最小值,我们可知该树根节点为1,左边为左子树,右边为右子树,左子树的根节点为3,右子树的根节点为5;对于左子树它也存在它的左子树和右子树。于是我们可以用递归进行操作,来求取每个根节点的值。

    • 因为要进行层序遍历,所以我们可以在递归的过程中记录对应元素的层序

  • 法一:以下是参考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;
     }

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值