JDK源码研究PriorityQueue(优先队列)

时间:2022-07-13 17:02:50

Priority Queue

 

目的:

通过对JDK源码的分析,进一步了解堆和优先队列,体会JDK源码的优美之处。

目录:

        1:概念

        2:源码结构

        3:方法分析

概念:

概念1:堆

堆,n个关键字序列K1,K2,…,Kn,当且仅当该序列满足如下性质称为堆

ki≤K2i且ki≤K2i+1(最小堆) 或 (2)Ki≥K2i且ki≥K2i+1 (最大堆)

堆一般用顺序存储结构存储(数组),但逻辑上可以认为是一个完全二叉树。

概念2:优先队列

优先队列,不同于普通的遵循FIFO(先进先出)规则的队列,每次都选出优先级最高的元素出队,优先队列里实际是维护了这样的一个堆,通过堆使得每次取出的元素总是最小的(用户可以自定义比较方法,相当于用户设定优先级)。

 

源码结构

   字段

 

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. //默认初始化大小  
  2. private static final int DEFAULT_INITIAL_CAPACITY = 11;  
  3. //堆  
  4. private transient Object[] queue;  
  5. //当前大小  
  6. private int size = 0;  
  7. //比较器  
  8. private final Comparator<? super E> comparator;  
  9. //修改次数(增、删、改、查)  
  10. private transient int modCount = 0;  

   方法    

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. //增加  
  2. public boolean add(E e)   
  3. //出队(不删除)  
  4. public E peek()  
  5. //出队(删除)  
  6. public E poll()  
  7. //删除  
  8. public boolean remove(Object o)  
  9. //是否包含某元素  
  10. public boolean contains(Object o)  
  11. //清空  
  12. public void clear()  
  13. //扩容  
  14. private void grow(int minCapacity)  
  15. //查找  
  16. private int indexOf(Object o)  

方法分析

1:增加

堆在增加元素后,需要进行调整才能维护其最大堆或者最小堆的性质,下面以最小堆为例:

JDK源码研究PriorityQueue(优先队列)
       增加元素26,默认是从队尾增加,即直接添加到数组最后。下一步需要执行上滤。从上图可以看出,26比其父节点39小,因此两者交换位置;再次比较此时的26和其父节点30,30>26,调整位置,依次进行直到找到比26小的父节点,结束。

代码:

 add

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. public boolean add(E e) {  
  2.         return offer(e);  
  3.     }  

   可以看出,add方法实际上是全部委托给offer(E)

 

