JAVA优先级队列详解

基本使用

默认:最小堆,每次可获得最小元素

优先队列按照其作用不同,可以分为以下两种:

  • 最大优先队列: 可以获取并删除队列中最大的值
  • 最小优先队列: 可以获取并删除队列中最小的值
将元素放入队列:add,offer
将队首元素从队列删除:remove,poll
查看队列内的对首元素:element,peek

和标准队列不同的是,当删除队首元素的时候,删除的是priority queue中最小的元素。但是,priority queue并不是对所有的元素排序,其内部是用heap(堆)实现的。堆是一个自组织的二叉树,在这个二叉树里,addremove操作使得最小的元素“吸引”到二叉树的根部,而不用在排列整个队列上耗费时间。

PriorityQueue<Integer> heap = new PriorityQueue<>(
                (n1,n2) -> n1-n2  //这一句加不加结果是一样的
        );
        heap.add(4);
        heap.add(1);
        heap.add(2);
        heap.add(3);
        while(!heap.isEmpty())
        {
            System.out.println(heap.poll());
        }

输出顺序是1,2,3,4。
可以看到默认的比较规则就是n1-n2,如果我们改成n2-n1

PriorityQueue<Integer> heap = new PriorityQueue<>(
	(n1,n2) -> n2-n1 //这一句加不加结果是一样的
);

输出顺序就是4,3,2,1。

可以想见优先队列内部是按照比较器返回的结果进行处理的,一般认为a,b比较结果大于0,就是a大于b,等于0就是a等于b;小于0就是a小于b。
现在我们知道当比较n1,n2的时候,如果结果大于0(也就是默认的n1-n2>0),那么要把n1下沉(默认的小根堆),当我们改成n2-n1的时候,结果大于0,证明n2大于n1,但还是n1(值较小的元素)下沉,于是这个堆就成了大根堆!

单元素优先级队列

就如上面的基本用法,下面换一个比较字符的例子:

将字母分为三个等级输出

输入一个字符串,高级为"bdfhkl",中级为"aceimnorstuvwxz",低级为"gjqpy",请将字符串按字母等级分割为3个字符串,每个字符串内按字典序输出。如果字符串为空输出null。

import java.util.*;

public class demo5 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String s = scanner.next();
        PriorityQueue<Character> q1 = new PriorityQueue<>();
        PriorityQueue<Character> q2 = new PriorityQueue<>();
        PriorityQueue<Character> q3 = new PriorityQueue<>();
        for (int i = 0; i < s.length(); i++) {
            switch (fun(s.charAt(i))) {
                case 1 :
                    q1.offer(s.charAt(i));
                    break;
                case 2 :
                    q2.offer(s.charAt(i));
                    break;
                case 3 :
                    q3.offer(s.charAt(i));
                    break;
                default:
                    break;
            }
        }
        if (q1.size() != 0) {
            while (q1.size() != 0) {
                System.out.print(q1.poll());
            }
            System.out.println();
        } else {
            System.out.println("null");
        }
        if (q2.size() != 0) {
            while (q2.size() != 0) {
                System.out.print(q2.poll());
            }
            System.out.println();
        } else {
            System.out.println("null");
        }
        if (q3.size() != 0) {
            while (q3.size() != 0) {
                System.out.print(q3.poll());
            }
            System.out.println();
        } else {
            System.out.println("null");
        }
    }
    static int fun(char c) {
        switch (c) {
            case 'b' :
            case 'h':
            case 'f':
            case 'k':
            case 'l':
            case 'd':
                return 1;
            case 'g':
            case 'j':
            case 'p':
            case 'q':
            case 'y':
                return 3;
            default:
                return 2;
        }
    }
}

再举一个链表节点比较的例子:

基本写法:

就像TreeSet一样,一个priority queue要么存储的是实现了Comparable接口的元素,要么在构造函数中传入一个Comparator对象。

	public class ListNode {
		int val;
        ListNode next;
        ListNode() {}
        ListNode(int val) { this.val = val; }
        ListNode(int val, ListNode next) { this.val = val; this.next = next; }
    }
    
	//自定义比较类,升序排列
    static Comparator<ListNode> cLNode = new Comparator<ListNode>() {
        public int compare(ListNode o1, ListNode o2) {
                return o1.val-o2.val;
        }
    };
    
    public static void main(String[] args) {
    	//自定义类的优先队列需要重写比较类作为传入参数
        Queue<ListNode> que = new PriorityQueue<>(cLNode);   
        //简单写法
        // Queue<ListNode> que = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
    }

