PriorityQueue源码分析

上篇文章,我们使用PriorityQueue解决了TopK问题,其中有个神奇的操作就是,当从PriorityQueue插入或者删除一个元素时,他总能通过一定的方式调整,使得堆顶的元素是这个PriorityQueue的最值,这一节我们就来研究一下PriorityQueue底层是用什么存储的数据,又是怎么调整数据,使得其满足以上特性。

预备知识

如何把一个数组想象为完全二叉树呢?

其实这个数组就是完全二叉树层序遍历的结果。如果我们把完全二叉树的节点按照层序遍历的顺序,依次标记为0, 1, 2, 3, …, n,会发现,标号为i的节点,正是数组下标为i的值。

堆的定义:n个元素的序列{k0,k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。
k i ≤ k 2 i + 1   且   k i ≤ k 2 i + 2     或 者     k i ≥ k 2 i + 1   且   k i ≥ k 2 i + 2 k_i \leq k_{2i+1}\ 且\ k_i \leq k_{2i+2}\ \ \ 或者\ \ \ k_i \geq k_{2i+1}\ 且\ k_i \geq k_{2i+2} kik2i+1  kik2i+2      kik2i+1  kik2i+2

堆的性质:

  • 堆总是一颗完全二叉树
  • 父节点的值总是小于等于(或者大于等于)其两个子节点的值

有一点值得注意,我们并不限制左右孩子的大小,就是说左孩子并不一定要小于等于(或者大于等于)右孩子!

一、底层的数据结构

二话不说,我们直接打开Java8的源码:

public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable {

    private static final int DEFAULT_INITIAL_CAPACITY = 11;

    transient Object[] queue; // non-private to simplify nested class access

    private int size = 0;

    private final Comparator<? super E> comparator;
}
  • DEFAULT_INITIAL_CAPACITY字段

    创建一个队列,默认情况具有11个元素;

  • queue字段

    是个Object类型的数组(嗯?为什么是Object类型,而不是E类型呢???)

    transient关键字是表示这个字段不会被序列化,很多说法是出于数据安全考虑(比如说密码)

  • size字段

    用于记录当前队列里有多少个数据

  • comparator字段

    比较规则。将根据这个字段指定的比较规则,对数据进行排序;

    如果不指定比较规则,将采用E类型自然大小(这个翻译感觉怪怪的)作为排序规则;

二、如何构造PriorityQueue

通过PriorityQueue的成员变量,我们已经大致猜到了,会有那些构造函数,比如没有任何参数的构造函数、只指定初始大小的构造函数、只指定比较规则的构造函数、即指定初始大小,又指定比较规则的构造函数、拷贝构造函数(其实这是C++的概念,习惯这么叫了),或者通过其他集合创建一个PriorityQueue

当通过集合创建PriorityQueue时,我们看个例子:

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

对于使用集合创建PriorityQueue,首先通过CollectiontoArray方法将元素转为数组,存储在数据成员queue里,之后更新数据成员size;如果集合类型不是SortedSetPriorityQueue的实例,还会调用heapify()方法调整数据成员queue中的元素,使其满足的特性。

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

heapify()方法内部,我们发现是循环调用siftDown()方法,从某个节点开始向下调整

我们看下默认的向下调整是如何实现的:

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

从左右孩子中选取一个较小的,与父节点比较,如果父节点比 较小的哪个孩子 还小,就用子节点的值覆盖父节点的值(我们用key保存了入参的值,不用担心被覆盖),然后从较小孩子位置,继续向下调整,直到调整到最后一个非叶子节点的下一个位置。如果不存在交换,直接退出循环,此时k标记的是哪个孩子节点位置,也是key的合适位置,将key放到k的位置上,这次向下调整就完成了。

回到我们heapify()方法,有两点需要我们注意:

  1. 开始调整的位置?

    最后一个非叶子节点的位置(想想完全二叉树的性质!),只有这样,才能保证其有左右孩子;

    回顾我们向上调整的过程,最后一个非叶子节点的值,极有可能改变。

    经过一次调整,最后一个非叶子节点的值,就是这颗子树的最小值(因为最多只有两个孩子,经过两次比较,一定可以得到一个最小的)。

  2. 为什么要循环调整?

    经过一次调整,我们可以让非叶子节点的值,调整为这颗子树的最小值,如此向着根节点依次调整,就可以将这颗完全二叉树调整为堆。

    再声明一次,将数组调整为堆之后,并不能保证数组是有序的,只能保证第一个元素是这个数组里最小的

向下调整是堆最重要的操作之一!!

三、向PriorityQueue插入元素,会发生什么

PriorityQueue提供了两种方法来插入元素,分别是add()offer(),其实都是调用offer()方法:

    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;
    }
  1. 判空检查

    如果插入元素为空,会抛NPE;

  2. 判断空间是否充足

如果不充足,将会扩容。元素个数小于64,双倍扩容;否则容量增加50%。

先申请空间,再拷贝元素。

  1. 插入元素

    如果是个空的PriorityQueue,直接插入;

    如果非空,调用siftUp()方法向上调整;

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

在插入元素之前,保存元素的queue数据成员已经满足堆的特性,在插入元素时只要经过适当的调整,使得其继续满足堆的性质即可。数据成员size的大小,既可以表示当前数组有多少元素,同时可以标记下一个元素插入的位置(也许这个位置并不一定适合待插入元素,但是我们一定会将一个元素放到这个位置)。

从最后一个叶子节点开始,如果待插入元素的值比父节点的值小,就将父节点的值搬到子节点(此时父节点也许就是一个合适的位置来放置待插入元素,但是我们需要继续判断),于是循环进入父节点,继续寻找合适的插入位置,直到根节点(一定会有一个合适的插入位置),插入元素。

四、删除元素

PriorityQueue提供了poll()方法,在获取队头元素(队列的第一个元素,在在里就是堆顶元素)的同时,删除该元素;于此之外,还提供了remove()removeEq()方法来删除元素,他们的区别是:

  • remove方法

    删除值相等(equals方法)的第一个元素

  • removeEq方法

    删除相等(==)的某个对象,一般是iterator.remove(object)

  • 相同点

    他们底层都调用removeAt方法

    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()方法从i位置向下调整,而poll()方法,从0位置开始向下调整。

五、其他操作

  1. peek方法

    只读取元素,不移除元素,如果PriorityQueue为空,将返回null

  2. contains方法

    判断PriorityQueue是否包含值相等的元素,是,返回true,否则返回false;

  3. iterator方法

    返回一个迭代器,方便循环遍历(感觉没卵用!)

  4. size方法

    返回PriorityQueue中,有多少元素;

  5. clear方法

    PriorityQueue清空,是直接把底层数组设置为null,同时把size设置为0,不是移除数组的所有元素,也就是说,底层容量变了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值