优先级队列PriorityQueue在算法问题中的使用


优先级队列介绍

优先队列一般基于二叉堆实现,二叉堆:
堆的根节点的优先级最大(即最大或最小)
父节点的优先级必定大于子节点,兄弟节点的优先级不确定谁大谁小

PriorityQueue:

PriorityQueue是基于二叉堆原理的优先队列,队列用动态数组实现。
它是非阻塞的、非线程安全的;
PriorityQueue内部维护了几个重要属性:

transient Object[] queue; // non-private to simplify nested class access

    /**
     * The number of elements in the priority queue.
     */
    private int size = 0;

//比较器
    private final Comparator<? super E> comparator;

//被修改的次数
    transient int modCount = 0; // non-private to simplify nested class access

PriorityQueue的peek()和element()操作是常数时间,add()、offer()、 无参数的remove()以及poll()方法的时间复杂度都是log(N)。
在这里插入图片描述
PriorityBlockingQueue和PriorityQueue的实现基本一致区别就是在于加锁了,并发出了非空信号唤醒阻塞的获取线程。

1、jdk内置的优先队列PriorityQueue内部使用一个堆维护数据,每当有数据add进来或者poll出去的时候会对堆做从下往上的调整和从上往下的调整。

2、PriorityQueue不是一个线程安全的类,如果要在多线程环境下使用,可以使用 PriorityBlockingQueue 这个优先阻塞队列。其中add、poll、remove方法都使用 ReentrantLock 锁来保持同步,take() 方法中如果元素为空,则会一直保持阻塞。

与优先级队列有关的习题

[179. 最大数]

(https://leetcode-cn.com/problems/largest-number/)
给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。

**注意:**输出结果可能非常大,所以你需要返回一个字符串而不是整数。

示例 1:

输入:nums = [10,2]
输出:"210"

示例 2:

输入:nums = [3,30,34,5,9]
输出:"9534330"
public String largestNumber(int[] nums) {
PriorityQueue<String>queue = new PriorityQueue<>((x,y)->(y+x).compareTo(x+y));
for(int i:nums)
queue.offer(String.valueOf(i));
String ans="";
while(queue.size()>0)
{
ans+=queue.poll();
}
if(ans.charAt(0)=='0') return"0";
return ans;

    }

[918. 环形子数组的最大和]

(https://leetcode-cn.com/problems/maximum-sum-circular-subarray/)
首先关于一般数组的最大和:

    int maxSubArray(vector<int>& nums) {

int pre = 0;
int sum = nums[0];
for(const auto &i: nums){
    pre = max(pre+i,i);
    sum = max(pre,sum);
}
return sum;
    }

然后环形数组最大和的关键是知道最大和是怎么产生的,其实有两种情况,一个是最大和是数组中间的数字之和(从第0索引到第length-1个索引之间某几个数之和),另一个是由两端的产生,(即包含0和length-1索引,但中间有的元素不包含),第二种情况是有负数的情况,如[8,-1,-1,1],这样需要用所有元素之和减去最小子数组来求,有一种特殊情况,如果全为负数这时候所有元素之和减去最小子数组为0,这个环形数组的最大和应该是元素中最大的那个数,概况:

对于环形数组,分两种情况。
(1)答案在数组中间,就是最大子序和。例如[1,-2,3,-2];
(2)答案在数组两边,例如[5,-3,5]最大的子序和就等于数组的总和SUM-最小的子序和。(一种特殊情况是数组全为负数,也就是SUM-最小子序和==0,最大子序和等于数组中最大的那个)。

public int maxSubarraySumCircular(int[] nums) {
int premax = 0,premin = 0,maxval = nums[0],minval = nums[0],sum=0;
for(int i = 0;i < nums.length;i++)
{
    premax = Math.max(premax+nums[i],nums[i]);
    maxval = Math.max(maxval,premax);
    premin = Math.min(premin+nums[i],nums[i]);
    minval =  Math.min(minval,premin);
    sum+=nums[i];
}
return Math.max(maxval,sum-minval ==0?maxval:sum-minval);
    }

[1094. 拼车]

(https://leetcode-cn.com/problems/car-pooling/)
难度中等146

假设你是一位顺风车司机,车上最初有 capacity 个空座位可以用来载客。由于道路的限制,车 只能 向一个方向行驶(也就是说,不允许掉头或改变方向,你可以将其想象为一个向量)。

这儿有一份乘客行程计划表 trips[][],其中 trips[i] = [num_passengers, start_location, end_location] 包含了第 i 组乘客的行程信息:

  • 必须接送的乘客数量;
  • 乘客的上车地点;
  • 以及乘客的下车地点。

这些给出的地点位置是从你的 初始 出发位置向前行驶到这些地点所需的距离(它们一定在你的行驶方向上)。

请你根据给出的行程计划表和车子的座位数,来判断你的车是否可以顺利完成接送所有乘客的任务(当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true,否则请返回 false)。

示例 1:

输入:trips = [[2,1,5],[3,3,7]], capacity = 4
输出:false
示例 2:

输入:trips = [[2,1,5],[3,3,7]], capacity = 5
输出:true
示例 3:

输入:trips = [[2,1,5],[3,5,7]], capacity = 3
输出:true


首先按分别按上车地点和下车地点维护两个小顶堆。每次比较堆顶元素,并维护当前容量,若下车地点更小(或相等),先下车(当前容量增加),否则上车(当前容量减小),上车时要判断当前容量是否变成负值,若变成负值应直接返回false。
注意,因为最后一个下车地点一定大于最后一个上车地点,因此按上车地点维护的小顶堆一定先变为空。

 public boolean carPooling(int[][] trips, int capacity) {
PriorityQueue<int[]>up = new PriorityQueue<>((a,b)->a[1]-b[1]);//按上车地点进行排序
PriorityQueue<int[]>down = new PriorityQueue<>((a,b)->a[2]-b[2]);
for(int i = 0;i<trips.length;i++)
{
    up.offer(trips[i]);
    down.offer(trips[i]);
}
while(!up.isEmpty())
{
    int start = up.peek()[1];
    int end = down.peek()[2];
    if(end<=start)
    {
        int[] temp=down.poll();//下车,容量增加
        capacity+=temp[0];
    }
    else
    {
        int[] temp = up.poll();
        capacity-=temp[0];
    }
    if(capacity<0) return false;
}
return true;
    }

[264. 丑数 II]

(https://leetcode-cn.com/problems/ugly-number-ii/)
难度中等

给你一个整数 n ,请你找出并返回第 n丑数

丑数 就是只包含质因数 23 和/或 5 的正整数。

示例 1:

输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。

示例 2:

输入:n = 1
输出:1
解释:1 通常被视为丑数。

要得到从小到大的第 n个丑数,可以使用最小堆实现。

初始时堆为空。首先将最小的丑数 1加入堆。

每次取出堆顶元素 xx,则 xx 是堆中最小的丑数,由于 2x, 3x, 5x 也是丑数,因此将 2x, 3x, 5x 加入堆。

上述做法会导致堆中出现重复元素的情况。为了避免重复元素,可以使用哈希集合去重,避免相同元素多次加入堆。

在排除重复元素的情况下,第 n 次从最小堆中取出的元素即为第 n 个丑数。

 public int nthUglyNumber(int n) {
int[]ug=new int[]{2,3,5};
PriorityQueue<Long>queue = new PriorityQueue<>();
Set<Long>myset = new HashSet<>();//在添加元素的过程中可能出现非常大的数,因此要用long类型
queue.offer(1L);//1是丑数
myset.add(1L);
 int ans=1;
for(int i = 0;i<n;i++)
{
  long heap=queue.poll();
   ans = (int)heap;//强制转化为int类型
  for(int j = 0;j < ug.length;j++)
  {
      long temp = heap*ug[j];
      if(myset.add(temp))//用于去重,如果hashset可以添加成功则证明这个元素没有出现过,可以添加到堆中
      queue.offer(temp);
  }  

}
return ans;
    }
  

前k个出现频率最高的数字

   public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
        for (int num : nums) {
            occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
        }

        // int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
        PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
            public int compare(int[] m, int[] n) {
                return m[1] - n[1];//这一步比较不能写反,否则结果是错的
            }
        });
        for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
            int num = entry.getKey(), count = entry.getValue();
            if (queue.size() == k) {
                if (queue.peek()[1] < count) {
                    queue.poll();
                    queue.offer(new int[]{num, count});
                }
            } else {
                queue.offer(new int[]{num, count});
            }
        }
        int[] ret = new int[k];
        for (int i = 0; i < k; ++i) {
            ret[i] = queue.poll()[0];
        }
        return ret;
    }
}


