球球速刷LC-数据结构-优先级队列、单调队列 二轮

优先级队列

基础性质参考文章

是满二叉树
对于大顶堆,任意节点均>两子节点
对于小顶堆,任意节点<两字节点
利用二叉堆即可形成优先级队列。
基本操作包括2个,节点的下沉和上浮

合并多个有序序列!!!

将多个链表的表头加入小顶堆优先级队列。
再依次从队列中取出top元素构建新链表,若该top元素有后继,则将后继加入优先级队列中。

class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        auto cmp=[](ListNode*a, ListNode*b){
            return a->val>b->val;//构建小顶堆
        };
        priority_queue<ListNode*,vector<ListNode*>,decltype(cmp)>q(cmp);
        ListNode head;
        for(auto p:lists){
            if(p!=NULL){
                q.push(p);
            }
        }
        
        auto pHead=&head;
        while(!q.empty()){
            pHead->next=q.top();
            q.pop();
            pHead=pHead->next;
            if(pHead->next != NULL){
               q.push(pHead->next);
            }
            pHead->next=NULL;
        }
        return head.next;
    }
};
第K个最大的元素!!!

堆的经典应用

class Solution1 {
public:
    int findKthLargest(vector<int>& nums, int k) {
        //声明小顶堆
        priority_queue<int, vector<int>, greater<int>> pq;
        for (int i: nums) {
             //heap元素个数小于k,直接入队列
            if (pq.size() < k) pq.push(i);
            else {
            //heap元素==k,只有比堆顶大才能入队列
                if (pq.top() < i) {
                    pq.pop();
                    pq.push(i);
                }
            }
        }
        return pq.top();
    }
};
天际线问题

本质是拿到每个角点处,最大的高度。
对于所有的区间角点,即区间的起点与终点。如果当前所在x坐标是某区间的起点,则应将该区间高度加入比较,如果当前x坐标是某区间终点,则将该区间高度从比较队列中删除.为了区分一个横坐标点是某区间的起始点还是结束点,可以将结束点的高度表示为负值。

class Solution {
public:
    vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
        //有很多角点 (区间的起始点与结束点)
        //要解决的问题是在同一个x坐标下,有哪些高度参与比较。(即哪些区间需要加入,哪些需要删除)
        vector<pair<int,int>>points;
        for(auto &b:buildings){
            points.push_back(pair<int,int>(b[0],b[2]));
            //结束点的高度表示为负值
            points.push_back(pair<int,int>(b[1],-b[2]));
        }
        
        int curr_high=0;
        sort(points.begin(),points.end(),[](pair<int,int>&a,pair<int,int>&b)->bool{return a.first<b.first;});
        multiset<int>m;
        vector<vector<int>>ret;
        for(int i=0;i<points.size();++i){
            auto p=points[i];
            //碰到一个区间起始点,加入高度            
            if(p.second>0){
                m.insert(p.second);
            }else {
                //碰到一个区间终点,将该高度删除
                if(m.empty()==false){
                    m.erase(m.find(-p.second));
                }           
            }
                //对同一个x坐标的节点处理完后,再统一处理该处高度
                if(i<points.size()-1 && points[i].first == points[i+1].first) continue;
            
                int h_max=0;
                if(!m.empty()){
                   h_max=*m.rbegin(); 
                }
                if(h_max!= curr_high){
                    curr_high=h_max;
                    ret.push_back({p.first,curr_high});
                } 
        }
        return ret;        
    }
};
丑陋数2

这是生成数问题,生成数问题的套路是一致的。假设最终生成的数列是
a0 a1 a2 a3 … ai ai+1 …an
则对于其中任意一个元素ai,其必然可以由前面的某个元素aj某个生成因子得到。
本题中的生成因子为{2,3,5}
即任意元素ai都可由其前面的元素aj
2 或ak3 或 aL5得到。
{aj,ak,aL}称为2,3,5的生成算子。将aj ak aL初始化为1,则每次从三者生成的新元素{aj2 ,ak3 ,aL*5}中取最小值,即为最新的生成数ai。并将本次生成ai的生成算子指向数列的下一个位置。

