Java PriorityQueue(优先级队列/二叉堆)的使用及题目应用

Java中PriorityQueue底层通过二叉小顶堆实现,可以用一棵完全二叉树表示。

优先队列的作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列默认每次取最小元素,C++的优先队列默认每次取最大元素)。这里牵涉到了大小关系,元素大小的评判可以通过元素本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator,类似于C++的仿函数)。

如果排列的元素不是基本类型,而是自定义的对象(比如学生对象,按年龄排序,节点类型,按节点值排序),这种就需要重写比较器Comparator。或者想要每次从队头取出的都是最大的那个元素,也需要在创建PriorityQueue对象的时候重写作为参数的比较器Comparator。

PriorityQueue有几个需要注意的点:

  • 不允许加入NULL对象
  • 添加到PriorityQueue的对象必须具有可比性
  • 比较器Comparator可用于队列中对象的自定义排序(升序/降序,自定义参与比较的是对象的哪个变量)
  • PriorityQueue是一个无限制的队列,并且动态增长。默认初始容量’11’可以使用相应构造函数中的initialCapacity参数覆盖
  • 如果存在多个具有相同优先级的对象,则它可以随机轮询其中任何一个
  • PriorityQueue 不是线程安全的。PriorityBlockingQueue在并发环境中使用
  • 它为add/offer和remove/poll方法提供了O(log(n))时间

重写比较器的方法

第一种是容易理解的,比如对链表节点ListNode类型按val值进行升序排序:

PriorityQueue<ListNode> priorityQueue=new PriorityQueue<>(new Comparator<ListNode>(){
            //因为需要对ListNode进行排序,需要重写排序规则
            @Override
            public int compare(ListNode node1,ListNode node2){
                return node1.val-node2.val;//按val值升序规则排序
            }
        });

第二种是借助lambda表达式:

PriorityQueue<ListNode> priorityQueue=new PriorityQueue<>((node1,node2)->node1.val-node2.val);

应用题目

LeetCode 1845. 座位预约管理系统

原题链接

思路:
reverse与unreverse操作可能会比较频繁,需要较好的效率。简单的思路是每次加入座位后都对编号进行从小到大的排序,但是这样时间复杂度比较高,从题目要求reverse每次都返回最小编号,可以想到二叉堆(优先级队列)的实现,其默认是最小堆。

代码如下:

class SeatManager {
    PriorityQueue<Integer> pq=new PriorityQueue<>();

    //初始化
    public SeatManager(int n) {
        for(int i=1;i<=n;i++){
            pq.offer(i);
        }
    }
    
    //弹出最小编号
    public int reserve() {
        return pq.poll();
    }
    
    public void unreserve(int seatNumber) {
        pq.offer(seatNumber);
    }
}

/**
 * Your SeatManager object will be instantiated and called as such:
 * SeatManager obj = new SeatManager(n);
 * int param_1 = obj.reserve();
 * obj.unreserve(seatNumber);
 */

LeetCode 215. 数组中的第 K 个最大元素(同剑指 Offer II 076. 数组中的第 k 大的数字)

原题链接

只要维护一个大小为k的最小堆,遍历一遍数组,不断把遍历到的元素加入最小堆,当元素个数超过k时,将堆顶元素弹出,这样就可以把当前堆内最小的元素都弹出,最后剩下k个最大的元素,而堆顶元素就是第k大的数。

代码如下:

//最小堆方案
class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> pq=new PriorityQueue<>();
        for(int i=0;i<nums.length;i++){
            //所有元素过一遍最小堆
            pq.offer(nums[i]);
            //但是元素超过k个需要把顶部元素取出,保持堆大小为k
            if(i>=k)pq.poll();
        }
        return pq.peek();
    }
}

LeetCode 703. 数据流中的第 K 大元素(同剑指 Offer II 059. 数据流的第 K 大数值)

原题链接

这题与前一题是差不多的思路,只是要求add操作完要返回当前第k大的。

代码如下:

