优先队列介绍
优先队列也是一个队列,因此也适用于之前实现的队列接口。不过优先队列出队永远需要优先级对高的。
优先队列针对的是动态的情况,如果是静态的,那么只需要排序一次即可。
依然可以使用之前实现普通队列的方式实现优先队列,即使用数组或链表这样的线性结构实现优先队列。只不过无法再像实现普通队列那样,使入队和出队的复杂度都为 O(1)。
使用堆来实现优先队列,可以使得入队和出队操作都为 O(logn) 级别。
|
入队 |
出队 |
专门维护一个顺序的线性结构,即入队时进行排序 |
O(n) |
O(1) |
普通队列出队操作时遍历找到优先级最高的元素 |
O(1) |
O(n) |
使用堆实现 |
O(logn) |
O(logn) |
堆的介绍
- 满二叉树,除最后一层无任何叶子节点外,其所有非叶子节点均既有左孩子又有右孩子。
- 完全二叉树,其不一定是一颗满二叉树,但是其不满的部分一定是在树的右下侧。也就是除了最底层之外,上面的是一个满二叉树,最底层从左开始放置元素。
- 二叉堆就是一个完全二叉树。
- 最大堆:堆中某个节点的值总是不大于其父节点的值(相应也有最小堆)。需要注意的是,这个定义没有要求较下层的某个节点一定不会大于较上层的所有节点。
对于完全二叉树来说,通过层序遍历,可以将节点放入一个数组中。实际上堆的存储结构也是使用一个数组最为合适。
当使用一个数组表示一个堆的时候,根节点放在第一个位置,索引为 0。假如某节点索引为 i,其左孩子节点索引就是 2i + 1,右孩子节点索引就是 2i + 2,其父节点索引就是 (i - 1) / 2。如果为了表示方便,也可以空一个位置,将根节点放在索引为 1 的位置,那么假如某节点索引为 i,其左孩子节点索引就是 2i,右孩子节点索引就是 2i + 1,其父节点索引就是 i / 2。
根节点索引 |
当前节点索引 |
左孩子索引 |
右孩子索引 |
父节点索引 |
0 |
i |
2i + 1 |
2i + 2 |
(i - 1) / 2 |
1 |
i |
2i |
2i + 1 |
i / 2 |
对于完全二叉树,寻找最后一个非叶子节点的索引就是找最后一个节点的父亲节点。
复杂度
操作 |
复杂度 |
add |
O(logn) |
extractMax |
O(logn) |
findMax |
O(1) |
replace |
O(logn) |
heapify |
O(n) |
其中,add
操作的步骤是先将元素添加到队尾,之后使用 sift up
操作,即不断和其父节点做比较,如果其大于父节点元素就两者互换,这个操作的复杂度是 O(logn)
。extractMax
操作的步骤是先获取队首最大元素,之后将队尾元素放到队首,移除队尾多余的空位后,将换到队首的那个元素使用 sift down
操作,这个复杂度也是 O(logn)
。
replace
操作是使用一个新元素替换掉队首的最大元素,可以使用 add
操作之后接一个 extractMax
操作来实现,但是这样的话复杂度就是两个 O(logn)
操作。所以可以专门给它一个优化实现,就是使用新元素直接替换队首元素位置,之后将新元素做 sift down
操作,这样复杂度就是一个 O(logn)
。
heapify
操作是将一个任意数组转为一个堆的过程。具体实现是先将这个数组看作一个完全二叉树,之后从最后一个非叶子节点开始逐渐向前进行 sift down
操作。如果将 n 个元素逐个插入到一个空堆中,复杂度是 O(nlogn)
级别的。但是 heapify
的过程,算法复杂度其实是 O(n)
级别的(这个复杂度先记住就好)。
堆的实现
使用动态数组实现一个最大堆:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
| public class MaxHeap<E extends Comparable<E>> { private Array<E> data;
public MaxHeap(int capacity) { data = new Array<>(capacity); }
public MaxHeap() { data = new Array<>(); }
public MaxHeap(E[] arr) { data = new Array<>(arr); for (int i = parent(arr.length - 1); i >= 0; i--) { siftDown(i); } }
public int size() { return data.getSize(); }
public boolean isEmpty() { return data.isEmpty(); }
private int parent(int index) { if (index == 0) { throw new IllegalArgumentException("index-0 doesn't have parent."); } return (index - 1) / 2; }
private int leftChild(int index) { return index * 2 + 1; }
private int rightChild(int index) { return index * 2 + 2; }
public void add(E e) { data.addLast(e); siftUp(data.getSize() - 1); }
private void siftUp(int k) {
while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) { data.swap(k, parent(k)); k = parent(k); } }
public E findMax() { if (data.getSize() == 0) { throw new IllegalArgumentException("Can not findMax when heap is empty."); }
return data.get(0); }
public E extractMax() {
E ret = findMax();
data.swap(0, data.getSize() - 1); data.removeLast();
siftDown(0);
return ret; }
private void siftDown(int k) {
while (leftChild(k) < data.getSize()) { int j = leftChild(k); if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) { j = rightChild(k); }
if (data.get(k).compareTo(data.get(j)) >= 0) { break; }
data.swap(k, j); k = j; } }
public E replace(E e) { E ret = findMax(); data.set(0, e); siftDown(0);
return ret; } }
|
优先队列的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
private MaxHeap<E> maxHeap;
public PriorityQueue() { maxHeap = new MaxHeap<>(); }
@Override public int getSize() { return maxHeap.size(); }
@Override public boolean isEmpty() { return maxHeap.isEmpty(); }
@Override public E getFront() { return maxHeap.findMax(); }
@Override public void enqueue(E e) { maxHeap.add(e); }
@Override public E dequeue() { return maxHeap.extractMax(); } }
|
更多
关于堆:
我们实现的是二叉堆,相应的也就有三叉堆、四叉堆等等。对于 d叉堆来说,层数有可能更少,这在有些操作上会让速度更快。但是比如下沉操作,就要考虑 d 个子节点而不是 2 个。
另外,更高级的堆还有如索引堆(它可以操作堆中的某个元素)、二项堆、斐波那契堆等。
关于队列:
如果从广义上讲,能满足的队列接口的数据结构就可以成为队列。那么除了普通的队列,优先队列之外,其实栈也可以被理解是一个队列。当我们这么理解的时候,就会发现,之前使用栈辅助实现二分搜索树的前序遍历,使用队列辅助实现二分搜索树的层序遍历,他们在逻辑上是一致的。