《徐徐道来话Java》:PriorityQueue和最小堆

在讲解PriorityQueue之前,需要先熟悉一个有序数据结构:最小堆

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

可以得出结论,如果一棵二叉树满足最小堆的要求,那么,堆顶(根节点)也就是整个序列的最小元素。

最小堆的例子如下图所示:

 

 

可以注意到,20的两个子节点3121,和它们的叔节点30并没有严格的大小要求。以广度优先的方式从根节点开始遍历,可以构成序列:

[10,20,30,31,21,32,70]

反过来,可以推演出,序列构成二叉树的公式是:对于序列中下标为i的元素,左孩子为left(i) = i*2 +1,右孩子为 right(i) = left(i)+1

现在可以思考一个问题,对于给定的序列,如何让它满足最小堆的性质?

例如[20, 10, 12, 1, 7, 32, 9],可以构成二叉树:

 

 

这里提供一个方法:

 

1、倒序遍历数列;

 

2、挨个进行沉降处理,沉降过程为:和左右子节点中的最小值比对,如果比最小值要大,则和该子节点交换数据,反之则不做处理,继续1过程;

 

3、沉降后的节点,再次沉降,直到叶子节点。

 

同时,因为下标在size/2之后的节点是叶子节点,无需比对,所以可以从size/2-1位置开始倒序遍历,节约执行次数。

 

应用该方法对之前的数列进行解析:

 

1、数列[20,10,12,1,7,32,9]长度为7,所以size/2 - 1 =2,倒序遍历过程是12 -> 10 ->20

 

2、12的左孩子为32,右孩子为912>9,进行沉降,结果如下图所示:

 

 

 

 

310的左孩子为1,右孩子为710 > 1,进行沉降,结果如下图所示:

 

 

 

 

 
   

420的左孩子为1,右孩子为920 > 1,进行沉降,结果如下图所示:

 

 

520的左孩子为10,右孩子为720 > 7,进行沉降,得到最终结果:

 

 

 

 

 
   

满足最小堆的要求,此时,得出的序列为[1,7,9,10,20,32,12]

该实现的流程也就是PriorityQueueheapify方法的流程,heapify方法负责把序列转化为最小堆,也就是所谓的建堆。其源码如下所示:

 

 

private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }

 

siftDown方法也就是之前提过的沉降

 

 siftDown(k,x)方法解析

 

 

siftDown这个方法,根据comparator成员变量是否为null,它的执行方式略有不同:

 

如果comparator不为null,调用comparator进行比较;

 

反之,则把元素视为Comparable进行比较;

 

如果元素不为Comparable的实现,则会抛出ClassCastException

 

不论哪种,执行的算法是一样的,这里只做Comparator的源码解析:

 

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;
}

 

注释已经解释清楚了代码的执行逻辑,其目的是把不满足最小堆条件的父节点一路沉到最底部。从以上代码可以看出,siftDown的时间复杂度不会超出O(logn)

 

siftUp(k,x)方法解析

siftUp方法用于提升节点。新加入的节点一定在数列末位,为了让数列满足最小堆性质,需要对该节点进行提升操作。

siftDown一样,它也有两种等效的实现路径,这里只做shifUpUsingComparator的解析:

 

  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;
    }

 

节点的插入,是在数列的尾端的,它很可能比父节点要小,不满足最小堆的定义,所以,需要做上浮的操作。

这里提供一个例子帮助理解,有最小堆数列[10203040305070],构成最小堆如下所示:

 

 

 

 

 

 

1、执行添加19,变为:

 

 
   
 
   

219<40,与40交换位置:

 

319<20,与20交换位置:

419>10,终止上浮操作,最后得到的数列为:

[1019302030507040]

满足了最小堆的性质。

 

posted @ 2016-10-09 10:52  荒土  阅读(1170)  评论(0编辑  收藏  举报