优先队列特点
这里的特点,主要是和普通队列比较:
- 普通队列: 先进先出,后进后出
- 优先队列:出队和入队顺序无关,和优先级有关。
思路
-
public interface Queue<E> { int getSize(); default boolean isEmpty() { return getSize() == 0; } void enqueue(E e); E dequeue(); E getFront(); }
-
既然前面已经实现了二分搜索树,而二分搜索树也有元素具备可比较性,因此保证出队操作
dequeue()
处找到最大节点并删除就行了:@Override public E dequeue() { return bst.removeMax(); }
-
在二分搜索树中增加
findMax()
,作为复写getFront()
的具体实现:public E findMax() { Node maximum = maximum(root); return maximum == null ? null : maximum.e; }
缺点
上例中,虽然使用BST
实现了优先队列,但是感觉有些僵。
要知道,我们这里的dequeue
操作直接指定删除最大节点而缺乏灵活性,是否可以传入一个比较器Comparator
来提升灵活性呢?
其次,考虑二分搜索树本身的特点,是有可能退化为链表的,因此dequeue
操作的最差时间复杂度是O(n)
。有一种数据结构可以将最差时间复杂度控制在O(logn)
,叫(二叉)堆。
二叉堆
基础
- 二叉堆是一棵完全二叉树,意味着可以使用线性结构(如数组)表示
- 按层级放置元素,意味着不会退化为链表
- 最大堆,堆中每个节点的值总是不大于其父节点的值,最小堆,反之
实现
为了专注于堆的特性,而不考虑其他问题,如数组的扩容和缩容等,这里直接使用已实现的动态数组来代替。
索引关系
已知当前索引为i
,那么可以推算出它的父节点((i-1)/2
)及孩子节点(2*i+1
,2*i+2
)的索引值:
private int parent_index(int i) {
return Math.max(0, (i - 1) >> 1);
}
private int left_index(int i) {
return Math.min(array.getSize() - 1, (i << 1) + 1);
}
private int right_index(int i) {
return Math.min(array.getSize() - 1, (left_index(i)) + 1);
}
节点的增删
增删节点可能引起堆特性的破坏,比如删除了第一个元素后,其余的元素应该如何重新排序呢?假设有一个层高为h
的元素,在进行增删操作后层高发生了变化,一般根据变化情况,即层级的升降,将其分别称为上浮(siftUp
)或下沉(siftDown
)操作。
上浮
上浮的思路很简单,在给定索引的前提下,比较其与父节点的优先级,如果比父节点优先级高,则进行元素交换。直到当前索引为0为止。
private void sift_up(int i) {
if (i == 0) {
return;
}
int parent_index = parent_index(i);
E parent = array.get(parent_index);
E current = array.get(i);
if (comparator.compare(current, parent) > 0) swap(i, parent_index);
sift_up(parent_index);
}
下沉
下沉比起上浮稍微复杂一些,因为有俩孩子,因此需要先找到优先级比较高的孩子,然后对比,如果当前节点比优先级较高的孩子节点优先级低,则与其交换,直到叶子节点。
private void sift_down(int i) {
if (array.isEmpty() || i == array.getSize() - 1) return;
int priority_child_index = find_priority_child_index(i);
E max_child = array.get(priority_child_index);
E current = array.get(i);
if (comparator.compare(current, max_child) < 0)
swap(i, priority_child_index);
sift_down(priority_child_index);
}
增加
在数组末尾放置元素,然后进行上浮操作
public E put(E e) {
if (null == e) return null;
array.addLast(e);
sift_up(array.getSize() - 1);
return e;
}
删除
这里的删除,指的是优先级最高元素(也就是数组的第一个元素)出队。
首先交换第一个和最后一个元素的位置,再把最后(优先级最高)的元素删除,然后从头开始下沉。
public E pop() {
E e = array.get(0);
swap(0, array.getSize() - 1);
array.removeLast();
sift_down(0);
return e;
}
构造器
上面提到了应该增加比较器来增加灵活性,同时支持数组
等线性结构表达,那么可以直接将其设置为构造器参数。但需要注意传入的数组可能不具备堆的特性,因此在传入后将其堆化,也就是所谓的heapify
,思路也比较简单,遍历数组,然后逐个下沉:
public BiHeap(Comparator<? super E> comparator, E... es) {
this(comparator);
if (es != null) {
array = new Array<>(es).removeIf(Objects::isNull);
}
for (int i = 0; i < array.getSize(); i++) {
sift_down(i);
}
}
查看优先级最高元素
这里的堆是为优先队列服务的,故实现对应的getFront
方法:
public E peek() {
return array.isEmpty() ? null : array.get(0);
}
二叉堆实现的优先队列
实现了二叉堆,基本的优先队列就很简单了,直接调用相应方法即可。
public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
private BiHeap<E> biHeap;
public PriorityQueue() {
biHeap = new BiHeap<>();
}
public PriorityQueue(Comparator<E> comparator) {
super();
biHeap = new BiHeap<>(comparator);
}
@Override
public int getSize() {
return biHeap.size();
}
@Override
public void enqueue(E e) {
biHeap.put(e);
}
@Override
public E dequeue() {
return biHeap.pop();
}
@Override
public E getFront() {
return biHeap.peek();
}
}