PriorityQueue--优先队列源码解读(jdk1.8)

时间:2022-07-13 17:02:56

特点:

1、无边界的优先队列,间接实现自Queue
会有一个内部的capacitty,这个capacity初始值为11,这个值会自动增长
2、传入元素必须实现Comparable接口,或者通过带有Comparator的比较器的构造函数来定义,两者必须任选其一。如果既有Comparator传进去,元素又实现了Comparable接口,优先Comparator。
3、线程不安全,多线程程序,应该使用PriorityBlockingQueue
4、iterator方法不保证顺序,如果想拿出完全有序的列表,需要自己排序,如使用Arrays.sort进行排序
5、入列、出列方法,offer() add() poll() remove()的时间复杂度为O(log(n));
检索方法,peek()、element()和size方法的时间复杂度为O(1);
remove(Object)和contains(Object)方法时间复杂度为线性,O(n);
6、PriorityQueue是基于堆排序实现的,具体先看排序--堆排序,这里很多地方不细讲。

我们先看一遍源码,最后再来解释这里的一些未明确解决的问题。

变量

DEFAULT_INITIAL_CAPACITY 初始容量,默认是11,构造函数不指定容量时,默认使用的初始化容量
Object[] queue 数组,存放数据
size 大小,这个大小并不是数组的容量,数组容量一定大等于size,size表示最后一个有效数字的index+1,或者说队列长度,size每次入列出列remove都会变化,所以获取大小时,直接获取O(1)
comprator 比较器,如果有比较器,优先使用比较器,如果比较器 == null,要求元素必须实现IComparator接口,否则会在进行排序操作的时候报错。
modCount 表示被修改(structurally modified 结构化变更,意思是)的次数,用于迭代器next和remove的时候判断是否数据仍然发生了结构化变更。迭代器在初始化时,拿一次当时的modCount,next和remove的时候,会判断旧的modCount和最新的modCount是否相同。如果不同会抛出ConcurrentModificationException

构造函数

一共8个构造函数的重载
可以指定初始容量,如果不指定,使用默认的DEFAULT_INITIAL_CAPACITY 11
可以指定比较器,如果不指定比较器,比较器为null
这样有四个构造函数组合
可以通过传入Collection的子类来进行初始化。会判断子类类型进行分别操作。
如果是SortedSet的子类,会将SortedSet的comparator、数据数组和size拿过来。
如果子类是PriorityQueue的子类,判断是否就是PriorityQueue,如果是直接复制数据;如果不是除了赋值数据,还会重新构建堆。
如果子类是其它类,赋值数据,重新构建堆。
同样,可以直接使用具体子类来进行初始化,这样也一共有4个构造函数组合(3+1)。

核心方法

我们按照使用顺序来讲。

heapify()

建堆方法
1、会在初始化时候调用heapify()方法,构建堆。先看堆排序,这里堆方面不会那么细讲。
2、从n/2-1处开始到0,不停的调整堆,成为小顶堆。调用siftDown构建小顶堆,siftDown字面意思是向下筛选,就是从父节点开始向左右节点找最小值。
private void heapify() {
for ( int i = ( size >>> 1 ) - 1 ; i >= 0 ; i--)
siftDown(i, ( E ) queue [i]);
}

siftDown

从父节点向下调整堆
1、是一个递归操作,一旦发生了父节点和子节点的交换,交换下去的新的子节点可能和该子节点的子节点不满足堆特性,需要调整
// 构建小顶堆
// 下面是siftDown方法,有两个,一个使用比较器来实现比较,一个使用元素的CompareTo方法来实现,其实一样
// 这个构建堆的方法比我写的好多了,哈哈
private void siftDownUsingComparator(int k, E x) {
    // 有边界,因为是向下构建,只用到倒数第一个非叶子节点就可以了
    int half = size >>> 1;
    // 循环的原因是,假设某个节点出现了交换,父节点和子节点交换,为了保证交换下去的父节点和以下的节点也能构成小顶堆,需要递归
    // 最多递归到结束
    // 如果遇到不需要交换的情况,说明到此为止都能满足小顶堆的要求,break
    while (k < half) {
        // 取左子节点,左子节点一定存在,所以先取左子节点
        int child = (k << 1) + 1;
        // 取左子节点数值
        Object c = queue[child];
        // 取右子节点
        int right = child + 1;
        // 如果右子节点存在,取左右子节点中的较小值
        // comparator.compare((E) c, (E) queue[right]) > 0说明右子节点是较小值,让c等于右子节点的值,child等于右子节点的index
        if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        // 比较父节点和较小子节点,如果小等于,说明不用换,break
        if (comparator.compare(x, (E) c) <= 0)
            break;
        // 将k位置赋值成较小值
        queue[k] = c;
        // 因为出现了交换,为了保证交换下去的节点也能满足小顶堆,需要继续调整堆
        // 指定k为child,进入下一次循环
        k = child;
    }
    // 如果结束,说明k应该的位置,就是x
    queue[k] = x;
}



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、modeCount++
2、记录新元素下标i = size,因为size代表长度,所以当前最大index为size - 1,再来个新的就是size
3、如果放入这个新元素,会使得数组容量不够用的话,调用grow方法来增加容量,grow方法其实不太用细看,总之会按照一定规则增加容量。
4、size+1
5、如果queue为0,也就是第一个元素,直接就放了,没什么好说的
6、否则,从这个位置开始调整堆,就是将新元素升到最合适的位置。由于堆已经是小顶堆,这个操作只会经过树的任一分支,所以插入操作复杂度O(logn)

siftUp()方法

和siftDown一样,有shiftUpUsingComparator,或者shiftUpComparable,同样,两个方法类似,如果是使用传入比较器的构造函数,则使用Comparator,否则使用元素自身的compareTo方法。
shiftUp方法两个参数,一个是当前入队元素的位置,也就是堆的最后一个元素,然后开始和父节点开始比较,如果当前比父节点小(那么一定比左节点小),和父节点交换,然后继续和父节点比较,直到满足大等于父节点break,或者到堆顶。这个操作只会经过树的某一个分支。

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


1、如果size为0,return null
2、size-1,并且拿到size-1处的数据。size-1就会让有效数据减少一个,拿到size-1处,也就是当前最后的数据。
3、modCount++
4、取0位置的元素,准备拿出来
5、将最后的元素丢到顶上,然后调用siftDown方法,让它下降到合适的位置。
6、同样这个元素的行进路径,只会是树中的任一分支,所以复杂度也是O(logn)
7、极端的例子,假设队列中全部是相同的元素,最后的元素不就跑到第一位去了。。。

其它方法

remove()、remove(Object)或者removeAt(i)

1、移除跟出队差不多,只要从移除的位置开始向下调整堆就可以了。
2、remove(Object)需要先找到这个元素,复杂度O(n),移除复杂度O(logn),合起来取高阶算式,O(n)
3、removeAt(i),可以直接定位到目标位置,复杂度O(1),移除复杂度O(logn),合起来取高阶算式,O(logn)

peek()

代表读取方法

1、获取第一位的元素,直接取数组0位元素就好了,O(1)

iterator()

1、 iterator只会将元素顺次输出一遍,元素第一位一定是最小元素,并且整体满足堆特性,但是整体并不是严格的从小到大排列。这个我们之前在堆排序中也说过,堆只是整体有序,并不是完全有序。

如果想按顺序得到队列中的元素,需要使用Arrays.sort方法