PriorityQueue原理解读

PriorityQueue

优先队列本质上就是一个最小堆。所以先讲讲堆的性质:

     堆(也叫优先队列),是一棵完全二叉树,它的特点是父节点的值大于(小于)两个子节点的值(分别为大顶堆和小顶堆)。需要注意的是堆中任一子树也是堆。下图中给出了从二叉树角度来看的大顶堆。
    
     如果从数组角度来看,那么该大顶堆如下:
     按照编号来看,可以发现一个很有意思的规律:
         
左子结点的编号=父结点编号 * 2 
右子结点的编号=父结点编号 * 2 + 1

         考虑到数组从0开始,所以用java代码表示为:
          
public static int left( int i) {
   return i *  1;
}
public static int right( int i) {
   return i *  2;
}

调整堆

     假定我们需要建立一个大顶堆,那么对于这么一个最大堆来说,父节点的值必须要比它的子结点要大。如果不满足这个要求,就需要调整。如下图所示:
     
     这时需要调整结点4的位置,然后与子结点比较,和最大的子结点交换位置。
     
     
     经交换后发现交换后的结点符合要求了,可以结点4依然不符合要求,所以再次进行交换。
     再次交换后最终符合了大顶堆的要求,主要过程如下:

1. 比较当前结点和它的子结点,如果当前结点小于它的任何一个子结点,则和最大的那个子结点交换。否则,当前过程结束。

2. 在交换到新位置的结点重复步骤1,直到叶结点。

建最大堆

     建最大堆之前需要考虑的几个因素:1、是从前往后开始调整还是从后往前开始调整?2、从哪里结点开始调整。
     
     我们先来讨论第一个因数:如果是从前往后开始调整,如图所示:
          如果我们从跟结点开始,根结点元素4比它的两个子结点都大,不需要调整。而再往后面的时候子结点1调整之后被换成16。这样就出现了它的子结点比它还大的情况,因此从前往后调整的过程不可行。
          而采用从后往前进行调整,能够保证当上面的父结点调整的时候,下面的子数已经满足最大堆的条件了。
     第二个因素,从哪个结点开始调整:
          如果直接从尾结点进行调整,毫无疑问是有问题存在的,因为可能有相当一部分的结点是没有必要的叶节点,这些结点根本没有子结点,所以是没有必要的。那么其实可以追溯到最后一个结点的父结点进行调整,这个元素的位置索引即是尾结点/2。
理解了构建堆的过程我们来看看PriorityQueue。
     

PriorityQueue

存储结构-字段 

PriorityQueue中主要有三个字段
默认大小为11,有一个queue数组用来存储数据,size表示当前结点个数。注意的是queue也是被transient修饰的,这一点已经在ArrayList和LinkedList剖析里面谈过了,这里不再赘述。

功能实现-方法

构造方法

     PriorityQueue有很多的构造方法,默认为自行构造一个大小为11的Object数组。也可以传入一个Collection的实现类。在传入Collection的实现类时,被先都调用heapify方法。

建堆

     建堆就是调用的heapify方法,源码如下:
private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}
   就是和上面的建堆过程一样,我们来看看shiftDown方法
private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

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


     如果有comparator那么就调用siftDownUsingComparator方法,否则调用siftDownComparable方法。其实就是一个调整堆的过程。这里使用的是循环的版本没有使用递归,代码不难,只需要理解是从最后一个结点的父结点开始从顶向下调整就可以了。

扩容

      堆里面的数组长度不是固定不变的,如果不断往里面添加新元素的时候,也会面临数组空间不够的情形,所以也需要对数组长度进行扩展。 数组长度扩展的方法如下:
private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                     (oldCapacity + 2) :
                                     (oldCapacity >> 1));
    // overflow-conscious code
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    queue = Arrays.copyOf(queue, newCapacity);
}
和ArrayList中不同的是,这里当容量较小的时候(<64),每次只扩大2。

新增

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}
可以看出每次都会调用grow(i+1),这样保证了PriorityQueue永远不会溢出。另外,当插入一个新的数据时,因为是最小堆,使用的就是从底向上来进行调整。siftUp代码如下:
private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}

删除

private E removeAt(int i) {
    // assert i >= 0 && i < size;
    modCount++;
    int s = --size;
    if (s == i) // removed last element
        queue[i] = null;
    else {
        E moved = (E) queue[s];
        queue[s] = null;
        siftDown(i, moved);
        if (queue[i] == moved) {
            siftUp(i, moved);
            if (queue[i] != moved)
                return moved;
        }
    }
    return null;
}
需要注意的是要调用siftDown后还需判断一次,如果不成功那么就调用siftUp

总结

     其实只需要知道PriorityQueue是小顶堆,然后知道堆调整和建堆时候的从哪里开始。堆的概念熟悉后,阅读源码就没什么难度了。 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值