合并K个有序链表

把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点。

优先队列 pq 中的元素个数最多是 k,所以一次 poll 或者 add 方法的时间复杂度是 O(logk);所有的链表节点都会被加入和弹出 pq所以算法整体的时间复杂度是 O(Nlogk),其中 k 是链表的条数,N 是这些链表的节点总数

ListNode mergeKLists(ListNode[] lists) {
    if (lists.length == 0) return null;
    // 虚拟头结点
    ListNode dummy = new ListNode(-1);
    ListNode p = dummy;
    // 优先级队列,最小堆
    PriorityQueue<ListNode> pq = new PriorityQueue<>(
        lists.length, (a, b)->(a.val - b.val));
    // 将 k 个链表的头结点加入最小堆
    for (ListNode head : lists) {
        if (head != null)
            pq.add(head);
    }

    while (!pq.isEmpty()) {
        // 获取最小节点,接到结果链表中
        ListNode node = pq.poll();
        p.next = node;
        if (node.next != null) {
            pq.add(node.next);
        }
        // p 指针不断前进
        p = p.next;
    }
    return dummy.next;
}
class Solution {
    class Status implements Comparable<Status> {
        int val;
        ListNode ptr;

        Status(int val, ListNode ptr) {
            this.val = val;
            this.ptr = ptr;
        }

        public int compareTo(Status status2) {
            return this.val - status2.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 = tail.next;
            if (f.ptr.next != null) {
                queue.offer(new Status(f.ptr.next.val, f.ptr.next));
            }
        }
        return head.next;
    }
}

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
import java.util.*;
public class Solution {
    public ListNode mergeKLists(ArrayList<ListNode> lists) {
        if(lists.size() == 0){
            return null;
        }
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        PriorityQueue<ListNode> pq = new PriorityQueue<ListNode>(lists.size(), (a, b) -> (a.val - b.val));
        for(ListNode x : lists){
            if(x != null){
                pq.offer(x);
            }
        }
        while(!pq.isEmpty()){
            ListNode node = pq.poll();
            p.next = node;
            if(node.next != null){
                pq.offer(node.next);
            }
            p = p.next;
        }
        return dummy.next;
    }
}

双元素优先队列

PriorityQueue<int[]> heap = new PriorityQueue<>(
                (pair1,pair2) -> pair1[0]-pair2[0]
        );
        heap.add(new int[]{1,9});
        heap.add(new int[]{2,8});
        heap.add(new int[]{3,7});
        while(!heap.isEmpty())
        {
            int[] cur = heap.poll();
            System.out.println(cur[0] + "," + cur[1]);
        }

这里我们必须是要设置比较规则的,因为默认里面没有比较数组的规则。这里我们设置第一个元组的第一个数减去第二个元组的第一个数。
按照前面设想的逻辑,此时如果结果大于0,证明pair2的第一个数比pair1的第一个数大,而pair1下沉,说明此时是一个以元组的第一个数为比较依据的大根堆。

现在我们可以自由地依照我们的想法来设置排序规则了,比如我现在想要以元组的和为比较依据,并且是小根堆。根据猜想,我们需要让大的值下沉,也就是比较结果大于0的时候,应该是第一个元组是大值。

        PriorityQueue<int[]> heap = new PriorityQueue<>(
                (pair1,pair2) -> pair1[0]+pair1[1]-pair2[0]-pair2[1]
        );
        heap.add(new int[]{1,9});
        heap.add(new int[]{2,6});
        heap.add(new int[]{3,4});
        while(!heap.isEmpty())
        {
            int[] cur = heap.poll();
            System.out.println(cur[0] + "," + cur[1]);
        }

这里结合TreeSet使用一下,类似的道理:

import java.io.IOException;

import java.util.*;

public class demo02 {
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        scanner.nextLine();
        int[] height = new int[n];//身高
        String[] name = new String[n];//姓名
        for(int i = 0 ; i < n; i++){
            height[i] = scanner.nextInt();
        }
        scanner.nextLine();
        for(int i = 0 ; i < n; i++){
            name[i] = String.valueOf(scanner.next());
        }
        TreeMap<Integer, PriorityQueue<String >> treeMap = new TreeMap<>();
        for(int i = 0; i < n; i++){
            if(treeMap.containsKey(height[i])){
                treeMap.get(height[i]).add(name[i]);
            }else{
                PriorityQueue<String > queue = new PriorityQueue<>();
                treeMap.put(height[i], queue);
                queue.add(name[i]);
            }
        }
        List<String> list = new ArrayList<>();
        PriorityQueue<String > queue = new PriorityQueue<>();
        while(!treeMap.isEmpty()){
            queue = treeMap.pollFirstEntry().getValue();
            while(!queue.isEmpty()){
                list.add(queue.poll());
            }
        }
        for(int i = 0; i < n; i++){
            System.out.print(list.get(i) + " ");
        }
        System.out.println();
    }
}

