给jdk写注释系列之jdk1.6容器(12):PriorityQueue源码解析,
分享于 点击 36844 次 点评:79
给jdk写注释系列之jdk1.6容器(12):PriorityQueue源码解析,
本系列:
- 给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析
- 给jdk写注释系列之jdk1.6容器(2):LinkedList源码解析
- 给jdk写注释系列之jdk1.6容器(3):Iterator设计模式
- 给jdk写注释系列之jdk1.6容器(4):HashMap源码解析
- 给jdk写注释系列之jdk1.6容器(5):LinkedHashMap源码解析
- 给jdk写注释系列之jdk1.6容器(6):HashSet源码解析&Map迭代器
- 给jdk写注释系列之jdk1.6容器(8):TreeSet、NavigableMap、NavigableSet源码解析
- 给jdk写注释系列之jdk1.6容器(9):Strategy设计模式之Comparable&Comparator接口
- 给jdk写注释系列之jdk1.6容器(10):Stack&Vector源码解析
- 给jdk写注释系列之jdk1.6容器(11):Queue之ArrayDeque源码解析
PriorityQueue是一种什么样的容器呢?看过前面的几个jdk容器分析的话,看到Queue这个单词你一定会,哦~这是一种队列。是的,PriorityQueue是一种队列,但是它又是一种什么样的队列呢?它具有着什么样的特点呢?它的底层实现方式又是怎么样的呢?我们一起来看一下。
PriorityQueue其实是一个优先队列,什么是优先队列呢?这和我们前面讲的先进先出(First In First Out )的队列的区别在于,优先队列每次出队的元素都是优先级最高的元素。那么怎么确定哪一个元素的优先级最高呢,jdk中使用堆这么一种数据结构,通过堆使得每次出队的元素总是队列里面最小的,而元素的大小比较方法可以由用户指定,这里就相当于指定优先级喽。1.二叉堆介绍 那么堆又是什么一种数据结构呢、它有什么样的特点呢?(以下见于百度百科)
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全树。