class Solution {
public:
    int nthUglyNumber(int n) {
        if(n==1) return 1;
        vector<int>r={1};//保存丑陋数数列
        int l1=0,l2=0,l3=0; //用于记录生成算子在数列的位置,初始化在第一个位置
        while(r.size()<n){
            int c1=r[l1]*2; //每个生成算子乘以对应的生成因子,得到一个数列元素
            int c2=r[l2]*3;
            int c3=r[l3]*5;
            //最近的一个新生成元素为 当前各个算子生成的最小元素
            int curr=min(c1,min(c2,c3)); 
            r.push_back(curr);
            //对于生成当前新元素的算子,将其位置指向下一个
            if(c1==curr)++l1;
            if(c2==curr)++l2;
            if(c3==curr)++l3;
        }
        return r.back();
    }
};
超级丑陋数

原理同丑陋数,只是这里的生成因子是一给定的数组,因此初始化一个对应的生成算子数字,将算子位置均初始化在第一个位置

class Solution {
public:
    int nthSuperUglyNumber(int n, vector<int>& primes) {
        vector<int>numbers={1};
        vector<int>L(primes.size(),0);
        
        
        while(numbers.size()<n){
          //依次计算当前生成算子与对应生成因子相乘后得到的元素值
          //其中最小的是当前数列的新元素
          int curr_min = numbers[L[0]]*primes[0];
          for(int i=0; i<L.size();++i){
            curr_min=min(curr_min,numbers[L[i]]*primes[i]);
          }
         
          numbers.push_back(curr_min);
          for(int i=0;i<L.size();++i){
          //更新生成算子的位置
            if(numbers[L[i]]*primes[i] == numbers.back()){
                L[i]++;
            }
          }
        }
        return numbers.at(n-1);
    }
};
寻找中位数

中位数将数列划分为数量相等的两个部分,其中左边的最大值<=右边的最小值。因此可以分别用一个大顶堆和一个小顶堆保存左右两部分,使得大顶堆的堆顶元素<=小顶堆的堆顶。 从而实现max{left}<=min{right}。
则二者的堆顶就是划分切面。
当总数为偶数时,median=(left.top+right.top)/2;(此时left.size() == right.size())
当总数为奇数时,median=left.top;(假设奇数时,left.size == right.size+1)

class MedianFinder {
    //左边为大顶堆,右边为小顶堆
    priority_queue<int>left;
    priority_queue<int,vector<int>,greater<int>>right;//右边是小顶堆
public:
    /** initialize your data structure here. */
    MedianFinder() {
        
    }
    
    void addNum(int num) {
       if(left.empty()){
          left.push(num);  
       }else{
           //当前数<= max{left},即属于左侧部分
           if(num<=left.top()){
               left.push(num);
           }else{
               right.push(num);
           }
           
           //调整两堆,使得二者数量相等(总数为偶数)或左边比右边多一个(总数为奇数)
           int target_size=0;
           if((left.size()+right.size())%2==0){
               target_size = (left.size()+right.size())/2;
           }else{
               target_size = 1+(left.size()+right.size())/2;
           }
           
           while(left.size()<target_size){
               left.push(right.top());
               right.pop();
           }
           
           while(left.size()>target_size){
               right.push(left.top());
               left.pop();
           }
       }   
    }
    
    double findMedian() {
        if(right.size()==left.size()){
            return (double)left.top()/2.0+(double)right.top()/2.0;
        }else{
            return left.top();
        }
    }
};
前K个和最小的数字对

类似于top k元素题。本题的对象是前K对最小和。此时可以使用大顶堆记录。
当大顶堆容量<k时,元素直接入队列
当大顶堆容量==K时,只有当前数字对的和<top元素和,才入堆,且堆弹出最大元素,保持容量为K。

class Solution {
public:
    vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
        vector<vector<int>>ret;
        if(nums1.empty() ||nums2.empty() || k<=0) return ret;
        
        auto cmp=[](pair<int,int>&a,pair<int,int>&b){
          return a.first+a.second < b.first+b.second;  //大顶堆  
        };
  //数字对的大顶堆      priority_queue<pair<int,int>,vector<pair<int,int>>,decltype(cmp)>heap(cmp);
        //此处一个优化是,前k小和至多出现在每个数组的前K元素
        for(int i=0;i<nums1.size() && i<k;++i){
            for(int j=0;j<nums2.size()&&j<k;++j){
                if(heap.size()<k) heap.emplace(nums1[i],nums2[j]);
                else if((nums1[i]+nums2[j])<(heap.top().first+heap.top().second)){
                    heap.emplace(nums1[i],nums2[j]);
                    heap.pop();
                }
            }
        }
        
