阻塞队列之PriorityBlockingQueue实现算法详解

1. 简介

PriorityBlockingQueue阻塞队列可以进行排序,并且可以自行传入比较器进行排序。举个例子,比如我们往这个队列中先后存放了5、10、1、3这4个int值,存放完之后,我们进行取的操作,首先会取到的是1,然后依次是3、5、10,将会按照顺序取出;再比如我们存放的是User对象,我们还可以让User实现Comparable接口,按照我们自定义的排序规则进行依次取出。

本文只分析实现的排序算法,关于所有阻塞队列的源码分析,等近期有时间了再写分析解读。

2.分析

我们进入PriorityBlockingQueue源码,找到offer()这个入队的方法,核心排序方法为siftUpComparable,传入的参数依次为:当前队列中的有效元素数量、要插入到队列中的新元素、当前队列中的元素数组。siftUpComparable方法采用的是最小堆的排序方法。关于最小堆的定义,引用一下百度百科的定义:最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。

我这里结合一个例子来分析一下最小堆的构建,其实这也是PriorityBlockingQueue的核心算法。

假设我们要用20,40,35,10,50,30,0这几个数字进行最小堆的构建。

1. 我们先拿到第一个数字20,当前堆为空,这里没什么好说的,就是一个20的节点。然后我们拿到数字40,40要作为20的左子节点插入,我们要确保所有子节点的值均大于父节点,这里用40和20比较,发现40比20大,所以40可以作为20的左子节点,同样,35也可以作为20的左子节点。此时结构如下所示:

2.然后拿到10这个数字,按顺序,10应该放在40的左子节点的位置,但是由于要创建的是最小堆,这里的10比它的父节点40小,所以我们把40和10交换位置,如下:

然后我们发现10比20还要小,再继续调整为如下所示:

2.再拿到50这个数字,可以顺利放在20的右子节点的位置,再来是30,同理,因为30小于35,所以30和35需要换个位置,如下:

3.最后是数字0,同理,依次先和30交换位置,然后再和10交换位置,得到最终的结构。

用数字表示,即 [0, 20, 10, 40, 50, 35, 30]。这就是一个最小堆,堆的根节点是最小值。

来看方法实现:

    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];
            if (key.compareTo((T) e) >= 0)
                break;
            array[k] = e;
            k = parent;
        }
        array[k] = key;
    }

入队的位置为k,元素为x,数组为array。

第一次入队,k为0,不进入while循环,array[0] = x,入队结束。

第二次入队,k为1,进入while循环,得到父节点位置,parent = (1-1) >>> 1 = 0,即父节点为array[0],比较新元素和父节点的大小,如果新元素较大,while循环结束,array[1] = x;如果新元素比父节点小,array[1] = 父元素,k = 0,while结束,array[0] = 新元素,这时,新元素成了堆的根节点,父元素成了新元素的左子节点。

总结:每次入队,得到当前要插入的位置k的父节点parent,将父节点的位置和新元素的值进行比较,如果新元素比父节点小,则需要将父节点复制一份,放到k位置,再比较parent位置的父节点的值和新元素的值进行比较,如果还是新元素比较小,再将这个parent位置的父节点复制一份到parent位置....一直从下至上比较,直到新元素的值大于某个父节点时,停止比较,将新元素插入。

入队结束。

我们再来看出队。出队的方法为poll(),跟踪到dequeue(),出队的时候直接取array[0]返回,这个值必定是最小值,也就是我们上文中的数组的中第一个元素0,然后将数组最后一个元素取出,赋值给变量x,再将数组最后一个位置置为null,然后将0、变量x、元素数组(即上文构建的最小堆用数组表示)、队列元素数量n传入方法siftDownComparable进行处理。

    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)
                siftDownComparable(0, x, array, n);
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }

这里需要选举出新的最小值,同样我们先讲思路。

接着上文的最小堆,我们将最后一个节点30拿出来先存着,要找一个合适的地方放。

1. 从根节点0开始,依次比较它的左子节点和右子节点,取较小的节点,将这个较小的节点设为新的父节点,这里比较的是20和10,10比较小,所以我们直接把10拿一份放到0的位置,然后准备在10原先的位置试图放入被我们拿出来的30节点。

2. 当我们试图放入的时候,还需获取试图放入的这个位置的子节点的最小值(如果右子节点存在就取最小值,如果右子节点不存在,则左子节点就直接是最小值),如果比两个子节点的最小值还小,那就可以放入,然后整个流程结束。反之,如果最小值大,则不能放入,继续将这个最小值提上来,然后再到这个最小值原先的地方进行试图放入的操作,也就是重复这一步。

所以这里的30是可以成功放入的,因为10只有一个子节点,所以35就是最小值,30小于35,直接放入,流程结束。

再来看一种复杂的情况该如何操作:

我们来看代码实现:

    private static <T> void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
        if (n > 0) {
            Comparable<? super T> key = (Comparable<? super T>)x;
            int half = n >>> 1;           // loop while a non-leaf
            while (k < half) {
                int child = (k << 1) + 1; // assume left child is least
                Object c = array[child];
                int right = child + 1;
                if (right < n &&
                    ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                    c = array[child = right];
                if (key.compareTo((T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = key;
        }
    }

入参:k为0,x为要插入的元素(从数组中拿来下的尾节点),array为数组,n为元素个数。

首先构造一个while循环,参数k为0,k < half 表示从0号节点开始遍历,依次遍历所有的父节点。

比如,如果一共有5个节点,k < half  =>  k < 2 ,k=0、1。第一次遍历根父节点(子节点为2、3号元素),第二次遍历2号元素(2号元素为4、5号元素的父节点)。

遍历的时候依次取到父节点的左子节点和右子节点,这里用局部变量表示为child(左子节点)、right(右子节点),如果右子节点存在,并且左子节点的值大于右子节点,则将child赋值为右子节点的下标,c赋值为右子节点的值;如果右子节点不存在,则child还是为左子节点的下标,c为左子节点的值,这一步其实就是在取子节点中的最小值和最小值的下标(也就是尾节点试图插入的位置)。

取到最小值c和最小值下标child后,比较尾节点key和最小值c,如果尾节点key比较小,代表当前父节点k的左子节点和右子节点的值均小于尾节点key,满足最小堆的要求,跳出循环,将k位置插入尾节点key。如果发现尾节点key的值比最小值c大,则在尾节点试图插入的位置k处,放入最小值c,然后在最小值c的下标child出试图插入尾节点,再重复比较child的子节点的最小值和尾节点的值...直到发现某一个试图插入位置,尾节点的值小于这个位置的左子节点和右子节点中的最小值,循环结束。

3.结语

计划最近有空的时候,把所有的阻塞队列源码都研究一遍。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值