1. 优先级队列
PriorityQueue是一种基于堆的无界优先级队列。内部使用Object数组存储数据,在容量不足时会进行扩容操作。内部元素的排序规则,按照构造实例时传入的Comparator或者元素自身的排序规则(所属类实现Comparable接口)。
2. Fields
默认的数组长度为11。
private static final int DEFAULT_INITIAL_CAPACITY = 11;
用于存储数据的Object数组。
transient Object[] queue;
元素数量。
private int size = 0;
给元素排序的排序器,如果没有自定义排序器,则会使用元素自身的排序规则。
private final Comparator<? super E> comparator;
容器修改次数。
transient int modCount = 0;
3. Constructors
(1)空参构造器。使用默认的数组长度和元素自身的排序规则。
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
(2)自定义数组长度的构造器。
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
(3)自定义元素排序规则的构造器。
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
(4)此外,还有其他构造器,可以自定义多种配置信息。
4. 添加元素
add方法向优先级队列中添加元素。如果添加成功,会返回true。如果添加了null,会抛出空指针异常。
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
//不可以添加null
if (e == null)
throw new NullPointerException();
modCount++;
//获取当前元素数
int i = size;
//如果超出了数组长度,则数组扩容
if (i >= queue.length)
grow(i + 1);
//元素总数加1
size = i + 1;
//如果当前容器为空,则直接在0号位置放置元素
if (i == 0)
queue[0] = e;
else
//否则,将新元素插入到堆中,实行向上调整
siftUp(i, e);
return true;
}
【扩容策略】:如果旧数组的长度小于64,则新数组的长度变成旧数组的2倍+2;如果旧数组的长度大于等于64,则新数组的长度为旧数组的1.5倍。
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方法将新元素插入堆中时,根据是否使用自定义的排序器来执行不同的方法。
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
siftUpComparable方法使用了元素自身的排序规则。前提是这个元素的所属类必须已经实现Comparable接口。
private void siftUpComparable(int k, E x) {
//将新元素的类型转换为Comparable
Comparable<? super E> key = (C<? super E>) x;
//起始时,k指向最后一个元素的后一位
//向上调整堆,直到到达堆顶
while (k > 0) {
//堆是一棵完全二叉树
//取得最后一个元素的父亲节点(父节点的坐标为i,左子节点的坐标为2*i+1,右子节点的坐标为2*i+2)
//通过上述规则,如果某个节点的位置为k,则其父节点的位置为(k-1)/2
int parent = (k - 1) >>> 1;
//取得父节点元素
Object e = queue[parent];
//如果当前节点比父节点大,则可以直接插入在当前位置k上
if (key.compareTo((E) e) >= 0)
break;
//否则,就将父节点元素移动到当前位置k
queue[k] = e;
//k指向父节点
k = parent;
//在循环中,不断向上调整,直到遇到当前节点比父节点大,或者,k到达了顶点,就能退出循环了
}
//最后,k为插入位置
queue[k] = key;
}
siftUpUsingComparator方法使用自定义排序器进行插入,与上述方法类似。
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
5. 弹出堆顶元素
remove方法定义在PriorityQueue类的抽象父类AbstractQueue中,其内部也是调用了PriorityQueue类自身实现的poll方法。
在队列为空时做弹出操作,remove方法会抛出NoSuchElementException;而poll方法,会返回null。
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
//弹出的是堆顶元素
E result = (E) queue[0];
//取得最后一个元素
E x = (E) queue[s];
//将最后一个位置置为null,表示删除
queue[s] = null;
if (s != 0)
//然后将前面取得的最后一个位置的元素放到堆顶,再向下调整堆
siftDown(0, x);
return result;
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
siftDownComparable方法使用元素自身的排序规则向下调整堆。
private void siftDownComparable(int k, E x) {
//假设将最后一个元素key放在堆顶的位置,也就是k位置上
Comparable<? super E> key = (Comparable<? super E>)x;
//取得最后一个父节点的下一个位置
int half = size >>> 1; // loop while a non-leaf
//只需要调整到最后一个父节点,就可以结束。
while (k < half) {
//取得当前位置k的左孩子
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];
//比较k位置的元素和其最小的孩子,如果key更小,则可以停止调整了
if (key.compareTo((E) c) <= 0)
break;
//否则,就将更小的孩子放到k位置上
queue[k] = c;
//k下移到该孩子处
k = child;
//不断调整,直到最后一个父节点也调整完毕,或者当前的父节点已经比两个孩子都小了,就退出循环
}
//把key实际放入k位置上
queue[k] = key;
}
6. 优先级队列的应用
(1)求top K个元素。
在数据量很大的情况下,无法一次性将所有数据加载进内存进行排序来获得topK个元素。此时,可以先将前面k个元素放入一个容量为k的优先级队列,然后一点一点把后面的数据加载到内存中,并尝试插入堆中。
比如,我们要求最小K个元素,就创建一个大顶堆(也称最大堆),堆顶元素是最大的。所以排序规则需要按照从大到小排序。
插入的时候,首先比较堆顶元素,如果大于等于堆顶,就不用插入了;如果小于堆顶,就将堆顶移除,然后将新元素插入堆中。
使用这种方式,可以动态地求得top K个元素。
//numbers数组存储了全部数据
public void topK(int[] numbers, int k){
int len;
if(numbers == null || (len = numbers.length) == 0 || k > len){
throw new IllegalArgumentException();
}
//创建一个长度为k的大顶堆
Comparator<Integer> comparator = (o1, o2) -> {
if (o1.compareTo(o2) == -1) {
return 1;
} else if (o1.equals(o2)) {
return 0;
} else {
return -1;
}
};
//优先级队列就是一个堆,传入自定义的排序器,从大到小排序
PriorityQueue<Integer> pq = new PriorityQueue<>(k,comparator);
//将前k个元素插入堆中,时间复杂度为O(klogk)
for(int i = 0; i < k; ++i){
pq.add(numbers[i]);
}
//将剩余元素插入到堆中,时间复杂度O(nlogk)
for(int i = k; i < len; ++i){
//取得堆顶元素
Integer peek = pq.peek();
//如果当前元素比堆顶元素小,就将堆顶元素移除,将当前元素插入
if(numbers[i] < peek){
pq.remove();
pq.add(numbers[i]);
}
}
//输出堆中的元素
for(int i : pq){
System.out.print(i + " ");
}
}
(2)求动态数据的中位数。
可以用两个堆来求中位数。数据总数为n。
如果n为偶数,则将前n/2个元素放入到一个大顶堆中,将后n/2个元素放入到一个小顶堆中。此时,两个堆的堆顶是中位数。
如果n为奇数,则将前(n+1)/2个元素放入到一个大顶堆中,将后(n-1)/2个元素放入到一个小顶堆中。此时,大顶堆的堆顶是中位数。
//建两个堆,一个是大顶堆,一个是小顶堆
private PriorityQueue<Integer> maxpq = new PriorityQueue<>((o1, o2) -> {
if (o1.compareTo(o2) == -1) {
return 1;
} else if (o1.equals(o2)) {
return 0;
} else {
return -1;
}
});
private PriorityQueue<Integer> minpq = new PriorityQueue<>();
//numbers数组用于存储数据
private int[] numbers;
//size保存数据总数
private int size;
将已有的数据放入到两个堆中。
public void initHeaps(){
//将前一半数据放入大顶堆
int mid = (size - 1) >>> 1;
for(int i = 0; i <= mid; ++i){
maxpq.add(numbers[i]);
}
//遍历后一半数据,如果比大顶堆的堆顶元素小,就将这个堆顶元素移动到小顶堆,并将当前数据放入大顶堆
for(int i = mid+1; i < size; ++i){
Integer peek = maxpq.peek();
if(peek > numbers[i]){
Integer move = maxpq.remove();
maxpq.add(numbers[i]);
minpq.add(move);
}else {//否则,就将当前数据直接放入小顶堆
minpq.add(numbers[i]);
}
}
}
如果数组中存在动态添加元素的操作,中位数不断地变化,那么在添加元素时,如果新元素小于大顶堆的堆顶元素,则将其插入到大顶堆;如果大于等于大顶堆的堆顶元素,则插入到小顶堆。但是,这种做法可能会导致两个堆不平衡,此时只需要在每次插入操作后,对两个堆进行调整,将数量多的堆的堆顶元素不断移动到数量少的堆,直到两个堆平衡。时间复杂度为O(logn)。
public void add(int e){
ensureCapacity(size+1);
numbers[size++] = e;
//插入到两个堆中
Integer peek = maxpq.peek();
if(peek == null) {
maxpq.add(e);
}else {
if (e < peek) {
maxpq.add(e);
} else {
minpq.add(e);
}
}
//调整两个堆的大小
balanceTwoHeaps();
}
调整两个堆的大小。始终保持大顶堆比小顶堆多一个,或者两个堆数量相等。
private void balanceTwoHeaps() {
int size1 = maxpq.size();
int size2 = minpq.size();
while(!(size1 == size2) && !(size1 == size2 + 1)){
if(size1 > size2){
minpq.add(maxpq.remove());
--size1;
++size2;
}else {
maxpq.add(minpq.remove());
++size1;
--size2;
}
}
}
获取中位数。如果数据总数是奇数,则大顶堆的堆顶元素为中位数。如果是偶数,则两个堆顶的元素为中位数。时间复杂度为O(1)。
public void getMiddle(){
if((size&1) == 1){
System.out.println(maxpq.peek());
}else {
System.out.println(maxpq.peek() + " " + minpq.peek());
}
}
【举一反三】:取中位数可以用两个堆来实现,取其他位置的元素同样可以用两个堆来实现。