• 算法学习-优先队列(堆)


    笔者在刷题的过程中,经常碰到优先队列相关的题目,因此本文对其知识体系进行汇总,供大家参考学习。

    本文参考:

    算法吧-优先队列专题
    堆的性质、堆的实现、堆排序

    基础知识

    优先队列是一种抽象的数据结构,不同于普通的队列FIFO,其优先级最高的元素最先出队。堆是实现优先队列的一种高效的数据结构,因此网上常常将「堆」和「优先队列」等价。堆有两个性质:

    • 堆是一棵完全二叉树
    • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。

    因此就可以将其分为两类,对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做「大顶堆」。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做「小顶堆」。

    堆帮我们省去了手动维护队列有序性的工作,常用于求最值,或者在que.size()==K用于求固定数量的最值问题。

    Java中和优先队列有关的常用api如下:

    //优先队列声明
    //默认升序排列,小顶堆
    PriorityQueue<Integer> pq = new PriorityQueue<>();
    //自定义降序,大顶堆
    PriorityQueue<Integer> pq = new PriorityQueue<>((a,b)->b-a); 
    
    //返回队首元素
    pq.peek();
    //返回队首元素,队首元素出队列
    pq.poll();
    //添加元素
    pq.add();
    pq.offer();
    //返回队列元素个数
    pq.size();
    //判断队列是否为空,为空返回true,不空返回false
    pq.isEmpty();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    时间复杂度:插入和删除都是O(logK)

    更优的改进是建立「索引堆」。当我们的原数据可以通过索引index访问的话,我们可以在队列中,比较的是 data 的数据,交换的是 index 的位置。既能避免将原数据(比如字符串)直接放入堆的性能消耗,又可以通过索引非常高效地索引到原数据。比如下面的题857.

    原理实现

    听说有些大厂的面试官会让自己手写建堆,因此有必要了解一些建堆的知识。这块知识需要后面补上。

    相关题目

    心得体会:

    1. 需要弄清楚入堆的处理和建立什么堆,如何利用堆顶的信息。
    857.雇佣K名工人的最低成本

    最大索引堆+时薪升序入堆。为了在K个人中,既能按比例支付,同时比例支付的工资又不能低于期望工资,我们算出每个人单位quality的wage(时薪r),在K个人中选择时薪最大的maxr,就也能满足其他人的需求。以上K个人的总薪资为sumQ*maxr,为了能让总薪资最低,我们让maxr尽可能小,同时要让总时间sumQ也尽可能小。从小到达枚举r,当前的时薪就是最大时薪,用最大堆维护的是时间Quality,当堆中有k个元素时,如果当前Quality比堆顶小,则可以弹出堆顶,将Quality入堆,产生一个更小的sumQ,这有可能「折衷」产生一个更小的总薪资的结果。

    class Solution {
        public double mincostToHireWorkers(int[] quality, int[] wage, int k) {
            int len=quality.length;
            //建立索引
            Integer[]idx=new Integer[len];
            for(int i=0;i<len;i++){
                idx[i]=i;
            }
            //将时薪从小到大排序
            Arrays.sort(idx,(i,j)->wage[i]*quality[j]-wage[j]*quality[i]);
    
            //建立有k个元素的大根堆,维护quality
            PriorityQueue<Integer> pq=new PriorityQueue<>((a,b)->b-a);
            int sum=0;
            for(int i=0;i<k;i++){
                pq.offer(quality[idx[i]]);
                sum+=quality[idx[i]];
            }
            double ans= (double)wage[idx[k-1]]/quality[idx[k-1]]*sum;
    
            for(int i=k;i<len;i++){
                if(quality[idx[i]]<pq.peek()){
                    int outq=pq.poll();
                    pq.offer(quality[idx[i]]);
                    sum=sum-outq+quality[idx[i]];
                }
                double temp= (double)wage[idx[i]]/quality[idx[i]]*sum;
                ans=Math.min(ans,temp);
            }
            return ans;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    6178.将区间分为最少组数

    最小堆+左边界升序入堆+贪心。看灵神的题解说是用到堆,没想到这也可以,一步步分析来。首先看到答案/划分与输入顺序无关,应该可以想到「排序」,给我们一些利于解题的性质。然后就是如何贪心地将区间进行组合,尤其是可能会存在多组的情况,可以看到在划分过程中,如果某个划分的组中区间出现重叠,那我们就应该新开一个划分组,但是在没有出现重叠的情况下,我们尽可能将当前数据放到之前划分的右边界最小的组中,并且更新其右边界。这种找最小同时更新最小再放入下次比较,我们可以采用堆实现,如果当前元素左边界比最小还小的话,只能入堆,相当于新开了一组,最终堆中的元素就是总组数。

    class Solution {
        public int minGroups(int[][] intervals) {
            Arrays.sort(intervals,(a,b)->(a[0]-b[0]));
            PriorityQueue<Integer> pq=new PriorityQueue<>();
            for(int[]i:intervals){
                if(!pq.isEmpty()&&i[0]>pq.peek()){
                    pq.poll();
                    pq.offer(i[1]);
                }else{
                    pq.offer(i[1]);
                }
            }
            return pq.size();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    双堆模拟问题

    通常是放在服务器调度等题目上,需要根据各自的规则建立合适的优先队列比较规则。

    2402.会议室III

    双堆模拟。“当会议室处于未占用状态时,将会优先提供给原开始时间更早的会议。”从这句话就可以知道,会议的安排只和会议的开始时间顺序相关,因此可以按照开始时间对会议升序处理。用一个小顶堆维护空闲会议室编号,这是为了每次能够取用最小编号的空闲会议室;用另一个小顶堆维护(已占用会议室的结束时间,已占用编号),在遍历所有会议的时候,每次将遍历的meetings[0]前结束的会议弹出,放入空闲会议室。接下来在判断当前会议加入哪个会议室的时候,如果有空会议室,则占用;没有可用的会议室,则等待,编程上反映就是弹出已占有会议室中结束时间最早的会议室(但仍然比当前meetings[0]要大),占用它并更新已占用会议室的结束时间的差值(相当于当前会议室延后了我等待的时间),将其加入占用会议室堆。

    时间复杂度:O(MlogM+MlogN+N)。分别为会议排序时间,遍历会议调整堆中会议室,最后选出答案。

    class Solution {
        public int mostBooked(int n, int[][] meetings) {
            Arrays.sort(meetings,(a,b)->a[0]-b[0]);
            int[]ans=new int[n];
            PriorityQueue<Integer> pqidle=new PriorityQueue<>();
            //存放(会议室结束时间,编号),优先选择结束时间最早的会议室,当结束时间相同,优先弹出编号小的,后面if(pqidle.size()==0) poll需要注意
            PriorityQueue<Pair<Long,Integer>> pqusing=new PriorityQueue<Pair<Long,Integer>>((a,b)->Objects.equals(a.getKey(),b.getKey())?Integer.compare(a.getValue(),b.getValue()):Long.compare(a.getKey(),b.getKey()));
            for(int i=0;i<n;i++) pqidle.offer(i);
            int len=meetings.length;
            for(int i=0;i<len;i++){
                long start=meetings[i][0];
                long end=meetings[i][1];
                //将start以前结束的会议室放出
                while(!pqusing.isEmpty()&&start>=pqusing.peek().getKey()){
                	//这里只是在满足时间的情况下弹出,然后放入的时候需要满足pq两个条件构造优先队列方便后面的pqusing.poll()操作
                    Pair<Long,Integer> top=pqusing.poll();
                    pqidle.offer(top.getValue());
                }
    
                int id;
                //如果没有空闲会议室
                if(pqidle.size()==0){
                	//前面pq条件两个写好就是在poll的时候,结束时间相同,弹出编号最小的会议室
                    //等待top.getKey()-start的时间空出来并占用
                    Pair<Long,Integer>top=pqusing.poll();
                    end+=top.getKey()-start;
                    id=top.getValue();
                    pqusing.offer(new Pair<Long,Integer>(end,id));
                }else{
                    int idletop=pqidle.poll();
                    id=idletop;
                    pqusing.offer(new Pair<Long,Integer>(end,idletop));
                }
                ans[id]+=1;
            }
            int res=0;
            for(int i=0;i<ans.length;i++){
                if(ans[i]>ans[res]) res=i;
            }
            return res;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    1606.找到处理最多请求的服务器

    双堆模拟。一个最小堆用于维护当前可用服务器编号,一个最小堆用于维护当前正在使用的服务器的结束时间及编号。在维护使用服务器的时候,和2402.会议室III不一样的是PriorityQueue排序条件的书写,这里在有空闲服务器的时候才让出,不然就丢弃,只和空闲服务器的编号有关;而上一题不仅需要等待时间差值,还和会议室编号有关,因此会在时间相同的时候,优先让出编号最小的会议室。

    class Solution {
        public List<Integer> busiestServers(int k, int[] arrival, int[] load) {
            PriorityQueue<Integer> idle= new PriorityQueue<>((a,b)->a-b);
            //这里只需要满足每次把时间最小的放前面就行了,后面没有额外的poll()
            PriorityQueue<Pair<Long,Integer>> used= new PriorityQueue<>((a,b)->Long.compare(a.getKey(),b.getKey()));
            int len=arrival.length;
            int[]ans=new int[k];
            for(int i=0;i<k;i++){
                idle.offer(i);
            }
    
            for(int i=0;i<len;i++){
                long arrivaltime=arrival[i],loadtemp=load[i];
                while(!used.isEmpty()&&arrivaltime>=used.peek().getKey()){
                    Pair<Long,Integer> top=used.poll();
                    //考虑到编号从i%k开始往后考虑,因此放入的时候需要考虑从i开始向后进行合理偏移,也能够让小顶堆很好地维护
                    //如果是负数或者正数都是向后进行偏移,方便从i%k开始考虑,取余是避免了负数太大,+k没法变正,最后无法偏移的情况
                    int index=i+((top.getValue()-i)%k+k)%k;
                    idle.offer(index);
                }
                if(!idle.isEmpty()){
                    int top=idle.poll()%k;
                    ans[top]++;
                    used.offer(new Pair<Long,Integer>(arrivaltime+loadtemp,top));
                }
            }
            //一次遍历找到所有最大值序号
            ArrayList<Integer> res=new ArrayList<>();
            int m=0;
            for(int i=0;i<k;i++){
                if(ans[i]>m){
                    res.clear();
                    res.add(i);
                    m=ans[i];
                }else if(ans[i]==m){
                    res.add(i);
                }
            }
            return res;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    1882.使用服务器处理任务

    双堆模拟,和2402.会议室III很像,同样需要等待,但这一题是在有空闲服务器的时候,优先选择权重最小然后才是标号最小,因此需要多存储一个权重的信息。

    class Solution {
        public int[] assignTasks(int[] servers, int[] tasks) {
            //idle有[权重,服务器标号],先按照权重小的来,然后是下标小的
            PriorityQueue<long[]> idleServer=new PriorityQueue<>((a,b)->a[0]==b[0]?Long.compare(a[1],b[1]):Long.compare(a[0],b[0]));
            //used有[权重,服务器标号,服务器结束时间],先按照结束时间早空闲的来,在结束时间相同时优先选择权重最小,下标最小的
            PriorityQueue<long[]> used=new PriorityQueue<>((a,b)->a[2]==b[2]?(a[0]==b[0]?Long.compare(a[1],b[1]):Long.compare(a[0],b[0])):Long.compare(a[2],b[2]));
            int n=servers.length;
            int m=tasks.length;
            int[]ans=new int[m];
    
            long curtime=-1;
            for(int i=0;i<n;i++){
                idleServer.offer(new long[]{servers[i],i});
            }
    
            for(int j=0;j<m;j++){
                while(!used.isEmpty()&&j>=used.peek()[2]){
                    long[] usedtop=used.poll();
                    idleServer.offer(new long[]{usedtop[0],usedtop[1]});
                }
                if(idleServer.size()==0){
                    long[] usedtop=used.poll();
                    long endtime=usedtop[2]+tasks[j];
                    ans[j]=(int)usedtop[1];
                    used.offer(new long[]{usedtop[0],usedtop[1],endtime});
                }else{
                    long[] idletop=idleServer.poll();
                    long endtime=j+tasks[j];
                    ans[j]=(int)idletop[1];
                    used.offer(new long[]{idletop[0],idletop[1],endtime});
                }
            }
            return ans;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    Top K问题

    最小的K个数

    第一直觉是建立小根堆,这里学习建立大根堆的做法,维护一个大小为K的最大堆, 遍历N个元素,通过和堆头的最大值比较,若碰到比堆头小的数,则堆头必不可能属于最后找到的那K个最小值,将头弹出,当前元素入堆进行平衡性调整,时间复杂度为O(NlogK)

    相比于建立小根堆,若要弹出堆头的K个最小元素,需要遍历N个元素,同时维护一个大小为N的堆,这是因为你无法通过当前的最小值去判断它是否为最后的K个的最小值,比如原序列有大于K个元素是从大到小排列的,插入前K个元素显然不是最终答案,维护整个堆的时间复杂度为O(NlogN)

    class Solution {
        //建立大根堆,时间复杂度为O(NlogK)
        public int[] smallestK(int[] arr, int k) {
            if(k==0) return new int[0];
            PriorityQueue<Integer> pq=new PriorityQueue<>((a,b)->b-a);
            for(int i:arr){
                //堆达到k了以后,将有可能会是最小值的元素放入堆中
                if(pq.size()==k&&i<pq.peek()){
                    pq.poll();
                }
                // 比当前堆顶还大就不可能了
                if(pq.size()==k&&i>=pq.peek()){
                    continue;
                }
                // 在还没达到k的时候就直接加入
                pq.offer(i);
            }
            int[]ans=new int[k];
            for(int i=0;i<k;i++){
                ans[i]=pq.poll();
            }
            return ans;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    59.数据流的第K大数值

    维护一个K大小的小顶堆,在往里面add的时候不断进行检查维护,最后堆顶元素就是答案。

    class KthLargest {
        PriorityQueue<Integer> q=new PriorityQueue<>();
        int k;
        public KthLargest(int _k, int[] nums) {
            k=_k;
            for(int i:nums){
                q.offer(i);
            }
        }
        
        public int add(int val) {
            q.offer(val);
            // 维护一个K大小的小顶堆
            while(q.size()>k){
                q.poll();
            }
            // 堆顶元素就是第K大元素
            return q.peek();
        }
    }
    
    /**
     * Your KthLargest object will be instantiated and called as such:
     * KthLargest obj = new KthLargest(k, nums);
     * int param_1 = obj.add(val);
     */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    60.出现频率最高的k个数字

    哈希表+最小堆。

    class Solution {
        public int[] topKFrequent(int[] nums, int k) {
            // 哈希表存频率
            HashMap<Integer,Integer> mp=new HashMap<>();
            for(int i:nums){
                mp.put(i,mp.getOrDefault(i,0)+1);
            }
    
            // 最小堆存频率最高的K个数
            PriorityQueue<int[]> pq=new PriorityQueue<>((a,b)->(a[1]-b[1]));
            for(Map.Entry<Integer,Integer> m:mp.entrySet()){
                if(pq.size()==k){
                	// 唯一数字,> = 也没关系
                    if(m.getValue()>pq.peek()[1]){
                        pq.poll();
                        pq.offer(new int[]{m.getKey(),m.getValue()});
                    }
                }else{
                     pq.offer(new int[]{m.getKey(),m.getValue()});
                }
            }
            int[]ans=new int[k];
            for(int i=0;i<k;++i){
                ans[i]=pq.poll()[0];
            }
            return ans;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    61.和最小的k个数对

    最大堆,根据数对和进行排序。

    class Solution {
        public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
            PriorityQueue<int[]> pq=new PriorityQueue<>((a,b)->(b[0]+b[1])-(a[0]+a[1]));
            int len1=Math.min(nums1.length,k);
            int len2=Math.min(nums2.length,k);
            for(int i=0;i<len1;i++){
                for(int j=0;j<len2;j++){
                    if(pq.size()==k){
                        if(nums1[i]+nums2[j]<pq.peek()[0]+pq.peek()[1]){
                            pq.poll();
                            pq.offer(new int[]{nums1[i],nums2[j]});
                        }
                    }else{
                        pq.offer(new int[]{nums1[i],nums2[j]});
                    }
                }
            }
            List<List<Integer>> res=new ArrayList<>();
            while(!pq.isEmpty()){
                int[] top=pq.poll();
                res.add(Arrays.asList(top[0],top[1]));
            }
            return res;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
  • 相关阅读:
    TTS引用与选型
    Actor-Critic(AC)算法学习
    docker配置nginx
    嵌入式系统开发【深入浅出】 GPIO 类设备的驱动程序
    第13届 蓝桥杯青少年创意编程(Scratch、Python、C++)
    【docker】dockerfile概念简介
    (自学)黑客————网络安全技术
    如何得到numpy当前的随机数种子
    Redis 有哪些适合的场景?
    ASP.NET Core 6.0 启动方式
  • 原文地址:https://blog.csdn.net/qq_44036439/article/details/126654614