class KthLargest {
    private PriorityQueue<Integer> pq=new PriorityQueue<>();
    private int k;

    public KthLargest(int k, int[] nums) {
        
        for(int i=0;i<nums.length;i++){
            pq.offer(nums[i]);
            if(i>=k)pq.poll();
        }
        this.k=k;
    }
    
    public int add(int val) {
        pq.offer(val);
        if(pq.size()>k)pq.poll();
        return pq.peek();
    }
}

/**
 * Your KthLargest object will be instantiated and called as such:
 * KthLargest obj = new KthLargest(k, nums);
 * int param_1 = obj.add(val);
 */

LeetCode 295. 数据流的中位数(同剑指 Offer 41. 数据流中的中位数)

原题链接

用一个大顶堆和一个小顶堆维护一堆数据的中位数。

class MedianFinder {
    //假设nums是一个排序好的数组,那么小顶堆元素就是nums的后半截,堆顶是nums的中间数
    //大顶堆元素就是nums的前半截,堆顶是nums的中间数
    //插入需要保证大顶堆元素个数大于等于小顶堆元素个数,且最多多1个
    //这样在找中位数时,只要元素个数不同,中位数就是大顶堆堆顶
    private PriorityQueue<Integer> small;//小顶堆
    private PriorityQueue<Integer> large;//大顶堆

    public MedianFinder() {
        small=new PriorityQueue<>();
        large=new PriorityQueue<>((n1,n2)->{
            return n2-n1;
        });
    }
    
    //插入num需要保证大小顶堆的元素大小关系
    //num与大顶堆(小于等于中位数)的关系作为判断
    //如果num<=large.peek(),应该插入大顶堆
    //如果此时大顶堆元素比小顶堆元素多2,就要把大顶堆堆顶元素插入小顶堆
    //如果num>large.peek(),应该插入小顶堆
    //
    public void addNum(int num) {
        if(large.size()==0)large.offer(num);
        else if(num<=large.peek()){
            large.offer(num);
            if(large.size()>small.size()+1)small.offer(large.poll());
        }else{
            small.offer(num);
            if(large.size()<small.size())large.offer(small.poll());
        }

    }
    
    public double findMedian() {
        if(small.size()==large.size())return (double)(small.peek()+large.peek())/2.0;
        else return (double)large.peek();
    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

LeetCode 23. 合并 K 个升序链表(同剑指 Offer II 078. 合并排序链表)

原题链接

2023.06.10 二刷

代码如下:

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        /**PriorityQueue<ListNode> priorityQueue=new PriorityQueue<>(new Comparator<ListNode>(){
            //因为需要对ListNode进行排序,需要重写排序规则
            @Override
            public int compare(ListNode node1,ListNode node2){
                return node1.val-node2.val;//按val值升序规则排序
            }
        });
        */
        PriorityQueue<ListNode> priorityQueue=new PriorityQueue<>((node1,node2)->node1.val-node2.val);

        
        //遍历链表数组每一条链表(实际上就是每条链表的头结点)
        for(ListNode node:lists){
            //当前链表可能是空的,优先级队列不能添加null元素
            if(node!=null)
                priorityQueue.offer(node);
        }

        //用一个新链表承接节点
        ListNode dummy=new ListNode(-1);
        ListNode cur=dummy;
        while(!priorityQueue.isEmpty()){
            ListNode node=priorityQueue.poll();//取出优先级队列中最小的
            //取一个出来,就要放一个进去
            if(node.next!=null)priorityQueue.offer(node.next);
            cur.next=node;//把node接到新链表后面
            cur=cur.next;//新链表指针向前移动
        }
        return dummy.next;
    }
}

LeetCode 1834. 单线程 CPU

原题链接

思路见代码中注释。

代码如下:

