JDK源码---PriorityBlockingQueue

648 阅读4分钟

用PriorityBlockingQueue实现最小优先队列, 需要考虑以下几个方面: 扩容入队出队

PriorityBlockingQueue内部用数组实现了堆排序.

图在末尾, 代码看不明白就看图

一、堆排序

1.1 堆排序概念

  最小堆是一种经过排序的完全二叉树, 其中任一非终端节点的数据值均不大于其左子节点和右子节点的值

**换句话说: **

  • 1、数组来实现二叉树, 所以满足二叉树的特性
  • 2、根元素是最小的元素, 父节点小于它的两个子节点
  • 3、树中的元素是相对有序的
1.2 如何实现堆的相对有序

  插入元素时, 插入到数组中的最后一个元素的后面, 然后与该节点的父节点比较大小, 如果插入的元素小于父节点元素, 那么与父节点交换位置, 然后插入元素交换到父节点位置时, 又与该节点的父节点比较, 直到大于父节点元素或者到达堆顶, 该过程叫做 上浮, 即插入时上浮.

  移除元素时, 只能从堆顶移除元素, 再取最后一个元素放到堆顶中, 然后堆顶节点与子节点比较时, 先取子节点中的较小者, 如果堆顶节点大于较小节点, 那么交换位置. 此时堆顶节点元素交换到较小节点上, 然后再与其较小节点比较, 直到小于较小节点或者到达叶子节点为止, 该过程叫做 下沉, 即移除元素时下沉.

二、PriorityBlockingQueue入队操作

2.1 PriorityBlockingQueue.offer
public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    // 1.入队时, 先判断是否需要进行扩容
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
    try {
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
        	// 2.数据入队
            siftUpComparable(n, e, array);
        else
            siftUpUsingComparator(n, e, array, cmp);
        // 3.size记录当前数组中有效元素的个数
        size = n + 1;
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}

1、入队之前先判断当前数组是否需要扩容

2、size记录的是数组中元素的个数

2.2 PriorityBlockingQueue.siftUpComparable
private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = array[parent];
        // compareTo由x实现, 如果计算方式是return x.priority - e.priority, 则为最小堆
        // 即优先级值最小的先出队
        if (key.compareTo((T) e) >= 0)
            break;
        array[k] = e;
        k = parent;
    }
    array[k] = key;
}

这里的计算方式: 先将要入队的元素插入到数组末尾, 然后获取元素的父节点的索引值, 父节点的定义规则是当前元素在数组中的索引值(index)-1 >>> 1, 入队元素与其父节点进行比较, compareTo < 0, 则将父子节点进行替换, 顺序交换完成之后, 继续按这种方式向上遍历比较.

**备注: **新入队元素只与其父节点进行比较, 并不与其他节点比较, 所以该数组不是有序数组.

三、出队

3.1 dequeue
private E dequeue() {
    int n = size - 1;
    if (n < 0)
        return null;
    else {
        Object[] array = queue;
        E result = (E) array[0];
        E x = (E) array[n];
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
        	// x: 数组最后一个元素
            siftDownComparable(0, x, array, n);
        else
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n;
        return result;
    }
}
3.2 siftDownComparable
// 1.T x对应数组中最后一个元素
private static <T> void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
    if (n > 0) {
        Comparable<? super T> key = (Comparable<? super T>)x;
        // 2.初始化half为x元素的父节点
        int half = n >>> 1;          
        while (k < half) {
        	// 3.取当前节点的左子节点(第一次为根节点)
            int child = (k << 1) + 1; 
            // 4.当前节点的左子节点的值赋值给Object c
            Object c = array[child];
            // 5.取右子节点的索引值
            int right = child + 1;
            if (right < n &&
                    ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                // 6.如果左子节点 > 右子节点, 右子节点的值赋值给Object c
                c = array[child = right];
            if (key.compareTo((T) c) <= 0)
            	// 7.如果最后一个节点的值 < 左右子节点的值, 操作结束, 直接return
                break;
            // 8.最后一个元素移动到堆顶后, 与其子节点中较小者进行比较
            array[k] = c;
            // 9.继续向下遍历
            k = child;
        }
        // 10.遍历结束, 将最后一个元素的值放到指定的位置
        array[k] = key;
    }
}

单纯的看代码, 看文字很懵逼, 画个图方便日后回顾

四、流程图

在看下图之前, 需要明白一件事, 数组结构才是PriorityBlockingQueue中元素的真实结构, 二叉树只是该数组的映射, 画出数组的二叉树映射结构图, 是为了方便理解

4.1 数组实现最小堆的结构图

五、入队流程图

**结合上图可知: **用数组实现最小堆, 虽然出队是有序, 最小值先出队, 但是数组本身并不是有序数组

向数组中插入值为1的元素

5.1 val(1)对应index(13)

将val(1)插入到数组的末尾, 对应index = 13

5.2 val(1)与index(6)比较

父节点索引值parentIndex = 6, 对应val(12), 将val(1)与val(12)进行比较, 发现val(1)比val(12)小, 将val(1)与val(12)的位置进行置换, 置换以后val(1)对应index(6)

置换后如下图:

5.3 val(1)与index(2)比较

val(1)与val(12)交换位置以后, val(1)对应index = 6, 其父节点parentIndex = 2, 即val(10), 此时val(1) < val(10), 所以val(1)与val(10)进行位置互换, 置换以后val(1)对应index(2)

置换后如下图:

5.4 val(1)与index(0)比较

val(1)与val(10)交换位置以后, 其索引值对应index = 2, 其父节点parentIndex = 0, 即节点val(2), 然后val(1)与val(2)进行比较, val(1) < val(2), 所以val(1)与val(2)交换位 置, 互换以后val(1)对应index(0)

置换后如下图:

5.5 val(1)入队完成

六、出队流程图

根节点的元素始终是最小元素, 而且对应index(0), 出队取index(0)元素, 然后将数值末尾元素放置index(0)处, 然后依次与其子节点进行比较

6.1 取index(0)元素

6.2 val(12)元素放置index(0)处

将数值末尾元素放置index(0)的位置, 数组最后一个元素val = 12, 也就是将val(12)从index(13)移至根节点index(0)处

移动后如下图:

6.3 val(12)元素与index(1)、index(2)比较

index(0)其左右子节点分别为childIndex(1)、childIndex(2), 对应的值分别为val(3)、val(2), val(3) > val(2), 所以讲val(12)与其右子节点childIndex(2)进行比较, val(12) > val(2), 将val(12)与val(2)进行置换, 置换后其index = 2

置换后如下图:

6.4 val(12)元素与index(5)、index(6)比较

index(2)其左右子节点分别为childIndex(5)、childIndex(6), 对应的值分别为val(11)、val(10), val(11) > val(10), 所以讲val(12)与其右子节点childIndex(10)进行比较, val(12) > val(10), 将val(12)与val(10)进行置换, 置换后其index = 6

置换后如下图:

6.5 出队结束

完成一次出队操作之后, 当前数组结构如下图所示