文章目录
Java集合中PriorityQueue 优先队列类
1、PriorityQueue类的位置?
我想这个问题就是在讲PriorityQueue 继承了谁?它的父类特点是什么?
在官方的API文档中,我们可以清晰的看到这个结构
我们可以知道,PriorityQueue类继承了AbstractQueue类,AbstractQueue类继承了AbstractCollection
接下来,看看这张Java集合框架图。
我们可以看出这个方法实现了Queue接口,同时这个Queue接口继承了Collection接口,所以PriorityQueue类中实现了很多Queue接口的方法与特点。
Queue接口的方法的特点:
Queue接口设计用于在处理之前保留元素的集合。 除了基本的Collection
之外,队列还提供额外的插入,提取和检查操作。
2、PriorityQueue类有什么方法?
2.1 PriorityQueue是什么?
优先队列的作用是能保证每次取出的元素都是队列中权值最小的 ,该类是基于数组表示的二叉小顶堆实现的。 优先级队列的元素根据它们的默认有序规则,或由一个在队列构造的时候使用的构造方法提供的Comparator
进行排序存放。 优先队列不允许null
元素。 依靠默认排序的优先级队列也不允许插入不可比较的对象(这样做可能导致ClassCastException
)。
现在我们以5~11来举例子,数值在二叉小顶堆中排序,如下图
他在数组的存放就是如下图所示:
我们可以看到 8的父节点是6、10的父节点是7,我们可以得到父子节点之间的关系公式为:
左孩子:leftNo = parentNo*2+1
右孩子:rightNo = parentNo*2+2
父节点:parentNo = (nodeNo-1)/2
所以在添加新节点的时候,我们是根据以上三个公式进行位置的更替与排序,以上便是PriorityQueue类的底层数据结构。
2.2 PriorityQueue的方法
首先简单地列出其方法,用思维导图展示,如下图所示。
2.2.1 构造方法
1、基础构造方法
1.1 PriorityQueue()
创建一个PriorityQueue
,具有默认的初始容量(11),根据它们的默认排序方法对其元素进行排序。
源码如下:
//默认容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
public PriorityQueue() {
//传入了容量和Null,不指定具体的排序方法,即成为默认排序方法
this(DEFAULT_INITIAL_CAPACITY, null);
//这里调用了默认构造器
//public PriorityQueue(int initialCapacity,
// Comparator<? super E> comparator)
}
1.2PriorityQueue(int initialCapacity)
创建具有PriorityQueue
,具有指定容量initialCapacity
,根据它们的默认排序方法对其元素进行排序 。
源码如下:
public PriorityQueue(int initialCapacity) {
//这里也是调用了默认构造器
this(initialCapacity, null);
}
2.自定义排序规则的构造器
2.1 PriorityQueue(int initialCapacity,Comparator<? super E> comparator)
创建具有 initialCapacity
初始容量的PriorityQueue,根据指定的比较器对其元素进行排序。
值得一提的是:这个构造器是以上两个构造依赖的,通过this()进行调用和参数传递。
源码如下:
//transient关键字是用来指定不需要被序列化 ,定义的queue数组结构是用来实现二叉最小堆的,可以参考之前的分析。
transient Object[] queue;
//定义比较器
private final Comparator<? super E> comparator;
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
//定义的容量不能小于1
if (initialCapacity < 1)
throw new IllegalArgumentException();
//给queue数组定义容量大小
this.queue = new Object[initialCapacity];
//给比较器赋值
this.comparator = comparator;
}
2.2.PriorityQueue(Comparator<? super E> comparator)
创建具有默认初始容量为11的 PriorityQueue
,并根据指定的比较器对其元素进行排序。
源码如下:
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
3、从其他导入比较器
3.1 PriorityQueue(Collection<? extends E> c)
创建一个Collection集合中的元素的PriorityQueue。 如果指定的集合是一个实例SortedSet或者是另一种PriorityQueue ,这个优先级队列将按照相同的顺序进行排序。 否则,此优先级队列将根据其元素的默认顺序进行排序。
源码:
public PriorityQueue(Collection<? extends E> c) {
//判断c是否为SortedSet的一个实例
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
//给构造器添上具体的方法
this.comparator = (Comparator<? super E>) ss.comparator();
//给PriorityQueue 实例加上ss的数值
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);
}
}
你可以看下面的示例,可以帮你好的理解:
@Test
public void PriorityQueueConstructor(){
//SortedSet集合的一个实例 ,这里规定的排序为从大到小
SortedSet<Integer> sortedSet=new TreeSet<>((a,b)->(b-a) );
sortedSet.add(11);
sortedSet.add(56);
sortedSet.add(9);
sortedSet.add(87);
System.out.println(sortedSet.toString());//[87, 56, 11, 9]
//使用构造器,按照SortedSet集合实例中的构造器对里面的元素进行排序
PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(sortedSet);
System.out.println(priorityQueue.poll()); //87
System.out.println(priorityQueue.poll());//56
System.out.println(priorityQueue.poll());//11
System.out.println(priorityQueue.poll());//9
System.out.println(priorityQueue.poll());//null
//PriorityQueue集合的一个实例,这里规定排序为从到小
PriorityQueue<Integer> priorityQueue1=new PriorityQueue<>((a,b)->(b-a));
priorityQueue1.add(5);
priorityQueue1.add(4);
priorityQueue1.add(9);
priorityQueue1.add(1);
//注意这里输出的是[9, 4, 5, 1],可以思考下为什么是这个
System.out.println(priorityQueue1.toString());
///使用构造器,按照PriorityQueue集合实例中的构造器对里面的元素进行排序
PriorityQueue<Integer> pq=new PriorityQueue<>(priorityQueue1);
System.out.println(pq.poll());//9
System.out.println(pq.poll());//5
System.out.println(pq.poll());//4
System.out.println(pq.poll());//1
System.out.println(pq.poll());//null
//定义ArrayList实例
Collection<Integer> collection= new ArrayList<>();
collection.add(10);
collection.add(34);
collection.add(11);
collection.add(1);
System.out.println(collection.toString());//[10, 34, 11, 1]
//使用该构造器,并按照默认排序的方式对数值从小到大进行排序
PriorityQueue<Integer> pq1=new PriorityQueue<>(collection);
System.out.println(pq1.poll());//1
System.out.println(pq1.poll());//10
System.out.println(pq1.poll());//11
System.out.println(pq1.poll());//34
System.out.println(pq1.poll());//null
}
**思考题:**在上面例子中按顺序添加[5,4,9,1] 时,输出的数组顺序为什么是[9, 4, 5, 1]?
所以在数组中的顺序变为:9、4、5、1
3.2public PriorityQueue(SortedSet<? extends E> c)
用于创建一个SortedSet
指定排序集中的元素的PriorityQueue。 该优先级队列将按照与给定排序集相同的顺序进行排序。
源码:
public PriorityQueue(PriorityQueue<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initFromPriorityQueue(c);
}
3.3 public PriorityQueue(PriorityQueue<? extends E> c)
用于创建一个PriorityQueue
指定排序集合中的元素的PriorityQueue。 该优先级队列将按照与给定排序集相同的顺序进行排序。
源码:
public PriorityQueue(PriorityQueue<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initFromPriorityQueue(c);
}
2.2.2 主要方法
1、插入
-
add(E e)和offer(E e)
在PriorityQueue类中没有任何区别,插入成功返回true,如果插入的为null,则返回异常。
add(E e)
和offer(E e)
的语义相同,都是向优先队列中插入元素,只是Queue
接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回false
。可以看下源码,我们就知道add()方法其实是调用的offer方法,故两者都没区别。
public boolean add(E e) { return offer(e); }
public boolean offer(E e) { //元素为null时会抛出异常 if (e == null) throw new NullPointerException(); //队列中元素数量加1 modCount++; int i = size; if (i >= queue.length) //自动扩容 grow(i + 1); size = i + 1; //第一个元素直接添加 if (i == 0) queue[0] = e; else //其他元素要进行比较,进行排序,这比较重要拿出来看 siftUp(i, e); return true; }
siftUp(i, e)
方法源码:private void siftUp(int k, E x) { //有指定比较器的情况下 if (comparator != null) siftUpUsingComparator(k, x); else //没有比较器,默认优先级情况下 siftUpComparable(k, x); }
siftUpUsingComparator(k, x)
的源码,告诉了我们优先级队列是如何对插入数据进行比较的,如下:private void siftUpComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>) x; //限定范围 while (k > 0) { //父节点 int parent = (k - 1) >>> 1; Object e = queue[parent]; //元素与其父节点进行比较,满足条件退回 if (key.compareTo((E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = key; }
我们可以得到在插入数据的时候,要与其父节点进行比较,如果不满足比较器的关系,则交换数据,这个就是我们之前思考题里回答的问题。
2、查询
2.1 查询(首元素)后不删除元素
-
E peek()与E element()
说明:两者都是查询首元素后不删出元素,不同的是peek()在查不到元素后会返回null,而element()会抛出异常。
注意:element()方法并不在PriorityQueue类中定义,而是从父类AbstractQueue类中继承而来。
看他们的源码也很有意思,element也是peek()方法进行了封装。
在AbstractQueue类中的element()方法的源码:
public E element() { //调用peek()进行判断 E x = peek(); if (x != null) return x; else throw new NoSuchElementException(); }
peek()源码,如下:
public E peek() { return (size == 0) ? null : (E) queue[0]; }
2.2 查询任意元素是否存在,不删除,contains(Object o)
方法源码如下:
public boolean contains(Object o) {
return indexOf(o) != -1;
}
如果存在就返回该元素索引,如果不存就返回-1。indexOf
函数也是用的循环,可见对数组的查询,基本就是遍历。
private int indexOf(Object o) {
if (o != null) {
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
return -1;
}
2.3 查询首元素后,删除。
remove()
和poll()
方法的语义也完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null
。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。
- poll()源码分析:
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];//取出最后一个元素
queue[s] = null;//最后元素置为null
if (s != 0)
siftDown(0, x);//调整元素位置
return result;
}
siftDown()
分析
从图中,我们可以看到,最后一位元素拿到首位置后,从头向下依次与其左子树和右子树进行比较,直到找到合适的位置。
- remove
对remove()方法分析,
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
3、删除
3.1 remove(Object o)
remove(Object o)
方法用于删除队列中跟o
相等的某一个元素(如果有多个相等,只删除一个),该方法不是Queue接口内的方法,而是Collection接口的方法。由于删除操作会改变队列结构,所以要进行调整;又由于删除元素的位置可能是任意的,所以调整过程比其它函数稍加繁琐。
具体来说,remove(Object o)
可以分为2种情况:
-
删除的是最后一个元素。直接删除即可,不需要调整。
-
删除的不是最后一个元素,从删除点开始以最后一个元素为参照调用一次
siftDown()
即可。此处不再赘述。
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
private E removeAt(int i) {
// assert i >= 0 && i < size;
modCount++;
int s = --size;
if (s == i) // removed last element
queue[i] = null;
else {
E moved = (E) queue[s];
queue[s] = null;
//调整顺序
siftDown(i, moved);
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
3.2 clear()全部清除
public void clear() {
//记录队列操作的次数
modCount++;
for (int i = 0; i < size; i++)
queue[i] = null;
size = 0;
}
3、简单介绍Queue接口
3.1 Queue接口中的方法有哪些独特的?
这里简单介绍下Queue接口的一些方法,能够让我们很好地理解PriorityQueue类中的方法
我们可以看到每种操作下都有两个方法,这里对讲出他们之间的区别:
-
插入方法
- add方法:将指定的元素插入到此队列中,如果不违反容量限制,可以立即执行此操作,并且在成功后返回
true
, 如果当前没有可用空间, 则IllegalStateException
。 - offer方法:如果在不违反容量限制的情况下,会立即执行,并将指定的元素插入到此队列中,执行成功后返回
true
。 当使用容量限制队列时,将无法执行,并返回false
。 - 两者区别:对于插入元素失败的情况,处理方式不一样,add会返回异常,offer会返回false。
- add方法:将指定的元素插入到此队列中,如果不违反容量限制,可以立即执行此操作,并且在成功后返回
-
提取方法
- remove方法:检索并删除此队列的头。 如果此队列为空,它将抛出异常。
- poll方法:检索并删除此队列的头,如果此队列为空,则返回
null
。 - 两者区别:队列为空的时候,remove抛出异常,poll返回null。
-
查询方法
- element方法:查询队列中首元素,但不删除,查询到了返回元素,如果队列为空则返回异常。
- peek方法:查询队列中首元素,但不删除,查询到了返回元素,如果队列为空则返回
null
。 - 两者区别: 查询不到元素的时候,element返回异常,peek返回null。
2.2 为什么介绍Queue接口?
介绍了那么多Queue接口的方法,这跟PriorityQueue 有什么关系么?
通过了解父类和接口的一些方法,我们能更好地了解这个类的一些性质。
在第一部分讲了PriorityQueue的继承,它的父类AbstractQueue类,继承了AbstractCollection类同时也实现了Queue接口,因而PriorityQueue 类拥有着Queue接口的方法(当然也有Collection接口)。
public abstract class AbstractQueue<E>
extends AbstractCollection<E>
implements Queue<E>
当然我们知道在java中,子类继承父类之后,便拥有三个最重要的特点:
- 子类拥有父类非 private 的属性、方法。
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
父类AbstractQueue类的方法
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public void clear() {
while (poll() != null)
;
}
public boolean addAll(Collection<? extends E> c) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
4、 来看Leetcode中一道题
1、题目23
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例2:
输入:lists = []
输出:[]
示例3:
输入:lists = [[]]
输出:[]
2、解答
/**
* Definition for singly-linked list.
* 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; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length==0) return null;
ListNode mylist=new ListNode(-1);
ListNode p =mylist;
//这里用到了PriorityQueue类
PriorityQueue<ListNode> priorityQueue =new PriorityQueue<>(lists.length,(a,b)->(a.val-b.val));
for(ListNode head:lists){
if(head!=null){
//这里添加
priorityQueue.add(head);
}
}
while(!priorityQueue.isEmpty()){
//弹出首元素
ListNode p1=priorityQueue.poll();
p.next=p1;
if(p1.next!=null){
priorityQueue.add(p1.next);
}
p=p.next;
}
return mylist.next;
}
}
这个用到了PriorityQueue类,可以用来稳固下之前学习的内容。
参考
https://docs.oracle.com/javase/8/docs/api/ 官方文档
https://www.cnblogs.com/CarpenterLee/p/5488070.html 一个大佬写的,很不错。我这里有很多用的他写的内容