用优先级队列合并k个有序链表

class Status implements Comparable<Status> {
        int val;
        ListNode ptr;
        public Status(int val,ListNode ptr)
        {
            this.val=val;
            this.ptr=ptr;
        }
        
        public int compareTo(Status s2)
        {
return ptr.val-s2.val;
        }
    }
    PriorityQueue<Status>queue=new PriorityQueue<Status>();
    public ListNode mergeKLists(ListNode[] lists) {
for(ListNode node:lists)
if (node != null) {
queue.offer(new Status(node.val,node));
}
ListNode head=new ListNode(0);
ListNode tail=head;
while(!queue.isEmpty())
{
    Status f=queue.poll();
    tail.next=f.ptr;
    tail=f.ptr;
    if(f.ptr.next!=null)
    queue.offer(new Status(f.ptr.next.val,f.ptr.next));//这一步比较巧妙,没有直接展开上面的所有链表结点,而是先将链表数组中的元素按照第一个结点的大小加入优先级队列,后再对每一个结点中寻找下一个结点加入队列
}
return head.next;
    }

滑动窗口的最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 。

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值

[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7


由于出现了最大值,用优先级队列可以维护每次窗口的最大值,其中用数组下标用来排除窗口之外的元素

 public int[] maxSlidingWindow(int[] nums, int k) {
int[]ans = new int[nums.length+1-k];
PriorityQueue<int[]>queue = new PriorityQueue<int[]>(new Comparator<int[]>(){
    @Override
    public int compare(int[]a1,int[]a2)
    {
        return a1[0]==a2[0]?a2[1]-a1[1]:a2[0]-a1[0];
    }
});
for(int i = 0;i <k;i++)
queue.offer(new int[]{nums[i],i});
int index = 0;
ans[0]= queue.peek()[0];
for(int i = k;i<nums.length;i++)
{
      queue.offer(new int[]{nums[i],i});
   while(queue.peek()[1] <= i-k)//注意这里是while循环不是if判断
    queue.poll();
  
   
      ans[i-k+1] = queue.peek()[0];
  
}
return ans;
    }

其他:对二维数组自定义排序

Arrays.sort(intervals,new Comparator<int[]>() {
@Override
public int compare(int[]a,int[]b)
{
    return a[0]==b[0]?b[1]-a[1]:a[0]-b[0];
} });
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值