特点:
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方法