likes
comments
collection
share

从构造方法到实战练习优先级队列 PriorityQueue

作者站长头像
站长
· 阅读数 23

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情

一、前言

之前我们有讲到栈与队列,在 java中栈的类是 Stack,队列的接口是 Queue,并且我们通过队列的实现类 LinkedList对队列的方法进行了练习,并使用通过相关的知识完成了 LeetCode 232题 怎么用栈实现队列

了解的都知道,我这么不思进取的人怎么可能突发奇想的去了解一个工作中没用过的类,不出意外的,还是在今天做 LeetCode 347. 前 K 个高频元素题的时候,尝试了几次都不知道怎么做,看评论区发现这道题实际上应该是使用 优先级队列 来做,我才知道有优先级队列这么个东西

但是优先级队列,有的编程语言中并没有,好在 java是有的,实现优先级队列的类是 PriorityQueue,顾名思义, Priority就是优先级的意思

俗话说的好,很多东西都是你用到了才会去了解,那么今天我就和大家一起把玩把玩这个优先级队列吧

二、PriorityQueue

由于我没有下载 java8的源码,进入 PriorityQueue类里面有大量的英文注释,不方便截图,所以下面我会直接放代码

什么是优先级队列

队列我们都知道,放入元素就是放在了队尾,拿取元素就是拿取队头的元素,也就是我们常说的先进先出,那么优先级队列是什么?和队列又有什么不同?能解决什么样的问题呢?

优先级队列,依旧是 顾名思义,优先级队列会自动按照元素的权值排列,也就是说,优先级队列其实有反 先进先出规则,但是其对外接口依旧是从队头取元素,队尾添加元素,再无其他存取方式,看起来就是一个队列

优先级队列其实是一个 披着队列外皮的堆

什么是堆?

堆通常可以被看成一个完全二叉树的数组对象,树中的每个节点都不小于(或不大于)其左右孩子的值

  • 如果父节点大于左右孩子的值,那么是大顶堆
  • 如果父节点小于左右孩子的值,那么是小顶堆

优先级队列的特点

  • 优先级队列里的元素必须实现内部比较器或者外部比较器
  • 优先级队列拥有小顶堆的所有特性, 默认是 小顶堆
  • 优先级队列不是线程安全的
  • 不允许使用 null元素
  • 优先级队列本身是无序的,但是取出的元素所排成的序列才是有序序列

关于特点部分来源于百度,有问题的话及时评论指正,谢谢

基本属性

// 序列化相关,可以忽略
private static final long serialVersionUID = -7720805057305804111L;
// 默认无参构造时数组的长度
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 存储元素的数组队列
transient Object[] queue;
// 
private int size = 0;
// 集合排序策略
private final Comparator<? super E> comparator;
// 
transient int modCount = 0;

构造方法

我将下面三个构造方法分为一类,最主要的第四个构造方法,其他三个是方法重载, DEFAULT_INITIAL_CAPACITY上面有说,是值默认无参构造时队列的长度 comparator是指优先级排序的规则, 默认来说优先取的是权值小的

第四个构造方法就是初始化了队列数组的长度,同时设置了比较器


public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

public PriorityQueue(int initialCapacity) {
        this(initialCapacity, null);
    }
    
public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}