2.PriorityQueue的底层实现 先来看下PriorityQueue的定义:
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {我们看到PriorityQueue继承了AbstractQueue抽象类,并实现了Serializable接口,AbstractQueue抽象类实现了Queue接口,对其中方法进行了一些通用的封装,具体就不多看了。 下面再看下PriorityQueue的底层存储相关定义:
// 默认初始化大小 privatestaticfinalintDEFAULT_INITIAL_CAPACITY = 11; // 用数组实现的二叉堆,下面的英文注释确认了我们前面的说法。 /** * Priority queue represented as a balanced binary heap: the two * children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The * priority queue is ordered by comparator, or by the elements' * natural ordering, if comparator is null: For each node n in the * heap and each descendant d of n, n <= d. The element with the * lowest value is in queue[0], assuming the queue is nonempty. */ private transient Object[] queue ; // 队列的元素数量 private int size = 0; // 比较器 private final Comparator<? super E> comparator; // 修改版本 private transient int modCount = 0;我们看到jdk中的PriorityQueue的也是基于数组来实现一个二叉堆,并且注释中解释了我们前面的说法。而Comparator这个比较器我们已经很熟悉了,我们说PriorityQueue是一个有限队列,他可以由用户指定优先级,就是靠这个比较器喽。
3.PriorityQueue的构造方法
/** * 默认构造方法,使用默认的初始大小来构造一个优先队列,比较器comparator为空,这里要求入队的元素必须实现Comparator接口 */ public PriorityQueue() { this(DEFAULT_INITIAL_CAPACITY, null); } /** * 使用指定的初始大小来构造一个优先队列,比较器comparator为空,这里要求入队的元素必须实现Comparator接口 */ public PriorityQueue( int initialCapacity) { this(initialCapacity, null); } /** * 使用指定的初始大小和比较器来构造一个优先队列 */ public PriorityQueue( int initialCapacity, Comparator<? super E> comparator) { // Note: This restriction of at least one is not actually needed, // but continues for 1.5 compatibility // 初始大小不允许小于1 if (initialCapacity < 1) throw new IllegalArgumentException(); // 使用指定初始大小创建数组 this.queue = new Object[initialCapacity]; // 初始化比较器 this.comparator = comparator; } /** * 构造一个指定Collection集合参数的优先队列 */ public PriorityQueue(Collection<? extends E> c) { // 从集合c中初始化数据到队列 initFromCollection(c); // 如果集合c是包含比较器Comparator的(SortedSet/PriorityQueue),则使用集合c的比较器来初始化队列的Comparator if (c instanceof SortedSet) comparator = (Comparator<? super E>) ((SortedSet<? extends E>)c).comparator(); else if (c instanceof PriorityQueue) comparator = (Comparator<? super E>) ((PriorityQueue<? extends E>)c).comparator(); // 如果集合c没有包含比较器,则默认比较器Comparator为空 else { comparator = null; // 调用heapify方法重新将数据调整为一个二叉堆 heapify(); } } /** * 构造一个指定PriorityQueue参数的优先队列 */ public PriorityQueue(PriorityQueue<? extends E> c) { comparator = (Comparator<? super E>)c.comparator(); initFromCollection(c); } /** * 构造一个指定SortedSet参数的优先队列 */ public PriorityQueue(SortedSet<? extends E> c) { comparator = (Comparator<? super E>)c.comparator(); initFromCollection(c); } /** * 从集合中初始化数据到队列 */ private void initFromCollection(Collection<? extends E> c) { // 将集合Collection转换为数组a Object[] a = c.toArray(); // If c.toArray incorrectly doesn't return Object[], copy it. // 如果转换后的数组a类型不是Object数组,则转换为Object数组 if (a.getClass() != Object[].class) a = Arrays. copyOf(a, a.length, Object[]. class); // 将数组a赋值给队列的底层数组queue queue = a; // 将队列的元素个数设置为数组a的长度 size = a.length ; }构造方法还是比较容易理解的,第四个构造方法中,如果填入的集合c没有包含比较器Comparator,则在调用initFromCollection初始化数据后,在调用heapify方法对数组进行调整,使得它符合二叉堆的规范或者特点,具体heapify是怎么构造二叉堆的,我们后面再看。 那么怎么样调整才能使一些杂乱无章的数据变成一个符合二叉堆的规范的数据呢?
4.二叉堆的添加原理及PriorityQueue的入队实现 我们回忆一下,我们在说红黑树TreeMap的时候说,红黑树为了维护其红黑平衡,主要有三个动作:左旋、右旋、着色。那么二叉堆为了维护他的特点又需要进行什么样的操作呢。 我们再来看下二叉堆(最小堆为例)的特点: (1)父结点的键值总是小于或等于任何一个子节点的键值。 (2)基于数组实现的二叉堆,对于数组中任意位置的n上元素,其左孩子在[2n+1]位置上,右孩子[2(n+1)]位置,它的父亲则在[n-1/2]上,而根的位置则是[0]。 为了维护这个特点,二叉堆在添加元素的时候,需要一个”上移”的动作,什么是”上移”呢,我们继续用图来说明。


/** * 添加一个元素 */ public boolean add(E e) { return offer(e); } /** * 入队 */ public boolean offer(E e) { // 如果元素e为空,则排除空指针异常 if (e == null) throw new NullPointerException(); // 修改版本+1 modCount++; // 记录当前队列中元素的个数 int i = size ; // 如果当前元素个数大于等于队列底层数组的长度,则进行扩容 if (i >= queue .length) grow(i + 1); // 元素个数+1 size = i + 1; // 如果队列中没有元素,则将元素e直接添加至根(数组小标0的位置) if (i == 0) queue[0] = e; // 否则调用siftUp方法,将元素添加到尾部,进行上移判断 else siftUp(i, e); return true; }这里的add方法依然没有按照Queue的规范,在队列满的时候抛出异常,因为PriorityQueue和前面讲的ArrayDeque一样,会进行扩容,所以只有当队列容量超出int范围才会抛出异常。 既然PriorityQueue会进行队列扩容,那么就来看下扩容的具体实现吧(对于数组实现的容器,我们见过太多的扩容了。。。)。
/** * 数组扩容 */ private void grow(int minCapacity) { // 如果最小需要的容量大小minCapacity小于0,则说明此时已经超出int的范围,则抛出OutOfMemoryError异常 if (minCapacity < 0) // overflow throw new OutOfMemoryError(); // 记录当前队列的长度 int oldCapacity = queue .length; // Double size if small; else grow by 50% // 如果当前队列长度小于64则扩容2倍,否则扩容1.5倍 int newCapacity = ((oldCapacity < 64)? ((oldCapacity + 1) * 2): ((oldCapacity / 2) * 3)); // 如果扩容后newCapacity超出int的范围,则将newCapacity赋值为Integer.Max_VALUE if (newCapacity < 0) // overflow newCapacity = Integer. MAX_VALUE; // 如果扩容后,newCapacity小于最小需要的容量大小minCapacity,则按找minCapacity长度进行扩容 if (newCapacity < minCapacity) newCapacity = minCapacity; // 数组copy,进行扩容 queue = Arrays.copyOf( queue, newCapacity); }需要理解的是,这里为什么当minCapacity小于0的时候,就代表超出int范围呢,我们来看下。 int在java中占4个字节,一个字节8位,从0开始记,那么4个字节的最高位就是31,而java中的基本数据类型都是有符号的,所以最高位代表的是符号位。 int的最大值Integer.MAX_VALUE=0111 1111 1111 1111 1111 1111 1111 1111,Integer.MAX_VALUE+1=1000 0000 0000 0000 0000 0000 0000 0000,此时最高位是符号位为1,所以这个数是负数。负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1(即在反码的基础上+1)。 好了,看完上面这个小插曲,我们来看下二叉堆的一个重要操作”上移”是怎么实现的吧。
/** * 上移,x表示新插入元素,k表示新插入元素在数组的位置 */ private void siftUp(int k, E x) { // 如果比较器comparator不为空,则调用siftUpUsingComparator方法进行上移操作 if (comparator != null) siftUpUsingComparator(k, x); // 如果比较器comparator为空,则调用siftUpComparable方法进行上移操作 else siftUpComparable(k, x); } private void siftUpComparable(int k, E x) { // 比较器comparator为空,需要插入的元素实现Comparable接口,用于比较大小 Comparable<? super E> key = (Comparable<? super E>) x; // k>0表示判断k不是根的情况下,也就是元素x有父节点 while (k > 0) { // 计算元素x的父节点位置[(n-1)/2] int parent = (k - 1) >>> 1; // 取出x的父亲e Object e = queue[parent]; // 如果新增的元素k比其父亲e大,则不需要"上移",跳出循环结束 if (key.compareTo((E) e) >= 0) break; // x比父亲小,则需要进行"上移" // 交换元素x和父亲e的位置 queue[k] = e; // 将新插入元素的位置k指向父亲的位置,进行下一层循环 k = parent; } // 找到新增元素x的合适位置k之后进行赋值 queue[k] = key; } // 这个方法和上面的操作一样,不多说了 private void siftUpUsingComparator(int k, E x) { while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (comparator .compare(x, (E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = x; }结合上面的图解,二叉堆”上移”操作的代码还是很容易理解的,主要就是不断的将新增元素和其父亲进行大小比较,比父亲小则上移,最终找到一个合适的位置。
5.二叉堆的删除根原理及PriorityQueue的出队实现 对于二叉堆的出队操作,出队永远是要删除根元素,也就是最小的元素,要删除根元素,就要找一个替代者移动到根位置,相对于被删除的元素来说就是”下移”。



/** * 删除并返回队头的元素,如果队列为空则抛出NoSuchElementException异常(该方法在AbstractQueue中) */ public E remove() { E x = poll(); if (x != null) return x; else throw new NoSuchElementException(); } /** * 删除并返回队头的元素,如果队列为空则返回null */ public E poll() { // 队列为空,返回null if (size == 0) return null; // 队列元素个数-1 int s = --size ; // 修改版本+1 modCount++; // 队头的元素 E result = (E) queue[0]; // 队尾的元素 E x = (E) queue[s]; // 先将队尾赋值为null queue[s] = null; // 如果队列中不止队尾一个元素,则调用siftDown方法进行"下移"操作 if (s != 0) siftDown(0, x); return result; } /** * 上移,x表示队尾的元素,k表示被删除元素在数组的位置 */ private void siftDown(int k, E x) { // 如果比较器comparator不为空,则调用siftDownUsingComparator方法进行下移操作 if (comparator != null) siftDownUsingComparator(k, x); // 比较器comparator为空,则调用siftDownComparable方法进行下移操作 else siftDownComparable(k, x); } private void siftDownComparable(int k, E x) { // 比较器comparator为空,需要插入的元素实现Comparable接口,用于比较大小 Comparable<? super E> key = (Comparable<? super E>)x; // 通过size/2找到一个没有叶子节点的元素 int half = size >>> 1; // loop while a non-leaf // 比较位置k和half,如果k小于half,则k位置的元素就不是叶子节点 while (k < half) { // 找到根元素的左孩子的位置[2n+1] int child = (k << 1) + 1; // assume left child is least // 左孩子的元素 Object c = queue[child]; // 找到根元素的右孩子的位置[2(n+1)] int right = child + 1; // 如果左孩子大于右孩子,则将c复制为右孩子的值,这里也就是找出左右孩子哪个最小 if (right < size && ((Comparable<? super E>) c).compareTo((E) queue [right]) > 0) c = queue[child = right]; // 如果队尾元素比根元素孩子都要小,则不需"下移",结束 if (key.compareTo((E) c) <= 0) break; // 队尾元素比根元素孩子都大,则需要"下移" // 交换跟元素和孩子c的位置 queue[k] = c; // 将根元素位置k指向最小孩子的位置,进入下层循环 k = child; } // 找到队尾元素x的合适位置k之后进行赋值 queue[k] = key; } // 这个方法和上面的操作一样,不多说了 private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue [right]) > 0) c = queue[child = right]; if (comparator .compare(x, (E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = x; }jdk中,不是直接将根元素删除,然后再将下面的元素做上移,重新补充根元素;而是找出队尾的元素,并在队尾的位置上删除,然后通过根元素的下移,给队尾元素找到一个合适的位置,最终覆盖掉跟元素,从而达到删除根元素的目的。这样做在一些情况下,会比直接删除在上移根元素,或者直接下移根元素再调整队尾元素的位置少操作一些步奏(比如上面图解中的例子,不信你可以试一下^_^)。 明白了二叉堆的入队和出队操作后,其他的方法就都比较简单了,下面我们再来看一个二叉堆中比较重要的过程,二叉堆的构造。
6.堆的构造过程 我们在上面提到过的,堆的构造是通过一个heapify方法,下面我们来看下heapify方法的实现。
/** * Establishes the heap invariant (described above) in the entire tree, * assuming nothing about the order of the elements prior to the call. */ private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); }这个方法很简单,就这几行代码,但是理解起来却不是那么容器的,我们来分析下。 假设有一个无序的数组,要求我们将这个数组建成一个二叉堆,你会怎么做呢?最简单的办法当然是将数组的数据一个个取出来,调用入队方法。但是这样做,每次入队都有可能会伴随着元素的移动,这么做是十分低效的。那么有没有更加高效的方法呢,我们来看下。 为了方便,我们将上面我们图解中的数组去掉几个元素,只留下7、6、5、12、10、3、1、11、15、4(顺序已经随机打乱)。ok、那么接下来,我们就按照当前的顺序建立一个二叉堆,暂时不用管它是否符合标准。 int a = [7, 6, 5, 12, 10, 3, 1, 11, 15, 4 ];




private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); }int i = (size >>> 1) – 1,这行代码是为了找寻最后一个非叶子节点,然后倒序进行”下移”siftDown操作,是不是很显然了。 到这里PriorityQueue的基本操作就分析完了,明白了其底层二叉堆的概念及其入队、出队、建堆等操作,其他的一些方法代码就很简单了,这里就不一一分析了。 PriorityQueue 完!
参见: 给jdk写注释系列之jdk1.6容器(11)-Queue之ArrayDeque源码解析 给jdk写注释系列之jdk1.6容器(9)-Strategy设计模式之Comparable&Comparator接口
参考资料: 图解数据结构(8)——二叉堆 java实现选择排序(直接选择排序、堆排序)
用户点评