暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PriorityQueue源码解析

Coding的哔哔叨叨 2021-01-27
519

在之前我们做堆排数据流的中位数(LeetCode-295)的题目时,用到大顶堆、小顶堆等相关知识点,尤其是在数据流的中位数我们用到了PriorityQueue,今天这篇文章我们就PriorityQueue展开做一个详细的解析。

PriorityQueue介绍

Coding的哔哔叨叨

队列是遵循先进先出(First-In-First-Out)模式的,但有时需要在队列中基于优先级处理对象。
PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。
优先队列不允许空值,而且不支持non-comparable(不可比较)的对象,比如用户自定义的类。优先队列要求使用Java Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。
优先队列的头是基于自然排序或者Comparator排序的最小元素。如果有多个对象拥有同样的排序,那么就可能随机地取其中任意一个。当我们获取队列时,返回队列的头对象。
优先队列的大小是不受限制的,但在创建时可以指定初始大小。当我们向优先队列增加元素的时候,队列大小会自动增加。
PriorityQueue是非线程安全的,所以Java提供了PriorityBlockingQueue。
Java中ProirityQueue通过二叉小顶堆实现,可以用一颗完全二叉树表示(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue底层实现。

上图中,我们对每个元素进行了编号,认真观察可以 发现,父节点和子节点的编号存在一定关系,具体关系如下:

  • leftNo = parentNo*2+1
  • rightNo = parentNo*2+2
  • parentNo = (nodeNo-1)/2

通过上面三个公式,可以很轻易计算出某个节点的父节点及子节点在数组中的下标,这也是为什么可以用数组来存储堆的原因。

PriorityQueue重点方法

Coding的哔哔叨叨

PriorityQueuepeek()和element()操作是常数时间,add()offer(),无参数的remove()以及poll()方法的时间复杂度都是log(N)。

  • add()和offer()

add(E e)和offer(E e)两个方法语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后者会返回false。对于PriorityQueue这两个方法其实没什么差别。

新加入的元素e可能会破坏小顶堆的性质,因此需要进行必要的调整,调整的方法根据是否传入comparator有不同的实现,如下源码截图。

调整的过程为:从指定的位置k开始,将e逐层与当前点的parent进行比较并交换,直到满足e >= queue[parent]为止。注意这里的比较可以是元素的自然顺序,也可以是依靠比较器的顺序。
  • element()和peek()

element()和peek()的语义完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回null;而且java8以后element()方法已经去掉了。
根据小顶堆的性质,堆顶那个元素就是全局最小的那个;由于堆用数组表示,根据下标关系,0下标处的那个元素既是堆顶元素。所以直接返回数组0下标处的那个元素即可,源码中也是这么实现的。
  • remove()和poll()

remove()和poll()方法的语义也完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null;而且java8以后remove()方法已经去掉了

由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。

根据源码:poll()方法首先记录0下标处的元素,并用最后一个元素替换0下标位置的元素,之后调用siftDown()方法对堆进行调整,最后返回原来0下标处的那个元素(也就是最小的那个元素)。重点是siftDown(int k, E x)方法,该方法的作用是从指定的位置k开始,将x逐层向下与当前点的左右孩子中较小的那个交换,直到x小于或等于左右孩子中的任何一个为止。
  • remove(Object o)

remove(Object o)方法用于删除队列中跟o相等的某一个元素(如果有多个相等,只删除一个),该方法不是Queue接口内的方法,而是Collection接口的方法。由于删除操作会改变队列结构,所以要进行调整;又由于删除元素的位置可能是任意的,所以调整过程比其它函数稍加繁琐。具体来说,remove(Object o)可以分为2种情况:1. 删除的是最后一个元素。直接删除即可,不需要调整。2. 删除的不是最后一个元素,从删除点开始以最后一个元素为参照调用一次siftDown()即可。此处不再赘述。

以上就是今天的全部内容,希望大家可以通过阅读源码有所收益。

不积跬步,无以至千里。

文章有帮助的话,点个转发、在看呗

谢谢支持哟 (*^__^*)

END


👇




文章转载自Coding的哔哔叨叨,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论