public PriorityQueue(int initialCapacity,
                     Comparator<? super E> comparator) {
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

下面是对 PriorityQueue构造方法的测试,可以发现,我们调用 PriorityQueue.size()方法的时候返回的并不是我们设置的队列长度,而是元素的数量,留下一个疑问,一会我们一起看一下

从构造方法到实战练习优先级队列 PriorityQueue

下面的这几个构造方法就会稍微复杂一点了

// 包含优先级元素

public PriorityQueue(PriorityQueue<? extends E> c) {
    this.comparator = (Comparator<? super E>) c.comparator();
    initFromPriorityQueue(c);
}

下图是通过上面的构造函数进行构造的,真是有趣极了,大家可以看到,priorityQueue4是按照我们设置的规则进行了降序排列,没有问题,但是 priorityQueue5继承了 priorityQueue4的元素和比较器之后打印出来的却不完全是按照顺序打印的,后面四个元素变成了 2,1,2,1,到最后面遍历的时候却又是按照了2,2,1,1顺序打印的

从构造方法到实战练习优先级队列 PriorityQueue

创建含有指定顺序set元素的 PriorityQueue

public PriorityQueue(SortedSet<? extends E> c) {
    this.comparator = (Comparator<? super E>) c.comparator();
    initElementsFromCollection(c);
}

具体测试代码如下所示,和入参为 PriorityQueue类一样,都是继承了其类的比较器

从构造方法到实战练习优先级队列 PriorityQueue

下面这个构造方法,集成了上两个构造方法,他们最后调用的方法主要就是初始化数组,里面各种跳转,感兴趣的可以自己看一下

public PriorityQueue(Collection<? extends E> c) {
    if (c instanceof SortedSet<?>) {
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        initElementsFromCollection(ss);
    }
    else if (c instanceof PriorityQueue<?>) {
        PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        initFromPriorityQueue(pq);
    }
    else {
        this.comparator = null;
        initFromCollection(c);
    }
}

常用方法

  • add/offer: 添加元素
  • element/peek:返回元素不删除
  • remove/poll:弹出元素,并删除
  • remove:删除元素,若元素有多个则只删除一个

add, element, 无参remove()方法不建议使用,若失败会抛出异常 具体就不进行测试了,因为还是队列的那几个方法,感兴趣的可以看我之前的文章, 都有比较详细的测试 文章地址: 你有用过 java中的栈和队列吗?怎么用栈来实现队列呢

三、实践出真知,LeetCode走起

不知道你是不是和我一样,看完就会,上手就废型选手,废话不多说,上链接 347. 前 K 个高频元素

题目描述

从构造方法到实战练习优先级队列 PriorityQueue

解题思路

  • 很明显是要用优先级队列来做,而且这里需要注意的是优先级队列默认是升序排列,我们这边要给他变成降序,因为弹出的时候优先弹出次数多的
  • 首先定义一个返回值 int数组 result,长度就是 k,毕竟是让我们返回前 k的元素
  • 在创建一个 map对象,key是数字,value是出现的次数
  • 遍历 nums对象,查看 map中是否存在当前元素,存在则 value++,不存在则新增,value = 1
  • 创建优先级队列 queue
  • 获取到 map的 Map.EntrySet对象,因为我们要自定义一个降序排列的比较器
    • 这里不能直接使用 Collections.reverseOrder(),因为我们是对 map中的 value,当前数字出现的次数进行比较
  • 遍历 entries,然后将获取到的 map加入到 queue队列中
  • 之后直接遍历队列,因为我们是优先级队列,而且设置了比较器,所以直接 queue.poll()方法就可以,会弹出当前队列最大的元素,且在队列中删除该元素
    • 终止条件是 k,因为题中要求的就是返回长度为 k的数组,多了该越界了

代码展示

public static int[] topKFrequent(int[] nums, int k) {
    int[] result = new int[k];
    // 优先级队列元素
    Map<Integer,Integer> map = new HashMap<>();
    // 遍历 nums获取元素及其出现次数
    for (int i = 0; i < nums.length; i++) {
        int num = nums[i];
        map.put(num, map.getOrDefault(num, 0 ) + 1);
    }
    Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
    PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o2.getValue() - o1.getValue());
    for (Map.Entry<Integer, Integer> entry : entries) {
        queue.add(entry);
    }
    for (int i = 0; i < k; i++) {
        result[i] = queue.poll().getKey();
    }
    return result;
}

提交结果

从构造方法到实战练习优先级队列 PriorityQueue

四、总结

很多东西都是你用到了才会去了解,如果可以的话,还是提前了解的比较好,防止出现像我一样的情况:等到需要了我却不知道

那句话怎么说的来着,书到用时方恨少,加油

本文内容到此结束了

如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。

如有错误❌疑问💬欢迎各位大佬指出。

我是 宁轩 , 我们下次再见