        while( heap.empty()==false){
            ret.push_back({heap.top().first,heap.top().second});
            heap.pop();
        }
        return ret;
    }
};
有序矩阵中的第K小元素!!!

<1>归并排序的思路
每一行都是一个有序序列,一个直观的做法是对矩阵的m行{r0,r1,r2 … rm-1}
进行归并排序。归并排序方法是,将0… i… m-1行的首元素Ri0 放入小顶堆优先级队列中
队列==》 { R00, R10 ,R20 ,R30 …Ri0…Rn-10}
从队列中拿出最小元素,假设是Ri0,再把Ri0所在第i行的下一个元素Ri1放入队列中。循环以上过程,每次取最小元素Rij,如果第i行还有剩余元素,就把Rij+1放入队列中,并把Rij弹出。直至最终队列为空,我们也就完成了对m行的归并排序。
<2>优化归并排序的思路
<1>的思路中我们最终对整个矩阵进行了排序,但是我们只需求得第K最小元素。此处我们尚未利用到的一个条件是,对于矩阵的每一行也是有序的。也即对每一个行首元素,Ri0,大于Ri0的元素是Ri1或者R(i+1)0。
因此我们在做归并排序时,只需先将第一个行首元素{R00}放入队列,当取出R00时,由于下一个>R00的元素为R01或下一个有序序列首元素R10,此时我们把{R01,R10}加入队列。假设下一个最小元素是R01,那么>R01的下一个元素要么是本行下一个元素R02,或者在下一行,但由于下一行的最小元素R10已经在队列中,因此只需将R02加入。以此类推得到:

队列初始化 q->{R00}
while(取出次数<k)
   从q中取出当前最小元素Rij
      如果Rij为行首元素,即j==0,将R(i+1)0 Ri(j+1)加入队列
      如果Rij不是行首元素,即j≠0,将Ri(j+1)加入队列

class Solution{
    public:
     int kthSmallest(vector<vector<int>>& matrix, int k) {
         if(matrix.empty()) return -1;
         
         int m=matrix.size(),n=matrix[0].size();
         //构建小顶堆
         auto cmp=[](pair<int,int>&a,pair<int,int>&b)->bool{
             return a.first>b.first;
         };
         priority_queue<pair<int,int>,vector<pair<int,int>>,decltype(cmp)>heap_q(cmp);
          
         heap_q.push(pair<int,int>(matrix[0][0],0));
         int kc=0;
         while(kc<k && heap_q.empty()==false){
             ++kc;
             if(kc==k){
                 return heap_q.top().first;
             }
             int i= heap_q.top().second/n;
             int j= heap_q.top().second%n;
           
             heap_q.pop();
             //是行首元素,加入下一行的行首
             if(j==0 && i+1<m) heap_q.push(pair<int,int>(matrix[i+1][0],(i+1)*n));
             //当前行右边还有元素,加入队列
             if(j+1<n) heap_q.push(pair<int,int>(matrix[i][j+1],i*n+j+1));
         }         
         return -1;                  
     }
};
二维接雨水

<1>如下图,如果一片区域能够形成水槽,假设其外层水槽壁的最低高度是
x,则水槽中形成的水面高度也是x。
=====最低点X =====
| ~~~~~~~~~~~~~~~~ |
| ~~~~~~ 水 ~~~~~~~~|
| ~~~~~~~~~~~~~~~~ |
==================

<2>如下图,假设有内外两层水槽,则外层水槽的水面最高高度为x。
内层水槽的槽壁最低点高度为Y。若Y<X,则内层水槽水面最高高度为X,
若Y>=X,则内层凹槽水面高度最高为Y。
======= 最低点X =======
| ~~~~~~~ 水 ~ ~~~~~~~~~ |
| ~~ ===== Y ======= ~~~|
| ~~ ||| ~~ ~水 ~~~~~ ||| ~~ |
| ~~ ||| ~~~~~~~~~~~ ||| ~~ |
| ~~ ============= ~~~~|
| ~~~~~~~~~~~~~~~~~~~~ |
=====================
有以上分析,对于越里层的凹槽,其最大水面高度>={X,Y}.X Y分别为外层凹槽槽壁最低点和里层槽壁最低点。也及是其最大水面高度随着凹槽壁最低点的升高而升高。
<3>因此,我们应该从最外层槽壁的最低点开始访问,把与该最低点相邻的且更低的格子形成的体积加入。而水面高度则随着最低点的升高而升高。因此处需要一个小顶堆优先级序列控制访问顺序。考虑到在做BFS搜索时,我们使用一个队列q用于辅助,此处可以将q更改为优先级队列进行访问。

