Java 集合深入理解 :优先队列(PriorityQueue)之源码解读,及最小顶堆实现研究
Posted 踩踩踩从踩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合深入理解 :优先队列(PriorityQueue)之源码解读,及最小顶堆实现研究相关的知识,希望对你有一定的参考价值。
文章目录
前言
queue 接口
队列就是一个先入先出(FIFO)的数据结构
AbstractQueue类
也是继承自AbstractCollection 并实现queue接口的,在集合基础增加一些队列自带的方法 的基础实现
PriorityQueue类
继承AbstractQueue ,用object数组存储数组,通过二叉小顶堆实现,用一棵完全二叉树表示。(对于平常我们研究的树,大部分都是链表实现的,对于堆实现还是很有研究的意义)
这种实现:
它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()或poll()方法,返回的总是优先级最高的元素。而对于什么是优先级最高的元素,在数组中看起来不是最顺序的。
先写一个例子
public static void main(String[] args) {
PriorityQueue<Integer> q = new PriorityQueue<>();
q.offer(3);
q.offer(1);
q.offer(2);
System.out.println(q.poll()); // 1
System.out.println(q.poll()); // 2
System.out.println(q.poll()); // 3
System.out.println(q.poll()); // null,因为队列为空
}
看例子 默认会将integer在offer的时候就进行排序,自动插入到合适的位置保证队列有序。
PriorityQueue 的注释
/**
*基于优先级堆的无限优先级{@linkplain Queue}。
*优先级队列的元素根据它们的优先级排序
*{@linkplain Comparable natural ordering},或通过{@link Comparator}
*在队列构造时提供,具体取决于所使用的构造函数用过的。优先级队列不允许{@code null}元素。
*依赖于自然排序的优先级队列也不允许插入不可比较的对象(这样做可能会导致
*{@code ClassCastException})。
*<p>这个队列的<em>头是<em>最少的<em>元素
*关于指定的顺序。如果有多个元素为了最小的价值,头部是其中的一个元素——领带是
*任意破坏。队列检索操作{@code poll},{@code remove}、{@code peek}和{@code element}访问
*位于队列头部的元素。
*<p>优先级队列是无界的,但具有内部
*<i>容量</i>控制用于存储队列中的元素。它总是至少和队列一样大
*大小。当元素添加到优先级队列时,它的容量自动增长。增长政策的细节并不清楚指定。
*<p>这个类及其迭代器实现了
*<em>可选的{@link集合}和{@link集合的方法迭代器}接口。方法{@link中提供的迭代器
*#iterator()}不能保证遍历按任何特定顺序排列的优先级队列。如果您需要订购
*遍历时,请考虑使用{@code Arrays.sort(pq.toArray())}。
*<p><strong>请注意,此实现不同步。</strong>多个线程不应访问{@code PriorityQueue}
*如果任何线程修改队列,则同时执行。相反,使用线程安全的{@link
*java.util.concurrent.PriorityBlockingQueue}类。
*<p>实施说明:此实施提供
*O(log(n))排队和出列方法的时间
*({@code offer}、{@code poll}、{@code remove()}和{@code add});
*{@code remove(Object)}和{@code contains(Object)}的线性时间
*方法;检索方法的时间常数({@code peek}、{@code element}和{@code size})。
* <p>This class is a member of the
* <a href="{@docRoot}/../technotes/guides/collections/index.html">
* Java Collections Framework</a>.
*
* @since 1.5
* @author Josh Bloch, Doug Lea
* @param <E> the type of elements held in this collection
*/
总结注释上面的意思 Java1.5中引入的
1.基于优先级堆进行排序
2.优先级队列不允许{@code null}元素 依赖于自然排序的优先级队列也不允许插入不可比较的对象 就可能导致ClassCastException异常
3.优先级队列是无界的,但具有内部 容量控制用于存储队列中的元素。它总是至少和队列一样大
4.此实现不同步如果想使用线程安全的,则使用PriorityBlockingQueue类
实现原理
可以用一棵完全二叉树表示(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。
根据例图来看
PriorityQueue的底层实现为平衡二进制堆的优先级队列:
队列[n]的子级是队列[2n+1]和队列[2(n+1)]。
leftNo = [2n+1]
rightNo = [2(n+1)]
队列n也就是父节点
通过上述2个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。
PriorityQueue_的peek()和element操作是常数时间,add(), offer(), 无参数的remove()以及poll()方法的时间复杂度都是_log(N) 。
不理解_log(N) 参考
时间复杂度 O(log n) 意味着什么
属性分析
private static final int DEFAULT_INITIAL_CAPACITY = 11;
/**
*表示为平衡二进制堆的优先级队列:队列[n]的子级是队列[2*n+1]和队列[2*(n+1)]。这个
*优先级队列由比较器或元素的自然排序,如果比较器为空:对于堆和n的每个后代d,n<=d。具有
*最小值在队列[0]中,假设该队列为非空。
*/
transient Object[] queue; // 非私有以简化嵌套类访问
/**
* 优先级队列中的元素数。
*/
private int size = 0;
/**
* 比较器,如果优先级队列使用元素
* natural ordering.
*/
private final Comparator<? super E> comparator;
/**
* 此优先级队列被删除的次数
* <i> 结构上修改的</i>。请参见抽象列表。
*/
transient int modCount = 0; // 非私有以简化嵌套类访问
默认容量
默认容量为11,该容量是初始化容器时进行设置的
transient修饰的变量不参与序列化和反序列化
在序列化时,定义了两个方法writeObject和readObject,实现了自己可控制的序列化,而达到定制的效果,也就是不跟据queue的大小进行序列化,而是根据 size进行的
/**
* 将此队列保存到流中(即序列化它)。
*
* @serialData The length of the array backing the instance is
* emitted (int), followed by all of its elements
* (each an {@code Object}) in the proper order.
* @param s the stream
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// 写下元素计数和任何隐藏的东西
s.defaultWriteObject();
// 写出数组长度,以便与1.5版本兼容
s.writeInt(Math.max(2, size + 1));
// 按“适当的顺序”写出所有的内容。
for (int i = 0; i < size; i++)
s.writeObject(queue[i]);
}
/**
* 从流中重建{@code PriorityQueue}实例
* (that is, deserializes it).
*
* @param s the stream
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 阅读的大小,以及任何隐藏的东西
s.defaultReadObject();
// 读入(和丢弃)数组长度
s.readInt();
queue = new Object[size];
// 读入所有元素。
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// 元素保证“有序”,但
// spec从未解释过这可能是什么
heapify();
}
理解作者所写的平衡二进制堆的优先级队列
先从什么是堆说起
堆是一种非线性结构,(分析堆的数组实现)可以把堆看作一个数组,也可以被看作一个完全二叉树,通俗来讲堆其实就是利用完全二叉树的结构来维护的一维数组
按照堆的特点可以把堆分为大顶堆和小顶堆
大顶堆:每个结点的值都大于或等于其左右孩子结点的值
小顶堆:每个结点的值都小于或等于其左右孩子结点的值
堆的这种特性非常的有用,堆常常被当做优先队列使用,因为可以快速的访问到“最重要”的元素
优先级队列
是由小顶堆实现的数据结构。
构造方法
- 不带参数的构造方法
设置默认的容量1 以及默认的Comparator - 带默认容量的构造方法,
设置默认的Comparator - 指定Comparator和容量
- PriorityQueue 构造方法中放集合
利用initElementsFromCollection 初始化方法初始化数据,并赋值pq.comparator(),没有构造器则设置为null
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);
}
/**
* 创建具有指定初始容量的{@code PriorityQueue}
* 它根据指定的比较器对其元素进行排序。
*
* @param initialCapacity此优先级队列的初始容量
* @param comparator the comparator that will be used to order this
* priority queue. If {@code null}, the {@linkplain Comparable
* natural ordering} of the elements will be used.
* @throws IllegalArgumentException if {@code initialCapacity} is
* less than 1
*/
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// 注意:实际上不需要至少一个的限制,
// 但1.5的兼容性仍在继续
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
/**
* 创建包含中元素的{@code PriorityQueue}
* 指定的集合。如果指定的集合是
* 一个{@link SortedSet}或者是另一个{@code PriorityQueue},这个
* 优先级队列将按照相同的顺序排序。否则,此优先级队列将根据
* 元素的{@linkplain可比自然排序}。
*
* @param c the collection whose elements are to be placed
* into this priority queue
* @throws ClassCastException if elements of the specified collection
* cannot be compared to one another according to the priority
* queue's ordering
* @throws NullPointerException if the specified collection or any
* of its elements are null
*/
@SuppressWarnings("unchecked")
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);
}
}
private void initElementsFromCollection(Collection<? extends E> c) {
Object[] a = c.toArray();
// 如果c.toArray不正确地返回Object[],请复制它。
if (a.getClass() != Object[].class)
a = Arrays.copyOf(a, a.length, Object[].class);
int len = a.length;
if (len == 1 || this.comparator != null)
for (int i = 0; i < len; i++)
if (a[i] == null)
throw new NullPointerException();
this.queue = a;
this.size = a.length;
}
- initElementsFromCollection方法
不调整数据顺序,直接将 初始化 集合中的数据赋值给属性queue 和size
//initFromCollection 调整数据结构
/**
* 在整个树中建立堆不变量(如上所述),
* 在调用之前不考虑元素的顺序。
*/
@SuppressWarnings("unchecked")
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
/**
* 在位置k处插入项目x,保持堆不变
* 重复将x降级到树下,直到它小于或等于
* 等于它的孩子或是一片叶子。
*
* @param k the position to fill
* @param x the item to insert
*/
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
siftDown方法
这就是保持堆不变 做调整树的方法,在remove()和poll() 方法中也会调用的,这里先不 深究
add 和offer 方法
/**
* 将指定的元素插入此优先级队列。
*
* @return {@code true} (as specified by {@link Collection#add})
* @throws ClassCastException 如果指定的元素不能
* 与当前在此优先级队列中的元素进行比较
* 根据优先级队列的顺序
* @throws NullPointerException if the specified element is null
*/
public boolean add(E e) {
return offer(e);
}
/**
* 将指定的元素插入此优先级队列。
*
* @return {@code true} (as specified by {@link Queue#offer})
* @throws ClassCastException 如果指定的元素不能
* 与当前在此优先级队列中的元素进行比较
* 根据优先级队列的顺序
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
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;
}
/**
* 增加阵列的容量。
*
* @param 最小容量所需的最小容量
*/
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 两倍大小,如果小;其他增长50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
//arrylist中代码
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 有溢出意识的代码
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
从上面的代码分析
- offer 方法 如果添加e值为null则直接抛出 NullPointerException 异常
- i >= queue.length 才判断需要扩展队列长度,当前size只要等于队列的大小就进行扩容,
这个可以arrayList中的动态扩容比对看一下 - arraylist中会先从size加+1判断 在和默认容量比较取最大容量,在与数组大小对比扩容,并扩展老容量的50%,其实都是达到动态扩容的效果
- PriorityQueue扩容则直接判断老容量是否小于64,然后在采用不同的扩容方式,小于64直接扩容两倍,大于64,直接扩容50%
- 只有当所需最小的容量大于最大容量,才使用最小容量去计算新容量
PriorityQueue的动态扩容
siftUp方法
/**
* 在位置k处插入项x,保持堆不变
* 在树上提升x,直到它大于或等于
* 它的父级,或者是根。
*
* 简化和加速强制和比较。这个
* 可比版本和比较版本分为不同的版本
* 其他方面相同的方法(类似于siftDown。)
*
* @param k要填补的职位
* @param x the item to insert
*/
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
这是一个最小根堆,从某个节点开始往上对比,一旦父节点比它大,则交换,直到比它小为止。 这可能比较难理解,就用一个图画来展示。
从末尾添加一个节点
往上和父节点比较交换数据
完成交换实现代码
对于没有传入Comparator 的
1.默认需要传入对象需要支持Comparable,不然会出现转换异常,并且,对比也是调用的是对象的compareTo方法
2. 在这里 k是size, 这个公式是求左右节点的2*(n+1) 的变形
从k指定的位置开始,将x逐层与当前点的parent进行比较并交换,直到满足x >= queue[parent]为止
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;//parentNo = (nodeNo-1)/2
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
这就是整个 add offer操作,重要的一点主要是 利用最小堆调整数据位置的
peek 和indexOf和element()方法
** peek 方法 获取到 第一个数据数据,先进先出 **
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
private int indexOf(Object o) {
if (o != null) {
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
return -1;
}
AbstractQueue 类中element方法
当没有数组则抛NoSuchElementException异常
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
poll()和remove()方法
remove()和poll()方法的语义也完全相同,都是获取并删除队首元素,区别是前者当方法失败时前者抛出异常,后者返回null。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。
AbstractQueue 中实现remove 方法
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
数据为空则抛出NoSuchElementException异常
poll方法源码
@SuppressWarnings("unchecked")
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;
if (s != 0)
siftDown(0, x);
return result;
}
modCount 变量加+1,size -1 将头数据给获取
以上是关于Java 集合深入理解 :优先队列(PriorityQueue)之源码解读,及最小顶堆实现研究的主要内容,如果未能解决你的问题,请参考以下文章
Java 集合深入理解 :LinkedList链表源码研究,及双向队列如何实现