PriorityQueue源码分析

PriorityQueue

从名字上来看,PriorityQueue叫做优先队列,它的另一个名字叫堆(heap),这个名字更熟悉。它和普通的队列不太一样,它里面存储的元素具有一定的顺序性,而不是简单的“先进先出”。它的实现原理通常是二叉堆,二叉堆是一个完全填满的二叉树。这里不做过多的介绍,都是数据结构的知识。来介绍一下优先队列的通常实现。

优先队列的整体结构虽然是一个二叉堆,也是树结构,但是存储元素的却是用数组实现的,不是通常用链表作为树的节点。因为这里隐含着一个规律,用数组的索引可以很轻松的获取它的父节点和儿子节点的索引。举个例子。

在这里插入图片描述
这就是一个优先队列,大优先。整体上是一个满二叉树,下面的图表示在数组中的排列。

假设优先队列的第一个元素的索引就是数组中索引为0的元素。其中优先队列的一个节点对应数组的索引是parent,左儿子节点索引是left,右儿子节点是索引是right,则存在下面的关系:

  • left=2parent+1,right=2parent+2
  • parent=left/2,parent=(right-1)/2

特性

完整二叉树: 每一层都是二叉树 都被填满 ,除了最低/最下面的一层,并且所有的最底层顶点都尽力向左靠拢。

最大二叉堆特性: 每个顶点的父元素 - 除了根元素 - 都比当前元素的值要大。这种定义比下面这种更容易验证:一个顶点的值 - 除了叶顶点 - 必须必它的一个或者两个子元素要大。

举例

@Test
public void test() {

    PriorityQueue<Integer> queue = new PriorityQueue<>();
    queue.add(9);
    queue.add(1);
    queue.add(3);
    queue.add(6);
    queue.add(0);

    System.out.println(Arrays.toString(queue.toArray()));
}

测试结果:[0, 1, 3, 9, 6]

树结构:

在这里插入图片描述
简单介绍后,我们直接看看jdk的实现代码。

源码

private static final int DEFAULT_INITIAL_CAPACITY = 11;

transient Object[] queue;

private final Comparator<? super E> comparator;

内部定义了一个数组,数组得话就涉及到扩容,默认得容量是11。因为优先队列的元素是需要排序得,不然怎么知道哪个元素优先呢?排序就涉及到两个接口Comparable和Comparator,这在需要排序的集合类中是通用的套路,如果元素的类型实现了Comparable接口,则使用Comparable排序,否则通过定制Comparator实例排序。

还是先来看add方法。

public boolean add(E e) {
    return offer(e);
}

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

add内部调用的offer方法,因为PriorityQueueu也是无界的队列,索引add和offer几乎没有区别。内部实际上是数组存储元素,随着元素的增多肯定涉及到扩容。

当元素个数大于等于队列长度时,进行扩容。扩容的方法是grow。

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

方法传递过来的满足需求的最小容量,是在原来的长度上加一。这个方法判断了一下,如果当前的队列长度小于64,将长度变为两倍的原有长度再加2,如果长度大于等于64,1.5被扩容。

继续看offer的下面代码。

如果是第一次插入元素,不管元素时什么直接写到数组的索引0位置。否则,执行siftUp方法。siftUp方法有个专业名词,叫“上滤”,这里判断了一下采用哪种接口的排序,实际的逻辑是一致的,我们来看 siftUpComparable(k, x);

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}
private void siftUpComparable(int k, E x) {bu
    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)  //比较大小,如果插入的元素比父节点大,就不需要再不比较了,新元素直接作为父节点的子节点就行了。
            							//否则,说明新元素需要往上移动,用新元素替换父节点,循环比较,直到找到比父节点大的节点终止或者已经到了
            							//根节点,就是while的循环条件不满足
            break;
        queue[k] = e;//将父节点替换为新元素
        k = parent;  //继续向上比较
    }
    queue[k] = key;  //比较结束后需要替换节点
}

我们以上面的例子来说明,类型是PriorityQueue,PriorityQueue没有配置Comparator比较接口,默认是小优先,就说父元素要大于子元素,根元素最小。

像上面的例子一样,我们假设已经插入了几个节点0, 1, 3, 9, 6,树结构如下图,现在插入节点2,我们跟踪一下插入过程。

在这里插入图片描述

再来看remove和poll方法。

PriorityQueue中并没有实现remove方法,而是继承了抽象类AbstractQueue的remove方法,AbstractQueue的remove方法如下。

public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

内部调用的是poll方法,我们关注poll方法。

public E poll() {
    if (size == 0)
        return null;
    int s = --size; 
    modCount++;
    E result = (E) queue[0];  //获取根节点
    E x = (E) queue[s];  //获取最后一个节点
    queue[s] = null;
    if (s != 0)  //数组中超过一个元素
        siftDown(0, x);  //移除数组中第0个元素,也是树的根
    return result;
}

移除的时候并不会将数组缩容,只是将队列的size减1。如果只有一个元素的话,直接返回,否则执行“下滤”操作。

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
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; //根节点左儿子索引值
        Object c = queue[child];  //根节点左儿子值
        int right = child + 1;    //根节点右儿子索引值
        if (right < size &&
            ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)  //如果存在右儿子的话,比较左儿子和右儿子,如果右儿子小,获取右儿子																		  //的值,这里意思是,获取儿子中较小的节点值,保存在变量c
            c = queue[child = right];  
        if (key.compareTo((E) c) <= 0)  //如果最后一个节点的值比c小,直接退出循环,执行queue[k] = key;用最后一个元素替换原来的根节点,结束	
            break;
        queue[k] = c;  //否则用c的值替换根节点的值,但是此时需要c节点为跟节点,循环执行上面的逻辑直到直到退出循环
        k = child;
    }
    queue[k] = key;
}

这是一个通用的方法,可以移除任意树的节点,在优先队列中,移除任意节点不常用,因为值确定根节点最大或者最小,并不知道第二大,第三大的节点在数组中的哪个位置。我们分析移除的根节点。方法传入的是0,最后一个元素的值。看一下这个过程,假设当前的树结构如下。

在这里插入图片描述
我们移除最小节点0

在这里插入图片描述
解析来看element和peak方法。

public E element() {
    E x = peek();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

public E peek() {
    return (size == 0) ? null : (E) queue[0];
}

这两个方法没什么可说的,直接获取数组中第0个元素。

最后需要看一下的是有参构造器。

public PriorityQueue(Collection<? extends E> c) {
    if (c instanceof SortedSet<?>) {
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        initElementsFromCollection(ss);
    }
    else if (c instanceof PriorityQueue<?>) {
        PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        initFromPriorityQueue(pq);
    }
    else {
        this.comparator = null;
        initFromCollection(c);
    }
}

这个构造器传入的是个集合,当然集合肯定分为有序的无序各种,这里按类别分别处理,但最终都要调用initFromCollection方法处理

private void initFromCollection(Collection<? extends E> c) {
    initElementsFromCollection(c);  //从Cellection初始化元素
    heapify();  //堆化
}
private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

(size >>> 1) - 1表示的是最后一个节点的父节点,然后该遍历执行“下滤”操作,也就是如果父节和儿子节点比较,如果比儿子小,将儿子节点置为父节点,这样遍历下来,构建出了完整的堆。

总结

PriorityQueue通过以二叉堆的结构实现了优先队列,底层数据结构是数组,可以保证按照一定顺序出队,线程不安全。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值