双端队列

101 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第24天,点击查看活动详情

双端队列

双端队列,维护着一个窗口,在这个窗口里保持着单调性。起始有两个指针LR,都是指向窗口首段。

  • LR 只能向右增加,不能回退
  • 双端队列从头部删除过期元素
  • 双端队列从尾部增加新的元素

当求窗口内的最大值时,让双端队列从队首到队尾按保持单调递减:双端队列的头始终指向最大值,尾部是最小值。每次取最大值时就直接从队首取出元素即可。此外对于数组,队列里存放的可以是元素下标。

  • 队尾:R 指向新的元素。若新元素小于队尾的元素就直接入队;若大于等于队尾的元素,就一直弹出队尾的元素,直到尾部的元素大于新元素或者队列是空。
  • 队首:当L向右移,就检测队首元素是否过期

这里的窗口不是固定的,可以任意扩充缩减。

应用

滑动窗口最大值

对于序列 [1,3,-1,-3,5,3,6,7], k=3

     [1, 3, -1, -3, 5, 3, 6, 7] 
     0  1   2   3  4  5  6  7
 ​
     设立一个辅助双端队列 dqeue ,开始时候,L、R 指向队列首。
     1) R 右移一位,此时dqeue为空,1从dqeue尾部加入
     2)R==3时,31大,要保持窗口内元素单调递减,需要将1从尾部弹出,3加入。
     3)R==-1,-13小,入队。
     4) 此时窗口滑过了前三个元素,那么取出三个元素中的最大值,直接从队首取出。
     5)R==-3,比-1小,入队。
     6) 此时窗口滑过[3,-1,-3],最大值就是此时的队列首元素:3。dd
     7)R==5,大于-3,-1,因此需要将元素依次弹出,再将5压入 
     8)此时窗口滑过[-1,-3, 5],此时最大值就是队列首元素:5。
     9)R==3,小于5,入队。
     10) 此时窗口滑过[-3, 5, 3],最大值是队列首元素:5
     11)R==6,依次将53弹出,6入队
     10)此时滑动窗口滑过[5,3,6],最大值就是队首:6
     11)R==7,弹出6,入队7
     12)此时滑动窗口划过[3,6,7],最大值就是队首:7

从队列中删除元素,有两种情况:

  • 队首元素过期,即当队首元素和当前元素距离之差超过窗口值k,队首元素就是过期,应该从队首删除
  • 新的元素大于等于队尾元素,需要不断地弹出队尾元素,直到满足队尾大于新的元素,或者弹到空
   class Solution {
   public:
       std::vector<int> maxSlidingWindow(std::vector<int>& nums, int k) {
               
         int N = nums.size();
         std::vector<int> windMax(N -k+1);
         std::list<int> path;
 ​
         for(int R=0; R < N; ++R) { 
 ​
           while(!path.empty() && nums[path.back()] <= nums[R]) { 
             path.pop_back();
           }
 ​
           path.push_back(R);
 ​
           // 新元素的加入可能会导致窗口大于 k 
           // 使得队列首部元素过期
           if(path.front() + k == R) { 
             path.pop_front();
           }
 ​
           // 只要窗口大于等于k,就需要计算下最大值
           if(R >= k-1) { 
             windMax[R-(k-1)] = nums[path.front()];
           }
         }
 ​
         return windMax;
       }
   };

窗口的最大值减去最小值小于等于 num 的子数组个数

现象:

  • 如果一个子数组满足要求,那么这个子数组的任何一个子数组也符合要求:因为这个大的子数组中 max - min <= num ,那么这个大的子数组的子数组的肯定也是在[min, max]里面
  • 如果一个子数组不满足要求,那么这个子数组再怎么扩,也不达标:因为这个子数组max - min > num,那么扩充只能让 max变大min变小,更加不满足。

建立两个双端队列,一个用来保存最大值,一个用来保存最小值。

  • 对于数组 arrLR 开始指向起始位置
  • R 向右扩,扩到第一个不符合要求的元素,那么 [L, R) 都是满足要求的。 那么以 L 为开始且符合要求的子数组共有R-L个。
  • 然后 L 右移动,R 重复之前动作。
 int getNum(std::vector<int> arr, int num) { 
     if(arr.empty()) return 0;
 ​
     std::list<int> qmin;
     std::list<int> qmax;
 ​
     int result =0;
     // 计算以每个 L 的情况
     for(int L  =0; L < arr.size(); ++L) { 
         // R 扩到不能再扩 停止
         for(int R=0; R < arr.size(); ++R) { 
             //  单调递增
             while(!qmin.empty() && arr[qmin.back()] >= arr[R]) { 
                 qmin.pop_back();
             }
             qmin.push_back(R);
 ​
             // 单调递减
             while(!qmax.empty() && arr[qmax.back()] <= arr[R]) { 
                 qmax.pop_back();
             }
             qmax.push_back(R);
 ​
             if(arr[qmax.front()] - arr[qmin.front()] > num) { 
                 break;
             }
         }
 ​
         // 最小值过期查询
         if(qmin.front() == L)  qmin.pop_front();
         // 最大值过期查询
         if(qmax.front() == L)  qmax.pop_front();
 ​
         result += R - L;
     }
 ​
     return result;
 }