《Algorithms 4th Edition》读书笔记——2.4 优先队列(priority queue)-Ⅳ
2.4.4 堆的算法
我们用长度为 N + 1的私有数组pq[]来表示一个大小为N的堆,我们不会使用pq[0],堆元素放在pq[1]至pq[N]中。在排序算法中,我们只能通过私有辅助函数less()和exch()来访问元素,但因为所有的元素都在数组pq[]中,我们在2.4.4.2节中会使用更加紧凑的实现方式,不再将数组作为参数传递。堆的操作会首先进行一些简单的改动,打破堆的状态,然后再遍历堆并按照要求将堆的状态回复。我们称这个过程叫做堆的有序化(reheapitying)。
堆实现的比较和交换方法如一下代码所示。
private boolean less(int i, int j) { return pq[i].compareTo(pq[j]) < 0; } private void exch(int i, int j) { Key t = pq[i]; pq[i] = pq[j]; pq[j] = t; }
在有序化的过程中我们会遇到两种情况。当某个结点的优先级上升(或是在对底加入一个新的元素)时,我们需要由下向上恢复堆的顺序。当某个结点的优先级下降(例如,将根节点替换为一个较小的元素)时,我们需要由上至下恢复堆的顺序。首先,我们会学习如何实现这两种辅助操作,然后再用他们实现插入元素和删除最大元素的操作。
2.4.4.1 由下至上的堆有序化(上浮)
如果堆的有序状态因为某个结点变得比它的父节点更大而被打破,那么我们就需要通过交换它和它的父节点来修复堆。减缓后,这个结点比它的链各个子节点都大(一个是曾经的父结点,另一个比它更小,因为他是曾经父节点的子节点),但这个结点仍然可能比它现在的父结点更大。我们可以一遍遍地用同样的方法恢复秩序,将这个结点不断向上移动直到遇到更大的父结点。只要记住位置k的结点的父结点的位置k的结点的父结点的位置是[k / 2(d)],这个过程实现起来很简单。swim()方法中的循环可以保证只有位置k上的结点大于它的父结点那堆的有序状态才会被打破。因此之哟啊该节点不再大于它的父结点。由下至上的堆有序化的实现代码如下所示。
private void swim(int k) { while (k > 1 && less(k/2, k)) { exch(k, k/2); k = k/2; } }
下图展示了由下至上的堆有序化示意图。
2.4.4.2 由上至下的堆有序化(下沉)
如果堆的有序状态因为某个界定啊变得比它的两个子界定啊或是其中之一更小了而被打破了,那么我们可以通过将它和它的两个子节点中的较大者交换来恢复堆。交换可能会在子节点处继续打破堆的有序状态,因此我们需要不断地用相同的方式来将其修复,将结点向下移动直到它的子节点都比它更小或是到达了堆的底部。由位置为k的结点的子节点位于2k和2k + 1可以直接得到对应的代码。至于方法名,由上至下的堆有序化的示意图及其实现代码分别如下所示。当一个结点太小的时候需要沉(sink)到堆的最底层。
private void sink(int k) { while (2*k <= N) { int j = 2*k; if (j < N && less(j, j+1)) j++; if (!less(k, j)) break; exch(k, j); k = j; } }
如果我们把堆想象成一个严密的黑社会组织,每个子节点都表示一个下属(父节点则表示它的直接上级),那么这些操作就可以得到很有趣的解释。swim()表示一个很有能力的新人加入组织并逐级提升(将能力不够的上级踩在脚下),直到他遇到了一个更强的领导。sink()则类似于整个社团的领导退休被外来者取代之后,如果他的下属比他更厉害,他们的角色就会交换,这种交换会持续下去,直到他的能力比其下属都强为止。这些理想化的背景在现实生活中可能很罕见,但他们能够帮你理解堆的基本行为。
(笔者感悟:Robert & Kevin用了个生动的例子,把堆的两个有序化操作描述的十分的清晰。这是对两种有序化方法的实体描述,遇到实际问题可以用此例对比,就能清楚的理解上浮与下沉操作的实现原理和方式。)
sink()和swim()方法都是高效实现优先队列API的基础,原因如下:
插入元素。我们将新元素加到数组末尾,增加堆的大小并让新元素上浮到合适的位置。如下图左部分。
删除最大元素。我们从数组顶端删去最大的元素并将数组的最后一个元素放到顶端,减小堆的大象并让这个元素下沉到合适的位置,如下图右部分。
以下算法解决了一开始的基本问题:它堆优先队列API的实现能够保证插入元素和删除最大元素这两个操作的用时和队列的大小仅成对数关系。
import java.util.Comparator; import java.util.Iterator; import java.util.NoSuchElementException; public class MaxPQ<Key> implements Iterable<Key> { private Key[] pq; // store items at indices 1 to N private int N; // number of items on priority queue private Comparator<Key> comparator; // optional Comparator public MaxPQ(int initCapacity) { pq = (Key[]) new Object[initCapacity + 1]; N = 0; } public MaxPQ() { this(1); } public MaxPQ(int initCapacity, Comparator<Key> comparator) { this.comparator = comparator; pq = (Key[]) new Object[initCapacity + 1]; N = 0; } public MaxPQ(Comparator<Key> comparator) { this(1, comparator); } public MaxPQ(Key[] keys) { N = keys.length; pq = (Key[]) new Object[keys.length + 1]; for (int i = 0; i < N; i++) pq[i+1] = keys[i]; for (int k = N/2; k >= 1; k--) sink(k); assert isMaxHeap(); } public boolean isEmpty() { return N == 0; } public int size() { return N; } public Key max() { if (isEmpty()) throw new NoSuchElementException("Priority queue underflow"); return pq[1]; } // helper function to double the size of the heap array private void resize(int capacity) { assert capacity > N; Key[] temp = (Key[]) new Object[capacity]; for (int i = 1; i <= N; i++) temp[i] = pq[i]; pq = temp; } public void insert(Key x) { // double size of array if necessary if (N >= pq.length - 1) resize(2 * pq.length); // add x, and percolate it up to maintain heap invariant pq[++N] = x; swim(N); assert isMaxHeap(); } public Key delMax() { if (isEmpty()) throw new NoSuchElementException("Priority queue underflow"); Key max = pq[1]; exch(1, N--); sink(1); pq[N+1] = null; // to avoid loiterig and help with garbage collection if ((N > 0) && (N == (pq.length - 1) / 4)) resize(pq.length / 2); assert isMaxHeap(); return max; } private void swim(int k) { while (k > 1 && less(k/2, k)) { exch(k, k/2); k = k/2; } } private void sink(int k) { while (2*k <= N) { int j = 2*k; if (j < N && less(j, j+1)) j++; if (!less(k, j)) break; exch(k, j); k = j; } } private boolean less(int i, int j) { if (comparator == null) { return ((Comparable<Key>) pq[i]).compareTo(pq[j]) < 0; } else { return comparator.compare(pq[i], pq[j]) < 0; } } private void exch(int i, int j) { Key swap = pq[i]; pq[i] = pq[j]; pq[j] = swap; } // is pq[1..N] a max heap? private boolean isMaxHeap() { return isMaxHeap(1); } // is subtree of pq[1..N] rooted at k a max heap? private boolean isMaxHeap(int k) { if (k > N) return true; int left = 2*k, right = 2*k + 1; if (left <= N && less(k, left)) return false; if (right <= N && less(k, right)) return false; return isMaxHeap(left) && isMaxHeap(right); } public Iterator<Key> iterator() { return new HeapIterator(); } private class HeapIterator implements Iterator<Key> { // create a new pq private MaxPQ<Key> copy; // add all items to copy of heap // takes linear time since already in heap order so no keys move public HeapIterator() { if (comparator == null) copy = new MaxPQ<Key>(size()); else copy = new MaxPQ<Key>(size(), comparator); for (int i = 1; i <= N; i++) copy.insert(pq[i]); } public boolean hasNext() { return !copy.isEmpty(); } public void remove() { throw new UnsupportedOperationException(); } public Key next() { if (!hasNext()) throw new NoSuchElementException(); return copy.delMax(); } } public static void main(String[] args) { MaxPQ<String> pq = new MaxPQ<String>(); while (!StdIn.isEmpty()) { String item = StdIn.readString(); if (!item.equals("-")) pq.insert(item); else if (!pq.isEmpty()) StdOut.print(pq.delMax() + " "); } StdOut.println("(" + pq.size() + " left on pq)"); } }
优先队列由一个基于堆的完全二叉树表示,存储与数组pq[1..N]中,pq[0]没有使用。在insert()中,我们将N加一并吧新元素添加在数组最后,然后用swim()恢复堆的秩序。在delMax()中,我们pq[1]中得到需要返回的元素,然后将pq[N]移动到pq[1],将N减一并用sink()恢复堆的秩序。同时我们还将不再使用的pq[N + 1] 设为null,以便系统回收它所占用的空间。和以前一样,这里省略了动态调整数组大小的代码。