Java中PriorityQueue底层通过二叉小顶堆实现,可以用一棵完全二叉树表示。
优先队列的作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列默认每次取最小元素,C++的优先队列默认每次取最大元素)。这里牵涉到了大小关系,元素大小的评判可以通过元素本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator,类似于C++的仿函数)。
如果排列的元素不是基本类型,而是自定义的对象(比如学生对象,按年龄排序,节点类型,按节点值排序),这种就需要重写比较器Comparator。或者想要每次从队头取出的都是最大的那个元素,也需要在创建PriorityQueue对象的时候重写作为参数的比较器Comparator。
第一种是容易理解的,比如对链表节点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);
原题链接
思路:
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);
*/
原题链接
只要维护一个大小为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();
}
}
原题链接
这题与前一题是差不多的思路,只是要求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);
*/
原题链接
用一个大顶堆和一个小顶堆维护一堆数据的中位数。
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();
*/
原题链接
2023.06.10 二刷
代码如下:
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
/**PriorityQueue priorityQueue=new PriorityQueue<>(new Comparator(){
//因为需要对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;
}
}
原题链接
思路见代码中注释。
代码如下:
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
//需要被安排的任务一定是进入序列的时间小于等于当前时间线的
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;
}
}
原题链接
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;
}
}