class Solution {
public:
    int trapRainWater(vector<vector<int>>& heightMap) {
        if(heightMap.empty()) return 0;
        if(heightMap.size()<3 || heightMap[0].size()<3) return 0;
        
        int m=heightMap.size();
        int n=heightMap[0].size();
 //小顶堆优先级队列       
        priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;        
        vector<vector<bool>>visit(m,vector<bool>(n,false));//是否访问标志
        
        //从最外围边界开始向内探索,将最外围的槽壁加入队列
        for(int i=0;i<m;++i){
            for(int j=0;j<n;++j){
                if(i==0 || i==m-1 ||j==0 || j==n-1){
                    q.push(pair<int,int>(heightMap[i][j],i*n+j));//注意一次性存储x y位置的方法
                    visit[i][j]=true;
                }
            }
        }
        
        //由于是优先级序列,始终从最低的边界开始,此时与该最低的边界相邻的格子即可计算其储水面积
        int volume =0;
        int water_level=-1;
        
        while(!q.empty()){
            int h=q.top().first;
            int x=q.top().second/n; //注意xy换算是列数!!!
            int y=q.top().second%n;
            q.pop();
            //水面高度更新为当前最低点槽壁的最大值
            water_level = max(water_level,h);
            
            int dx[4]={x-1,x+1,x,x};
            int dy[4]={y,y,y-1,y+1};
            for(int k=0;k<4;++k){
                int new_x = dx[k];
                int new_y = dy[k];
                //访问尚未访问的邻节点
                if(new_x>=0 && new_y>=0 && new_x<m && new_y<n && visit[new_x][new_y] == false){
                //当前水面高度高于格子,则计入体积
                    if(water_level>heightMap[new_x][new_y]){
                        volume+= water_level-heightMap[new_x][new_y];
                    }
                    visit[new_x][new_y]=true;
                    q.push(pair<int,int>(heightMap[new_x][new_y],new_x*n+new_y));
                }
            }            
        }
        return volume;                
    }
};

单调队列

类似于单调栈,单调队列是指始终保持队列内部元素处于有序状态。如单调递增序列,则从队首到队尾,每个元素依次小于后面的元素。因此,在将某元素a入单调递增队列时,需要从队尾将把所有<a的元素删除。
如将{1 3 4 6 2}入单调递增序列,则
队首 [1
队首 [1 3
队首 [1 3 4
队首 [1 3 4 6
队首 [1 3 4 6:2=》[1 3 4 :2 =>[1 3:2=>[1:2
因此单调队列需要队首队尾两头都能操作

滑窗最大值

此题使用单调递减队列,每次滑窗移动时
<1>将过期元素,即索引值小于滑窗起始位置的元素从队首弹出
<2>将新元素从队尾入单调递减序列
从而队尾元素始终是当前滑窗最大值。

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int>ret;
        if(nums.size()== 0|| k==0) return ret;
//滑窗初始起始位置和结束位置
        int start=0;
        int end=start+k-1;
        list<int>s;
        for(start=0;end<=nums.size()-1;){
            if(start==0){
            //滑窗刚刚建立,没有过期元素,直接将滑窗元素放入单调递减队列
                for(int i=start;i<=end;++i){
                //对于单调递减队列,其队首到队尾为递减状态,因此从队尾删除所有<当前入队新元素
                    while(s.empty()==false && nums[i]>=nums[s.back()]){
                        s.pop_back();
                    }
                    s.push_back(i);
                }
            }else{
            //删除过期元素
                while(s.empty()==false && s.front()<start){
                    s.pop_front();
                }
           //当前滑窗新元素入队列     
                while(s.empty()==false && nums[end]>=nums[s.back()]){
                    s.pop_back();
                } 
                s.push_back(end);
            }
           //单调递减队列的队首元素就是最大值 
            ret.push_back(nums[s.front()]);
           //向后移动滑窗 
            ++start;
            end=start+k-1;
        }
        return ret;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值