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,以便系统回收它所占用的空间。和以前一样,这里省略了动态调整数组大小的代码。


《Algorithms 4th Edition》读书笔记——2.4 优先队列(priority queue)-Ⅳ的更多相关文章

  1. 《Algorithms 4th Edition》读书笔记——2.4 优先队列(priority queue)-Ⅶ(延伸:堆排序的实现)

    2.4.5 堆排序 我们可以把任意优先队列变成一种排序方法.将所有元素插入一个查找最小元素的有限队列,然后再重复调用删除最小元素的操作来将他们按顺序删去.用无序数组实现的优先队列这么做相当于进行一次插 ...

  2. 《Algorithms 4th Edition》读书笔记——2.4 优先队列(priority queue)-Ⅵ

    · 学后心得体会与部分习题实现 心得体会: 曾经只是了解了优先队列的基本性质,并会调用C++ STL库中的priority_queue以及 java.util.PriorityQueue<E&g ...

  3. 《Algorithms 4th Edition》读书笔记——2.4 优先队列(priority queue)-Ⅴ

    命题Q.对于一个含有N个元素的基于堆叠优先队列,插入元素操作只需要不超过(lgN + 1)次比较,删除最大元素的操作需要不超过2lgN次比较. 证明.由命题P可知,两种操作都需要在根节点和堆底之间移动 ...

  4. 《Algorithms 4th Edition》读书笔记——2.4 优先队列(priority queue)-Ⅰ

    许多应用程序都需要处理有序的元素,但不一定要求他们全部有序,或者是不一定要以此就将他们排序.很多情况下我们会手机一些元素,处理当前键值最大的元素,然后再收集更多的元素,再处理当前键值最大的元素.如此这 ...

  5. 《Algorithms 4th Edition》读书笔记——2.4 优先队列(priority queue)-Ⅲ

    2.4.3 堆的定义 数据结构二叉堆能够很好地实现优先队列的基本操作.在二叉堆的数组中,每个元素都要保证大于等于另两个特定位置的元素.相应地,这些位置的元素又至少要大于等于数组中的两个元素,以此类推. ...

  6. 《Algorithms 4th Edition》读书笔记——2.4 优先队列(priority queue)-Ⅱ

    2.4.2初级实现 我们知道,基础数据结构是实现优先队列的起点.我们可以是使用有序或无序的数组或链表.在队列较小时,大量使用两种主要操作之一时,或是所操作元素的顺序已知时,它们十分有用.因为这些实现相 ...

  7. C++Primer 4th edition读书笔记-第二章

    1 变量的定义用于为变量分配存储空间,还可以为变量指定初始值.在一个程序中,变量有且只有一个定义.声明用于向程序表明变量的名字和类型.定义也是声明:当定义变量时,我们声明了它的类型和名字.可以通过使用 ...

  8. 《Algorithms 4th Edition》读书笔记——3.1 符号表(Elementary Symbol Tables)-Ⅳ

    3.1.4 无序链表中的顺序查找 符号表中使用的数据结构的一个简单选择是链表,每个结点存储一个键值对,如以下代码所示.get()的实现即为遍历链表,用equals()方法比较需被查找的键和每个节点中的 ...

  9. 《C++ Primer 4th》读书笔记 序

    注:本系列读书笔记是博主写作于两三年前的,所以是基于<C++ Primer>第四版的,目前该书已更新至第五版,第五版是基于C++11标准的,貌似更新挺多的.博主今年应届硕士毕业,如若过阵子 ...

随机推荐

  1. 专访OPPO李紫贵:ColorOS用户过千万 软硬融合生态版图初现

    专访OPPO李紫贵:ColorOS用户过千万 软硬融合生态版图初现 专访OPPO李紫贵:ColorOS用户过千万 软硬融合生态版图初现

  2. ibatis学习之道:ibatis的<[CDATA]>dynamic属性跟#$的应用

    ibatis的<![CDATA]>,dynamic属性和#,$的应用 <![CDATA[   ]]>的正确使用 ibatis作为一种半自动化的OR Mapping工具,其灵活性 ...

  3. hdu 4869 Turn the pokers(组合数+费马小定理)

    Problem Description During summer vacation,Alice stay at home for a long time, with nothing to do. S ...

  4. iOS之即时通讯相关理解

    Socket: 1>Socket又称"套接字" 2>网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket. 3>应用程序通常通 ...

  5. PHP设计模式笔记四:适配器模式 -- Rango韩老师 http://www.imooc.com/learn/236

    适配器模式 1.适配器模式,可以将截然不同的函数接口封装成统一的API 2.实际应用举例,PHP的数据库操作有mysql.mysqli.pdo三种,可以用适配器模式统一成一致,类似的场景还有cache ...

  6. hdu2531之BFS

    Catch him Time Limit: 5000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total ...

  7. 使用Qt Style Sheets制作UI特效

    引言 作为一套GUI框架,Qt是非常强大的.(注:Qt 不仅是一套优秀的GUI框架,同时也是一套出色的应用程序框架).在UI的制作方面Qt为广大开发者提供了一套强大而易用的工具,她就是——Qt Sty ...

  8. DotNet加密方式解析--散列加密

    没时间扯淡类,赶紧上车吧. 在现代社会中,信息安全对于每一个人都是至关重要的,例如我们的银行账户安全.支付宝和微信账户安全.以及邮箱等等,说到信息安全,那就必须得提到加密技术,至于加密的一些相关概念, ...

  9. vc中调用Com组件的方法详解

    vc中调用Com组件的方法详解 转载自:网络,来源未知,如有知晓者请告知我.需求:1.创建myCom.dll,该COM只有一个组件,两个接口:   IGetRes--方法Hello(),   IGet ...

  10. poj1094 topsort

    Sorting It All Out Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 32275   Accepted: 11 ...