常用的场景——调度

单线程CPU

这个面试时候遇到过,现场手撕

1834. 单线程 CPU - 力扣(LeetCode)

在这里插入图片描述

这题的难度不算大,就是有些复杂,难点在于你要同时控制三个变量(开始时间、处理时间、索引)的有序性,而且这三个变量还有优先级

  • 首先应该考虑开始时间,因为只要到了开始时间,任务才进入可执行状态;
  • 其次应该考虑任务的处理时间,在所有可以执行的任务中优先选择处理时间最短的;
  • 如果存在处理时间相同的任务,那么优先选择索引最小的。

所以这道题的思路是:

先根据任务「开始时间」排序,维护一个时间线变量 now 来判断哪些任务到了可执行状态,然后借助一个优先级队列 pq 对「处理时间」和「索引」进行动态排序

利用优先级队列动态排序是有必要的,因为每完成一个任务,时间线 now 就要更新,进而产生新的可执行任务。

import java.util.*;

public class demo6 {
    public static void main(String[] args) {
        int[][] tasks = {{1, 2}, {2, 4}, {3, 2}, {4, 1}};
        int[] res = getOrder(tasks);
        System.out.print(Arrays.toString(res));
        // 输出[0, 2, 3, 1]
    }
    static int[]  getOrder(int[][] tasks) {
        int n = tasks.length;
        // 把原始索引也添加上,方便后面排序用
        ArrayList<int[]> triples = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            triples.add(new int[]{tasks[i][0], tasks[i][1], i});
        }
        // 数组先按照任务的开始时间排序
        triples.sort(Comparator.comparingInt(a -> a[0]));
        //上面一行等价于
        //    triples.sort((a, b) -> {
        //        return a[0] - b[0];
        //    });
        // 按照任务的处理时间排序,如果处理时间相同,按照原始索引排序
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> {
            if (a[1] != b[1]) {
                // 比较处理时间
                return a[1] - b[1];
            }
            // 比较原始索引
            return a[2] - b[2];
        });
        ArrayList<Integer> res = new ArrayList<>();
        // 记录完成任务的时间线
        int now = 0;
        int i = 0;
        while (res.size() < n) {
            if (!pq.isEmpty()) {
                // 完成队列中的一个任务
                int[] triple = pq.poll();
                res.add(triple[2]);
                // 每完成一个任务,就要推进时间线
                now += triple[1];
            } else if (i < n && triples.get(i)[0] > now) {
                // 队列为空可能因为还没到开始时间,
                // 直接把时间线推进到最近任务的开始时间
                now = triples.get(i)[0];
            }
            // 由于时间线的推进,会产生可以开始执行的任务
            for (; i < n && triples.get(i)[0] <= now; i++) {
                pq.offer(triples.get(i));
            }
        }
        // Java 语言特性,将 List 转化成 int[] 格式
        int[] arr = new int[n];
        for (int j = 0; j < n; j++) {
            arr[j] = res.get(j);
        }
        return arr;
    }
}

Arrays.sort()

在Arrays.sort中也可以自定义排序规则,试验后发现是一样的譬如:

int[][] arr = {{1,9},{2,8},{3,7}};
Arrays.sort(arr,(a,b)->b[0]-a[0]);

当结果大于0的时候,是a向后,也就是默认的是升序,这里的代码的含义是以元组的第一个值为依据,降序排列。
需要注意的是如果是一维数组,是无法自定义这样的规则的,如果想换成降序,最简单的办法就是翻转:

Arrays.sort(arr,Collections.reverseOrder());

注意这里的数组不能是int[],需要是Integer[]


今日推歌

----《还是爱着》 王铮亮

我们慢慢将对方活成 相互的生命
这样跟你 牵绊地 成一体
还是爱着你 却少了说爱你
平淡岁月里 总抹掉些言语
而热烈退去 活在平常里
越难舍难离
还是爱着你 最亲最近的你
好几程风雨 越吵闹越相依
手心连着你 脉搏跳动的呼吸

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星回昭以烂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值