• JDK源码剖析之PriorityQueue优先级队列


    写在前面

    版本信息:
    JDK1.8

    PriorityQueue介绍

    数据结构中,队列分为FIFOLIFO 两种模型,分别为先进先出,后进后出先进后出,后进先出(栈) 而一切数据结构都是基于数组或者是链表实现。

    在Java中,定义了Queue接口,接口中定义了CRUD的基本方法。分别add、offer、remove、poll等等,而PriorityQueue 实现此接口实现了基本的CRUD的同时拥有了自己的特性,从名字来看也能知道是优先级队列 : 保持队列头部节点是整条队列中永远是最小或者最大的节点,其实现原理就是一个小顶堆或者大顶堆。上文提及到一切数据结构都是基于数组或者是链表实现,而这里使用了数组实现。

    1. public class PriorityQueue extends AbstractQueue
    2. implements java.io.Serializable {
    3. transient Object[] queue; // 由数组实现
    4. }
    5. public abstract class AbstractQueue
    6. extends AbstractCollection
    7. implements Queue {}

    小顶堆和大顶堆介绍

    从上文描述了PriorityQueue的底层实现是小顶堆或者大顶堆,那么在看源码之前,我们需要先明白小顶堆和大顶堆如何实现~

    小顶堆:一颗完全二叉树,其中任意父节点都要小于左右子节点,所以树的根节点是整棵树的最小节点

    大顶堆:一颗完全二叉树,其中任意父节点都要大于左右子节点,所以树的根节点是整棵树的最大节点

    Comparable和Comparator区别

    在看PriorityQueue源码之前还需要分析Comparable和Comparator区别。

    Comparable:类需要实现此接口,重写compareTo方法,在compareTo方法中定义比较逻辑,使用时把类强转成Comparable调用compareTo方法,把比较对象传入。所以侵入性比较强,与业务代码强耦合。

    Comparator:这个就是一个比较器,只需要把A比较对象和B比较对象都传入即可,不需要于业务代码强耦合

    PriorityQueue添加元素源码分析

    下文直接把PriorityQueue叫成小顶堆

    我们直接从offer方法入手~

    1. public boolean offer(E e) {
    2. if (e == null)
    3. throw new NullPointerException();
    4. modCount++; // 用于检测是否并发
    5. // 因为size从0开始,所以size的值就是数组的索引值。
    6. int i = size;
    7. // 是否需要扩容
    8. if (i >= queue.length)
    9. grow(i + 1);
    10. // 为下次索引+1
    11. size = i + 1;
    12. // 如果是第一个,那么就直接占用数组第一个元素即可,
    13. // 因为不管是小顶堆还是大堆堆第一个都直接插入。
    14. if (i == 0)
    15. queue[0] = e;
    16. else
    17. // 非第一个节点,此时就需要调整
    18. siftUp(i, e);
    19. return true;
    20. }

    这里的逻辑比较简单,因为这里使用数组实现的小顶堆(也即使用数组实现完全二叉树),而小顶堆的第一个节点是最小的,所以当0索引直接插入即可,非0索引就需要调整小顶堆。这里应该有很多读者是第一次见数组实现二叉树,所以这里把上文的二叉树进行扩展,把数组部分画上去。

    在看 siftUp调整方法前,我们看一下grow扩容方法, 因为里面有一个思路大家可以学习~

    1. private void grow(int minCapacity) {
    2. int oldCapacity = queue.length;
    3. // 容量小于64时,扩容为 oldCapacity + oldCapacity +2
    4. // 容量大于64,扩容为 oldCapacity + oldCapacity/2
    5. // 等同于,在容量小的时候,每次扩容大一些,当达到64这个阈值后,扩容小一些,要不然空间会太浪费了~
    6. int newCapacity = oldCapacity + ((oldCapacity < 64) ?
    7. (oldCapacity + 2) :
    8. (oldCapacity >> 1));
    9. if (newCapacity - MAX_ARRAY_SIZE > 0)
    10. newCapacity = hugeCapacity(minCapacity);
    11. // 数组拷贝迁移。
    12. queue = Arrays.copyOf(queue, newCapacity);
    13. }

    在每次扩容的时候,会去判断,当前容量是否大于64,如果小于64就直接 原大小 * 2 + 2 扩容,如果大于64以后直接 原大小 + 原大小/2 扩容。目的是为了在容量小的时候扩容大一些,减少扩容次数。在容量达到64阈值后,扩容小一些,减少内存浪费。

    下面开始讲解siftUp调整方法

    1. private void siftUp(int k, E x) {
    2. // 用户是否传入comparator比较器
    3. if (comparator != null)
    4. siftUpUsingComparator(k, x);
    5. else
    6. // 没传入就使用Comparable
    7. // 此时类需要实现Comparable接口
    8. siftUpComparable(k, x);
    9. }

    这里讲解siftUpComparable方法,本质上两个方法没任何区别~

    1. private void siftUpComparable(int k, E x) {
    2. Comparablesuper E> key = (Comparablesuper E>) x;
    3. while (k > 0) {
    4. // 拿到父节点
    5. // 因为是使用数组实现的一颗完全二叉树,所以直接-1 右移即可拿到当前插入节点的父节点
    6. int parent = (k - 1) >>> 1;
    7. Object e = queue[parent];
    8. // 与父节点做比较。
    9. if (key.compareTo((E) e) >= 0)
    10. // 达到用户的预期比较就直接break,要不然继续往父节点的父节点继续做比较,直到根节点
    11. break;
    12. // 没达到预期,所以把父节点插入到本次插入的节点的位置。
    13. queue[k] = e;
    14. // 拿到父节点的索引,继续往父节点的父节点做比较。
    15. k = parent;
    16. }
    17. // 插入
    18. queue[k] = key;
    19. }

    这里光看注释,肯定是看不明白的,所以以画图+注释来理解吧~

    PriorityQueue获取元素源码分析

    直接从poll方法入手~

    1. public E poll() {
    2. if (size == 0)
    3. return null;
    4. // 拿到最后一个节点的索引值。
    5. int s = --size;
    6. modCount++;
    7. // 因为第一个是小顶堆或者大顶堆要的数据。
    8. E result = (E) queue[0];
    9. // 拿到最后一节点
    10. E x = (E) queue[s];
    11. queue[s] = null; // help gc
    12. if (s != 0)
    13. // 调整
    14. siftDown(0, x);
    15. return result;
    16. }

    因为小顶堆或者大顶堆都是拿第一个元素,所以这里拿出第一个元素。但是每次拿完就需要调整小顶堆(调整完全二叉树),所以看到siftDown方法。

    1. private void siftDown(int k, E x) {
    2. if (comparator != null)
    3. siftDownUsingComparator(k, x);
    4. else
    5. siftDownComparable(k, x);
    6. }

    本质上这两个方法没任何区别,所以继续看到siftDownComparable方法

    1. // k为0
    2. // x是最后一个节点。
    3. private void siftDownComparable(int k, E x) {
    4. Comparablesuper E> key = (Comparablesuper E>)x;
    5. // 循环的次数
    6. // 是通过数组的大小 右移一位就可以知道树高了。
    7. int half = size >>> 1;
    8. while (k < half) {
    9. // 往下层找。
    10. int child = (k << 1) + 1;
    11. Object c = queue[child]; // 左子节点
    12. int right = child + 1; // 右子节点
    13. // 左右子节点比较,那个满足规范就作为父节点
    14. if (right < size &&
    15. ((Comparablesuper E>) c).compareTo((E) queue[right]) > 0)
    16. // 右节点满足于左节点
    17. c = queue[child = right];
    18. // 与最后一个节点比较后,达到预期直接退出
    19. if (key.compareTo((E) c) <= 0)
    20. break;
    21. // 替换
    22. queue[k] = c;
    23. // 下次循环的父节点
    24. k = child;
    25. }
    26. queue[k] = key;
    27. }

    因为每次poll取走的是第一个元素,所以需要调整整个小顶堆,而第一个元素是小顶堆的根节点,所以需要调整小顶堆找到一个符合的元素作为根节点。从根节点的左右子节点开始比较,左右子节点比较出预期的节点就作为新的根节点。预期的节点作为下次比较的父节点,通过父节点再找到他的左右子节点做比较,周而复始,直到最后一个节点。

    这里光看注释,肯定是看不明白的,所以以画图+注释来理解吧~

  • 相关阅读:
    ZCMU--5121: 打印机队列
    jfinal中如何使用过滤器监控Druid监听SQL执行?
    HTTP&Tomcat
    html和css基础练习
    java计算机毕业设计旅游系统源码+系统+mysql数据库+lw文档
    jbase实现通用码表
    跨链桥真的不能碰?一文详解跨链桥的分类以及过去、现在与未来
    RCNN系列1:RCNN介绍
    Java RMI 调用链与源码解析
    使用vue-cli搭建SPA项目及使用和路由及路由嵌套的使用
  • 原文地址:https://blog.csdn.net/qq_43799161/article/details/132734047