jdk源码分析之PriorityQueue

基本原理

PriorityQueue(优先级队列)的数据结构是最小堆,采用数组作为底层数据结构。
不同于普通的遵循FIFO规则的队列,PriorityQueue每次都选出优先级最高的元素出队,优先队列里实际是维护最小堆,通过最小堆使得每次取出的元素总是优先级最高的。

    /**
     * Priority queue represented as a balanced binary heap: the two
     * children of queue[n] are queue[2*n+1] and queue[2*(n+1)].  The
     * priority queue is ordered by comparator, or by the elements'
     * natural ordering, if comparator is null: For each node n in the
     * heap and each descendant d of n, n <= d.  The element with the
     * lowest value is in queue[0], assuming the queue is nonempty.
     */
    transient Object[] queue; // non-private to simplify nested class access

底层采用Object数组作为最小堆的实现方式
节点queue[n]的左孩子节点为queue[2*n+1] ,右孩子节点为queue[2*(n+1)]。
queue[0]表示优先级最高的节点

添加数据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;
    }

首先入口参数检查,PriorityQueue不支持存储null,所以如果给PriorityQueue添加null,抛出空指针异常,在错误发生后尽快检测出错误,符合Effective Java第38条原则《检查参数的有效性》
接着修改modCount,方便在迭代的过程中发生结构性修改可以抛出ConcurrentModificationException异常
因为是添加数据,所以需要判断是不是需要扩容

 if (i >= queue.length)
            grow(i + 1);

如果原队列为空,就把新添加的数据添加到队首

        if (i == 0)
            queue[0] = e;

否则就调用siftUp进行向上筛选

        else
            siftUp(i, e);

筛选分两种情况,因为有两种排序规则,按照数据自身的排序规则调用siftUpComparable或者按照外部规定的排序规则调用siftUpUsingComparator
siftUp(int k, E x)的含义为:在堆的数组下标k处,放置了一个数据x,此操作有可能破坏堆的性质,因此对堆进行调整

    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

siftUpComparable和siftUpUsingComparator代码逻辑类似,只是比较规则不同,因此只取其一进行分析

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

首先将需要插入的数据x强制转换为可比较的对象Comparable并保存为key
然后开始向上筛选,筛选终止的条件有两个,第一个是待插入的数据比堆顶元素都小,因此会筛选到下标k==0,此时结束循环;第二个是在向上筛选的过程中找到一个父节点比待插入节点小,此时不需要继续向上筛选了,break退出迭代。
迭代的过程中,首先获取父节点在数组中的下标位置并将父节点的数据保存为e

            int parent = (k - 1) >>> 1;
            Object e = queue[parent];

然后判断如果待插入节点比父节点大,break退出循环,待插入节点找到了自己的位置

            if (key.compareTo((E) e) >= 0)
                break;

否则的话,将父节点的数据(数组下标(k - 1) >>> 1)保存到正在筛选的节点(数组下标k),正在筛选的节点的下标k设为父节点下标(k - 1) >>> 1,进行下一次迭代

            queue[k] = e;
            k = parent;

迭代结束后,将待插入数据保存到在堆中合适的位置

        queue[k] = key;

获取并删除队首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);
        return result;
    }

首先检查队列容量,size==0表示队列此时没有数据,从没有数据的队列中读取数据直接返回null
然后对size和modCount进行修改,保存数组最后一个元素下标为s=size-1

        int s = --size;
        modCount++;

最小堆的性质保证优先级队列的队首元素queue[0]一定是最小的(优先级最高的),因此将队首元素保存为result

        E result = (E) queue[0];

队首元素出队列后,将队尾元素保存到队首,队尾元素置null加速垃圾回收,这个操作有可能破坏了堆的性质,因此需要向下筛选

        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
            siftDown(0, x);

siftDown(int k, E x)的含义为:将优先级队列数组下标为k处的数据设置为x,此操作有可能破坏了堆的性质,因此需要调整。

    private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

和向上筛选一样,分两种排序规则进行分类处理,但是两种情况代码大同小异,只是比较规则不一样,选其一进行分析

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

首先找到叶子节点的最小下标,也就是第一个叶子节点的下标,并保存为half。

        int half = size >>> 1;

最小堆也是一个完全二叉树,完全二叉树的性质决定了size的一半减一(size/2-1)是非叶子节点的最大下标,size的一半(size/2)是叶子节点的最小下标
然后向下筛选,要么找到了比筛选到了叶子节点,要么找到了数据x在堆中的合适位置,这两种情况都停止筛选
筛选的过程中,需要将待筛选节点和两个孩子节点进行比较,如果待筛选节点比两个孩子都小,则向下筛选结束,否则交换较小孩子和待筛选节点的位置,进行下一轮筛选。

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

然后左右孩子比较,设置左右孩子的较小者为c,然后用c和待筛选节点x进行比较

            if (comparator.compare(x, (E) c) <= 0)
                break;

如果待筛选节点x比左右孩子的较小者c都小,说明待筛选节点x找到了自己的合适位置,停止筛选,break退出循环
否则进行下一轮筛选

            queue[k] = c;
            k = child;

将左右孩子较小者c保存到父节点queue[k] ,设置下一轮待筛选节点下标k为左右孩子较小者下标,进行新的一轮筛选
筛选过程结束,将数据x放置到自己在最小堆中的最终位置

        queue[k] = x;

建堆过程

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

建堆的过程只从非叶子节点开始筛选,因为筛选的过程中会处理到叶子节点。并且筛选的过程是倒着来的,从最后一个非叶子节点开始,一直处理到数组中第一个节点,这种自底向上的思想类似于动态规划。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值