算法导论第六章:堆排序
堆排序是一种原地(in place)排序算法。堆排序还引入另一种算法设计技术,利用某种数据结构来管理算法执行中的信息,堆数据结构不仅在排序中有用,还可以构成一个有效的优先队列。
6.1 堆
(二叉)堆数据结构是一种数组对象,如图6-1所示,它可以被视为一颗完全二叉树,树中的每个节点与数组中存放该节点值的那个元素对应。树的每一层都是填满的,最后一层可能除外。堆A具有两个属性:length[A]是数组中的元素个数,heap-size[A]是存放在A中的堆的]元素个数。也就是说虽然A[1...length[A]]都可以包含有效值,但A[heap-size[A]]之后的元素都不属于堆。
树的根为A[1],给定了某个节点的下标,其父节点PARENT(i)、左儿子LEFT(i)和右儿子RIGHT(i)的下标都可以简单计算出来:
PARENT(i) = i/2; LEFT(i) = 2i; RIGHT(i) = 2i+1。
二叉堆有两种:最大堆和最小堆。在最小堆中,除根节点意外的任意节点i,有A[PARENT(i)] >= A[i]。最小堆则相反 A[PARENT(i)]<=A[i]。在堆排序算法中,我们使用最大堆,最小堆通常在构造优先队列时使用。
堆可以被看成一棵树,结点在堆中的高度定义为从本结点到叶子的最长简单下降路径上边的数目;定义堆的高度为树根的高度。因为具有n个元素的堆是基于一棵完全二叉树的,因而其高度为Θ(lgN)。
本章的西面部分将给出一些基本过程,并说明它们在排序算法和优先级队列数据结构中如何使用。
- MAX-HEAPIFY,运行时间为O(lgN)
- BUID-MAX-HEAP,以线性时间运行,可以在无序的输入数组基础上构造最大堆
- HEAP-SORT过程,运行时间为O(N*lgN),对一个数组进行原地排序
- MAX-HEAP-INSERT, HEAP-EXTRACT-MAX,HEAP-INCREASE-KEY和HEAP-MAXIMUM过程的运行时间为O(lgN),可以让堆结构作为优先级队列使用
练习:
6.1.7 证明:当用数组存储了n个元素的堆时,叶子结点的下标是 n/2+1, n/2+2,...,n
对于下标 k >= n/2+1, LEFT(k) >n,RIGHT(k)>n,可知其没有子节点,也就是这些结点都是叶子结点。
6.2保持堆的性质
MAX-HEAPIFY的输入为一个数组A和下标i,假定以LEFT(i)和RIGHT(i)为根的两棵子树都是最大堆,但这时候A[i]可能小于其子女,MAX-HEAPIFY让A[i]下降,以使i为根的子树成为最大堆。
MAX-HEAPIFY(A,i)
1 l <-- LEFT(i)
2 r <-- RIGHT(i)
3 if l<=heap-size[A] and A[l]>A[i]
4 then largest <-- l
5 else largest <-- i
6 if r<=heap-size[A] and A[r]>A[largest]
7 then largest <-- r
8 if largest != i
9 then exchange A[i]<-->A[largest]
10 MAX-HEAPIFY(A,largest)
算法的运行时间为调整A[i], A[LEFT(i)], A[RIGTH(i)]的时间和递归调用的时间,前者 Θ(1)。i结点子树最多为2n/2(最坏情况下发生在底层恰好半满的时候),那么MAX-HEAPIFY的运行时间可由下式描述: T(n) <= T(2n/3) + Θ(1), T(n) = O(nlgn)。
6.3建堆
我们可以自底向上地用MAX-HEAPIFY来将一个数组A[1...n]变成一个最大堆。
BUILD-MAX-HEAP(A)
1 heap-size[A] <-- length[A]
2 for i <-- length[A]/2 down to 1 ;参考练习6.1.7
3 do MAX-HEAPIFY(A,i)
我们可以计算出算法一个简单上界O(nlgn)。实际上,我们可以得到一个更加精确的界,这是因为在树中不同的高度的结点处运行MAX-HEAPIFY的时间不同,而且大部分结点的高度都较小。可得出上届O(n)。
PS:详细的计算方法见原书page76。
6.4 堆排序算法
开始时,堆排序算法先用BUILD-MAX-HEAP将输入数组A[1...n]建成一个最大堆。因为数组中的最大元素在根A[1],可以通过把它与A[n]互换来达到最终的正确位置。现在如果从堆中去掉结点n,可以很容易地将A[1...n-1]建成最大堆,原来根的子女仍是最大堆。如此重复,在这个过程中堆得大小由n一直降到2。
HEAP-SORT(A)
1 BUILD-MAX-HEAP(A)
2 for i<-- length[A] downto 2
3 do exchange A[1]<-->A[i]
4 heap-size[A] <-- heap-size[A]-1
5 MAX-HEAPIFY(A,1)
练习:
6.4.5 证明:在所有元素都不同时,堆排序算法的最佳运行时间是Ω(nlgn)。
分析: 堆排序的的运行时间由三部分组成, BUID-MAX-HEAP时间为O(n); 交换元素的操作执行了n-1次时间为O(n);MAX-HEAPIFY也执行了n次,每次执行MAX-HEAPIFY时,根节点都是值较小的点,这是因为新根总是通过交换最大堆的根和A[heap-size[A]]所得,所以MAX-HEAPIFY的执行时间必然是Ω(lgn'), n'为此时堆的大小。 于是最佳运行时间为 O(n) + Ω(∑k=2~nlgk) = Ω(lgn!)
PS: 从该题可以看出,堆排序属于一种代价较为稳定的排序算法,不会由于特殊的输入导致特差或特好的排序时间。
6.5优先级队列
虽然堆排序算法是一个很漂亮的算法,但在实际中,快速排序一个好的实现往往优于堆排序。尽管这样,堆数据结构还是有着很大的用处:其中一个就是作为高效的优先级队列,同样有两种最大优先级队列,最小优先级队列。
优先级队列是一种用来维护由一族元素构成的集合S的数据结构,这一组元素中的每一个都有一个关键字key,一个最大优先级队列支持一下操作:
INSERT(S,x) : 把元素x插入集合S。这一操作可写为S<--SU{x}
MAXIMUM(S): 返回S中具有最大关键字的元素
EXTRACT-MAX(S): 去掉并返回S中的具有最大关键字的元素。
INCREASE-KEY(S,x,k):将元素x的关键字值增加到k,k>x。
最大优先级队列的一个应用时在一台分时计算机上进行作业调度。
最小优先级队列支持的操作包括INSERT,MINIMUM,EXTRACT-MIN和DECREASE-KEY。这种队列可被用在基于事件驱动的模拟器中。
HEAP-MAXIMUM(A)
1 return A[1]
HEAP-EXTRACT-MAX(A)
1 if heap-size[A] < 1
2 then error
3 max <-- A[1]
4 A[1] <-- A[heap-size[A]]
5 heap-size[A] <-- heap-size[A] -1
6 MAX-HEAPIFY(A,1)
7 return max
HEAP-INCREASE-KEY(A,i,key)
1 if key < A[i]
2 then error
3 A[i] <-- key
4 while i>1 and A[PARENT(i)]<A[i]
5 do exchange A[i] <--> A[PARENT(i)]
6 i <-- PARENT(i)
MAX-HEAP-INSERT(A,key)
1 heap-size[A] <-- heap-size[A]+1
2 A[heap-size[A]] <-- -∞
3 HEAP-INCREASE-KEY(A,heap-size[A],key)
MAX-HEAP-INSERT首先加入一个关键字值为-∞的结点来扩展最大堆,然后调用HEAP-INCREASE-KEY来设置新结点的关键字的正确值,并保持最大性质。
练习:
6.5.8 请给出一个时间为O(nlgk)、用来将k个已排序链表合并为一个排序链表的算法。
分析:首先将k个链表作为k个结点,按链表的第一个元素的值为key,组成一个heap-size为k的最小堆;然后提取堆根处链表的第一个元素放在目标链表的尾处,此时根的第一个元素的key发生变化,再对其进行一次MIN-HEAPFIY操作即可,如此往复。
思考题:
6-1用插入方法建堆
第6.3节中的BUILD-MAX-HEAP过程可以通过反复调用MAX-HEAP-INSERT将各元素插入堆中来实现,考虑如下实现:
BUILD-MAX-HEAP'(A)
1 heap-szie[A] <-- 1
2 for i <-- 2 to length[A]
3 do MAX-HEAP-INSERT(A,A[i])
a)当输入数组相同时,过程BUILD-MAX-HEAP和BUILD-MAX-HEAP'产生的堆是否总是一样?
不同。反例序列 1, 2, 3。
b)证明:在最坏情况下,BUILD-MAX-HEAP'的时间复杂度为nlgn。
∑有算法for循环内的MAX-HEAP-INSERT的执行时间为 lgk, k=2~n。于是整个算法的复杂度为∑(k=2~n)lgk = lg n! = nlgn。
可知BUILD-MAX-HEAP'的时间复杂度劣于BUILD-MAX-HEAP,从a)的分析过程可知,前者进行了一些不必要的移动操作。
6-3 Young氏矩阵
一个m×n的Young氏是一个m×n的矩阵,其中每一行的数据都从左到右排列,每一列的数据都从上到下排序。Young氏矩阵中可能会有一些∞数据项,表示不存在的元素。所以Young矩阵可以用来存放r<=mn个有限的数。
a)略
b)略
c)给出一个在非空m×n的Young氏矩阵上实现EXTRACT-MIN的算法,使其运行时间为O(m+n)。
分析:Young矩阵在某种程度上像一个斜的最小堆,可以参考HEAP-EXTRACT-MIN的算法实现。但这里有个区别:Young矩阵不需要维护矩阵的大小是固定的,通过∞元素来表示无效元素;而堆是线性存储的,有一个heap-size属性来标识堆有效元素的个数;这样Young矩阵就没有“尾部元素“这个概念。于是可以考虑对HEAP-INCREASE-KEY的方法。先将Y[1,1]元素取出,再置为∞,再通过一个算法将矩阵调整为Young矩阵。后者可以参考MIN-HEAPIFY的思路,将Y[1,1]与Y[1,2]和Y[2,1]更小者进行交换,问题转化为m×(n-1)子矩阵Y(1,2,m,n)或(m-1)×n子矩阵Y(2,1,m,n)的调整,如此往复。
先证明该调整算法的正确性:对一个除结点[1,1]外满足Young矩阵特点的矩阵进行调整。按上述算法,假设将Y[1,1]与Y[1,2]进行了交换,可知原来的Y[1,2] <= Y[2,1],交换后第一列仍然满足从上到下有序的性质。 此时问题转化为对子矩阵Y(1,2,m,n)的调整,假设对Y(1,2,m,n)调整结束使其成为了一个Young举证,那么是否整个矩阵就是一个Young举证?关键就是所有的行是否仍然保持从左到右有序的特性,也即要证明向量Ak(1,n) [表示第k行]是有序的由于子矩阵Y(1,2,m,n)是Young矩阵,可知Ak(2,n)是有序的,由于在调整的过程中,只会出现将某个元素向上,或向左移动一格的情况,所以在子矩阵的调整过程中,可知Ak(2,n)中的元素只变大了。这样可知Ak(1,n) 必然还是有序的。证毕。
算法伪码如下:
EXTRAT-YOUNG-MIN(Y,m,n)
1 x = Y[1,1]
2 Y[1,1] = ∞
3 YOUNGIFY(Y,1,1,m,n)
4 return x
YOUNGIFY(Y,a,b,m,n)
1 minX = a;
2 minY = b;
3 if Y[a,b+1] < Y[minX,minY]
4 then minX = a
5 minY = b+1
6 if Y[a+1,b] < Y[minX,minY]
7 then minX = a+1
8 minY = b
9 if minX != a or minY != b
10 then Y[a,b] <--> Y[minX,minY]
11 YOUNGIFYY,minX,minY,m,n)
扩展1
d)如何在Young矩阵里面进行查找元素x。
分析:第一直觉应该是从对角线入手,沿着对角线查找,找到则返回,否则知道找到第一个大于x的位置为止,假设为[A,B]。那么可知子矩阵Y(1,1,a-1,b-1)中的元素都小于x,子矩阵Y(a,b,m,n)中的元素都大于x。问题转化为对子矩阵Y(a,1,m,b)和子矩阵Y(1,b,a,n)的查找。 该算法的复杂度应该大于 m+n。
其实可以从右下角进行查找,先看位置(m,1),如果Y[m,1]>x,则说明最后一行都比x大,于是问题转化为对子矩阵Y(1,1,m-1,n)的查找;如果Y[m,1]<x,说明第一列都比x小,于是问题转化为对子矩阵Y(1,2,m,n)的查找。可见一次比较可是矩阵的维度减小1。该算法的负载度为m+n。
扩展2
对于一个堆,它的查找操作可否从堆结构中获利?目前没想到什么方法。