Java代码   JDK源码研究PriorityQueue(优先队列)
  1.  public boolean offer(E e) {  
  2.     if (e == null)  
  3.         throw new NullPointerException();  
  4.     modCount++;  
  5.     int i = size;  
  6.     //检查容量(扩容)  
  7.     if (i >= queue.length)  
  8.         grow(i + 1);  
  9.     //改变size  
  10.     size = i + 1;  
  11.     //调整  
  12.     if (i == 0)  
  13.         queue[0] = e;//无父节点 ,直接赋值  
  14.     else  
  15.         siftUp(i, e);//有父节点,需要上滤  
  16.     return true;  
  17. }  

    第1步:判空

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. if (e == null)  
  2.     throw new NullPointerException();  

    第2步:改变大小和扩容

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. modCount++;  
  2. int i = size;  
  3. if (i >= queue.length)  
  4.     grow(i + 1);  
  5. size = i + 1;  

   第3步:添加元素并上滤

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. if (i == 0)  
  2.     queue[0] = e;  
  3. else  
  4.     siftUp(i, e);  

  从上面的3步中可以看出,实际上关键的步骤是:grow 和 siftUp

  grow方法

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. private void grow(int minCapacity) {  
  2.     if (minCapacity < 0// overflow  
  3.         throw new OutOfMemoryError();  
  4.     int oldCapacity = queue.length;  
  5.     // Double size if small; else grow by 50%  
  6.     int newCapacity = ((oldCapacity < 64) ? ((oldCapacity + 1) * 2)  
  7.             : ((oldCapacity / 2) * 3));  
  8.     if (newCapacity < 0// overflow  
  9.         newCapacity = Integer.MAX_VALUE;  
  10.     if (newCapacity < minCapacity)  
  11.         newCapacity = minCapacity;  
  12.     queue = Arrays.copyOf(queue, newCapacity);  
  13. }  

1:扩容方式是:

   当前队列大小queue.length<64,则增加一倍容量;反之则增加一半容量。

 2:调用Arrays的copyOf函数 ,实际上调用了该函数

  JDK源码研究PriorityQueue(优先队列)

 这是个native方法。(注意,该方法只是浅克隆)

siftUp 方法

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. private void siftUp(int k, E x) {  
  2.     if (comparator != null)  
  3.         siftUpUsingComparator(k, x);  
  4.     else  
  5.         siftUpComparable(k, x);  
  6. }  

根据不同的比较方式,采取不同比较策略。

下面以使用默认comparator的方式分析

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. private void siftUpComparable(int k, E x) {  
  2.     Comparable<? super E> key = (Comparable<? super E>) x;  
  3.     //k>0保证元素有父节点  
  4.     while (k > 0) {  
  5.         //父节点下标  
  6.         int parent = (k - 1) >>> 1;  
  7.         Object e = queue[parent];  
  8.         //如果比父节点大,不需要移动,结束  
  9.         if (key.compareTo((E) e) >= 0)  
  10.             break;  
  11.         //父节点元素下移  
  12.         queue[k] = e;  
  13.         //改变k的位置  
  14.         k = parent;  
  15.     }  
  16.     //找到key对一个的合适的位置k,赋值  
  17.     queue[k] = key;  
  18. }  

   可以看出,该方法是采用迭代的方式,找到元素x的位置。

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. int parent = (k - 1) >>> 1;  
  2. Object e = queue[parent];  

   通过无符号移位操作,取得父节点位置

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. if (key.compareTo((E) e) >= 0)  
  2.  break;  
  3.  queue[k] = e;  
  4.  k = parent;  

  此处采用了默认的comparator

  如果比父节点值大,结束。

   如果比父节点值小,父节点值下沉,K重新赋值,直到k=0或者k结点的值大于或等于parent结点值。

2:出队(不删除)

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. public E peek() {  
  2.     if (size == 0)  
  3.         return null;  
  4.     return (E) queue[0];  
  5. }  

 这个很简单,只是取出了其中的首位元素,但是并没有删除,不需要调整堆。

3:出队(删除最小元素)

出队过程
JDK源码研究PriorityQueue(优先队列)
当最小元素14出队,从数组尾处取39赋值给队首。之后,进行和增加元素后相反的动作即下滤。

首先选出根节点(父节点)39的两个孩子结点中较小者,和39交换位置;当39找到新位置后,执行同种方法,如果孩子结点为null或者都比39大,则结束。

代码:

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. public E poll() {  
  2.     // 优先队列为空,返回null  
  3.     if (size == 0)  
  4.         return null;  
  5.     int s = --size;  
  6.     modCount++;  
  7.     // 取出队首  
  8.     E result = (E) queue[0];  
  9.     E x = (E) queue[s];  
  10.     // 队尾赋值为null  
  11.     queue[s] = null;  
  12.     // 判断是否执行下滤  
  13.     if (s != 0)  
  14.         siftDown(0, x);  
  15.     return result;  
  16. }  

 可以看出其中主要方法是siftDown方法

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. private void siftDown(int k, E x) {  
  2.     if (comparator != null)  
  3.     siftDownUsingComparator(k, x);  
  4.     else  
  5.     siftDownComparable(k, x);  
  6. }  

  同样,和上滤一样,是根据不同的comparator采取不同措施比较

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. private void siftDownUsingComparator(int k, E x) {  
  2.     // 计算非叶子节点元素的最大位置  
  3.     int half = size >>> 1;  
  4.    // 如果不是叶子结点  
  5.     while (k < half) {  
  6.         // 左孩子  
  7.         int child = (k << 1) + 1;  
  8.        // 默认使用左孩子的值  
  9.         Object c = queue[child];  
  10.         //右孩子  
  11.         int right = child + 1;  
  12.         /如果右孩子小于左孩子,c重新赋值为右孩子的值  
  13.         if (right < size &&  
  14.             comparator.compare((E) c, (E) queue[right]) > 0)  
  15.             c = queue[child = right];  
  16.        // c和key(父节点)比较,若父节点大,不需要移动,结束  
  17.         if (comparator.compare(x, (E) c) <= 0)  
  18.             break;  
  19.         queue[k] = c;  
  20.         //改变k的位置,向下移动  
  21.         k = child;  
  22.     }  
  23.     queue[k] = x;  
  24. }  

 如果自上向下调整的位置k大于half,说明该结点是叶子结点,直接将x元素赋值给queue[k].

如果自上向下调整的位置k小于half,则递归调整。首先取出左右孩子结点,并取两者中较小者赋值给c,然后比较c和当前k处元素key,如果key小,则结束。如果c大,则将k和c调换位置,经过多次迭代后,当x应该存放在叶子结点上或者x的值小于其左右孩子节点时终止

4:删除

  删除有两种情况:

    情况1:

JDK源码研究PriorityQueue(优先队列)  此处是执行下滤过程。

 情况2:

JDK源码研究PriorityQueue(优先队列)此处是执行上滤过程。

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. private E removeAt(int i) {  
  2.     assert i >= 0 && i < size;  
  3.     modCount++;  
  4.     int s = --size;  
  5.     // 如果是最后一个元素,直接赋值null  
  6.     if (s == i)  
  7.         queue[i] = null;  
  8.     else {  
  9.         // 取最后一个元素后,最后位置赋值为null  
  10.         E moved = (E) queue[s];  
  11.         queue[s] = null;  
  12.         // 执行下滤  
  13.         siftDown(i, moved);  
  14.         // 如果下滤后元素位置没变,说明moved是该子树最小元素;之后需要执行上滤  
  15.         // 上滤和下滤实际效果是只会执行其中一个  
  16.         if (queue[i] == moved) {  
  17.             siftUp(i, moved);  
  18.             if (queue[i] != moved)// iterator中会用到此处  
  19.                 return moved;  
  20.         }  
  21.     }  
  22.     return null;  
  23. }  

 如果删除的是最后一个元素,则将最后一个元素设为null

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. if (s == i)  
  2.     queue[i] = null;  

 如果删除的不是最后一个元素,取出最后一个元素,并将最后一个元素设为null。执行向下调整函数  siftDown.

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. E moved = (E) queue[s];  
  2. queue[s] = null;  
  3. siftDown(i, moved);  

 如果执行了下滤之后,如情况2,此时24并没有向下移动,此时说明需要进行上滤过程

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. if (queue[i] == moved) {  
  2.     siftUp(i, moved);  
  3.     if (queue[i] != moved)// iterator中会用到此处  
  4.     return moved;  
  5. }  

 5:clear:清除

这个很简单,只是遍历数组,删除(设为null)

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. public void clear() {  
  2.     modCount++;  
  3.     for (int i = 0; i < size; i++)  
  4.         queue[i] = null;  
  5.     size = 0;  
  6. }  

 6:contains:是否包含

这个过程实际上就是查找过程

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. public boolean contains(Object o) {  
  2. urn indexOf(o) != -1;  
  3. }  

 7:idnexOf :查找

Java代码   JDK源码研究PriorityQueue(优先队列)
  1. private int indexOf(Object o) {  
  2. (o != null) {  
  3.         //遍历数组查询  
  4.         for (int i = 0; i < size; i++)  
  5.             //如果是自定义的元素,重写equals方法是很有必要的  
  6.             if (o.equals(queue[i]))  
  7.                 return i;  
  8.     }  
  9.     return -1;  
  10. }  

以上只是简单的分析了主要的方法,对于构造函数,实际上主要就是调用这几个方法,就没有再分析。有兴趣可以自行分析,相信会有所收获