基于JDK1.8剖析PriorityQueue源码

PriorityQueue 优先队列

 

其内部其实是一个最小堆

最小堆具有三个特性:

1.最小堆是完全二叉树。

2.任意节点如果有左右孩子,那么它的值必须小于或者等于左右孩子的值。

3.任意非叶子节点的左右子树也都是堆。

图示举例:

而最小堆可以由数组或者二叉链表实现,通过源码可知PriorityQueue是使用的数组实现最小堆。

transient Object[] queue;

因此queue[n]节点的左孩子为queue[2n+1],右孩子为queue[2n+2]。但需要小心是否数组越界!

也可以因此推导,queue[m]的父节点下标是  queue[(m-1)/2] 。因为Java中整数运算存在向下取整现象,因此针对左右孩子均满足。但注意当m=0时 (m-1)/2为0,但根节点不存在所谓的父节点,这样计算是错误的。

 

当对PriorityQueue进行迭代遍历时会发生什么?

调用PriorityQueue的 iterator()方法时,返回的是一个名为Itr的内部类,这个类实现了Iterator接口并且被声明为private final class 。并在内部利用cursor指针迭代内部数组queue。

简单来说就是对queue数组进行了一次遍历。

 

当对PriorityQueue进行插入时会发生什么?

调用add()或者offer()方法均可,因为本质上add()内部就是调用的offer()函数。

插入元素时分情况考虑,并且总是插入到堆的末尾。

 

堆为空:

直接插入,无需扩容,无需调整。(构造函数中初始化queue,默认初始容量11)

堆不为空:

1.判断是否需要扩容,需要的话则调用grow方法。

2.插入元素后可能会破坏堆的平衡,调用siftUp(上滤)方法调整堆,直到平衡。

 

    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length) //实际大小超过queue容量时进行扩容
            grow(i + 1);
        size = i + 1;
        if (i == 0)            //堆为空时
            queue[0] = e;
        else                   //堆不空时,上滤调整
            siftUp(i, e);
        return true;
    }

 

gorw扩容原理

与ArrayList扩容方式类似

若数组容量小于64,则扩大为原来的2倍

若数组容量大于等于64,则扩大为原来的1.5倍

最后还要检查新容量不能大于MAX_ARRAY_SIZE (为int的最大值-8,-8是在序列化的情况下有的JVM实现需要预留一个头部写入数组大小,不-8的话有可能造成数组越界)

且将原数组内容拷贝到新数组中,并丢弃原数组中内容

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

 

siftUp()原理

自底向上,不断地比较,交换

 

这里假设插入的元素为e

首先将e插入到queue末尾,再不断地比较e与父节点p的关系

若 e < p则交换,且将指针回溯到p

若e >= p则表示当前位置合适,并终止

//在k位置插入元素x 一般情况k是size+1   时间复杂度O(logN)
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;
    }

 

poll方法原理

即弹出堆顶元素,并将末尾元素直接放到堆顶,size-- 

再调用siftDown(下滤)进行堆调整

 

siftDown原理

自顶向下,使堆再平衡

直到下标越界

节点的值要同时小于左孩子及右孩子(选其中最小的交换)

//k为插入位置,一般情况是0 x为插入元素 时间复杂度O(logN)    
private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1;        // 取size中间值
        while (k < half) {            //当k大于一半的size时表示没有左右孩子
            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 = queue[child = right];
            if (key.compareTo((E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

 

无序数组建堆过程

queue中存的数组本就无序

//时间复杂度不是O(NlogN) 而是O(N)    
private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }

 

 

 

 

 

 

 

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值