class Solution {
    public int[] getOrder(int[][] tasks) {
        int n=tasks.length;//任务数量
        /**第一步 */
        //三元数组,将task的编号记录作为第三个元素,防止排序后丢失编号顺序
        int[][] newTasks=new int[n][3];
        for(int i=0;i<n;i++){
            newTasks[i][0]=tasks[i][0];
            newTasks[i][1]=tasks[i][1];
            newTasks[i][2]=i;//记录任务编号
        }

        /**第二步 */
        //根据任务入队时间升序排序
        Arrays.sort(newTasks,(o1,o2)->o1[0]-o2[0]);

        /**第三步 */
        //优先队列,重写排序规则
        //按执行时间升序排序,执行时间相同按任务编号升序
        PriorityQueue<int[]> pq=new PriorityQueue<>(new Comparator<int[]>(){
            public int compare(int[] o1,int[] o2){
                //执行时间不同时,优先按照执行时间升序排序
                if(o1[1]!=o2[1])return o1[1]-o2[1];
                //如果执行时间相同,就按任务编号升序
                return o1[2]-o2[2];
            }
        });


        /**最后一步--遍历处理 */
        int[] res=new int[n];//存储结果(任务编号顺序)
        int time=0;//记录任务执行时间线
        int resIndex=0;//执行完的任务数量,用于遍历res数组,res[resIndex]存储任务编号
        int tIndex=0;//用于遍历newTasks数组

        //用resIndex遍历
        while(resIndex<n){

            /**进入优先级队列的逻辑 */
            //安排任务进入优先队列
            //要保证还有任务可以安排(tIndex<n)
            //需要被安排的任务一定是进入序列的时间小于等于当前时间线的
            while(tIndex<n&&newTasks[tIndex][0]<=time){
                pq.offer(newTasks[tIndex]);
                tIndex++;//newTasks的指针前移指向下一个待处理任务
            }

            /**执行优先级队列里的任务的逻辑 */
            //如果优先队列为空,说明没有任务等待
            //时间线直接跳到下一个任务进入任务序列的时间
            if(pq.isEmpty()){
                time=newTasks[tIndex][0];
            }else{//队列不为空,就要执行堆顶任务
                int[] cur=pq.poll();//记录堆顶任务三元组
                res[resIndex++]=cur[2];//记录任务编号,并且索引前进
                time+=cur[1];//时间线快跳到任务执行完成
            }

        }
        return res;
    }
}

LeetCode 239. 滑动窗口最大值

原题链接

2023.06.01 三刷

最大堆思路:
将最大堆–二元组(num,index)排序规则设置为按num降序,num相同则按下标升序(但是优先队列的默认排序规则就是升序,所以num相同时下标会默认按升序来,不用特别写)

何时弹出二叉堆元素?

正常思维肯定是当堆元素个数超过k的时候弹出,但是这题当堆元素个数超过k,弹出的堆顶元素可能不是最左边窗口边界的元素。用i遍历剩下的元素,i-k+1就是窗口左边界,只要看堆顶元素的下标是不是小于这时候的左边界,如果小于,说明堆顶元素不在窗口内,可以弹出,这样一直判断直到堆顶元素是窗口内的,就可以给res赋值。

代码如下:

//最大堆,时间O(nlogn),空间O(n)
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        //设置优先级队列排序规则
        PriorityQueue<int[]> pq=new PriorityQueue<>(new Comparator<int[]>(){
            public int compare(int[] o1,int[] o2){
                return o1[0]!=o2[0] ? o2[0]-o1[0] : o2[1]-o1[1];
            }
        });

        //初始化二叉堆(存入前k个元素)
        for(int i=0;i<k;i++){
            pq.offer(new int[]{nums[i],i});
        }

        int n=nums.length;
        int[] res=new int[n-k+1];//最后会有n-k+1个窗口
        res[0]=pq.peek()[0];//先把最开始堆顶元素填入

        //遍历剩下的数组元素
        for(int i=k;i<n;i++){
            pq.offer(new int[]{nums[i],i});
            //当堆顶元素(最大的)不在当前窗口内,就弹出
            while(pq.peek()[1]<=i-k){
                pq.poll();
            }//出while能保证当前堆顶元素一定在窗口内,就是最大值
            res[i-k+1]=pq.peek()[0];
        }

        return res;
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值