堆排序的时间复杂度是 O(nlgn) ,堆排序具有空间原址性,任何时候都只需要常数个额外的元素空间存储临时数据。
堆的介绍
(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树。树上的每一个结点对应数组中的一个元素。除了最底层外,该树是完全充满的,而且从左向右填充。如下图:
表示堆数组A包括两个属性:A.length通常给出数组元素的个数,A.heap-size表示有多少个堆元素存储在该数组中。即虽然A[1..A.length]可能都存有数据,但只有A[1..A.heap-size]中存放的是堆的有效元素。这里 0≤A.heap−size≤A.length 。树的根结点是A[1],这样给定一个结点的下标i, 我们很容易计算得到它的父结点、左孩子和右孩子的下标:
PARENT(i)
return[i/2]
LEFT(i)
return [2i]
RIGHT(i)
return [2i+1]
上面的这个三个函数通常是以“宏”或“内联函数”的方式实现的。
二叉堆可以分为两种形式:最大堆和最小堆。
最大堆的性质是指除了根以外的所有结点都要满足:
A[PARENT(i)]≥A[i]
最小堆的性质是指除了根以外的所有结点都要满足:
A[PARENT(i)]≤A[i]
在堆排序算法中,我们使用的是最大堆。最小堆通常用于构造优先队列。
维护堆的性质
MAX-HEAPIFY是用于维护最大堆性质的重要过程。它的输入为一个数组A和一个下标i。在调用MAX-HEAPIFY的时候,我们假定根结点为LEFT(i)和RIGHT(i)的二叉树都是最大堆,但这时A[i]有可能小于其孩子,这样就违背了最大堆的性质。MAX-HEAPIFY通过让A[i]的值在最大堆中“逐级下降”,从而使得以下标i为根结点的子树重新遵循最大堆的性质。
MAX-HEAPIFY(A, i)
l = LEFT(i)
r = RIGHT(i)
if l <= A.heap-size and A[l] > A[i]
largest = l
else largest = i
if r <= A.heap-size and A[r] > A[largest]
largest = r
if largest != i
exchange A[i] with A[largest]
MAX-HEAPIFY(A, largest)
一个实例过程如下:
上面伪代码中,第10行中的递归调用可能会使某些编译器产生低效的代码。所以可以用循环控制结构来取代递归,重写MAX-HEAPIFY代码
MAX-HEAPIFY(A, i)———-无递归调用
while(true)
l = LEFT(i)
r = RIGHT(i)
if l <= A.heap-size and A[l] > A[i]
largest = l
else largest = i
if r <= A.heap-size and A[r] > A[largest]
largest = r
if largest != i
exchange A[i] with A[largest]
i = largest
else
break
维护堆的时间复杂度为 T(n)=O(lgn) 也就是说对于一个树高为h的结点来说,MAX-HEAPIFY的时间复杂度为O(h)
建堆
我们可以用自底向上的方法利用过程MAX-HEAPIFY把一个大小为n=A.length的数组A[1..n]转换为最大堆。我们知道,子数组A[n/2+1..n]中的元素都是树的叶子结点。每个叶子结点都可以看成包含一个元素的堆。过程BUILD-MAX-HEAP对树中的其他结点都调用一次MAX-HEAPIFY。
BUILD-MAX-HEAP(A)
A.heap-size = A.length
for i = [A.length/2] downto 1
MAX-HEAPIFY(A, i)
我们通过简单方法估算BUILD-MAX-HEAP(A)运行时间上界。每次调用MAX-HEAPIFY的时间复杂度为O(lgn),BUILD-MAX-HEAP需要O(n)次这样的调用。因此,总的时间复杂度是O(nlgn).当然这个上界虽然是正确的但不是渐近紧确的。BUILD-MAX-HEAP的时间复杂度渐近紧确的上界为O(n).
建堆的一个实例过程如下:
堆排序算法
初始时候,堆排序算法利用BUILD-MAX-HEAP将输入数组A[1..n]建成最大堆,其中n=A.length。因为数组中的最大元素总在根结点A[1]中,通过把它与A[n]进行互换,我们可以让该元素放到正确的位置。这时候,如果我们从堆中去掉结点n(这一操作可以通过减少A.heap-size的值来实现),剩余的结点中,原来根的孩子结点仍然是最大堆,而新的根结点可能会违背最大堆的性质。为了维护最大堆的性质,我们要做的是调用MAX-HEAPIFY(A, 1),从而在A[1..n-1]上构造一个新的最大堆。堆排序算法会不断重复这一过程,直到堆的大小从n-1降到2.
HEAPSORT(A)
BUILD-MAX-HEAP(A)
for i = A.length downto 2
exchange A[1] with A[i]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 1)
堆排序算法的时间复杂度为O(nlgn)
如下为一个堆排序的实例:
优先队列
堆排序是一个优秀的算法,但是在实际应用中,快速排序的性能一般要优于堆排序。尽管如此,堆这一结构仍然有很多应用。比如:作为高效的优先队列。和堆一样,优先队列也有两种形式:最大优先队列和最小优先队列。
优先队列(priority queue)是一种用来维护由一组元素构成的集合S的数据结构体。其中的每一个元素都一个相关的值,称为关键字(key)。一个最大优先队列支持一下操作:
INSERT(S, x): 把元素x插入集合S中,这一操作等价于
S=S∪{x}
MAXIMUM(S):返回S中具有最大关键字的元素
EXTRACT-MAX(S): 去掉并返回S中的具有最大关键字的元素
INCREASE-KEY(S, x, k): 将元素x的关键字值增加到k, 这里假设k的值不小于x的原关键字值.
用途:
最大优先队列的应用很多,其中一个就是在共享计算机系统的作业调度。
最小优先队列可以被用于基于事件驱动的模拟器。
HEAP-MAXIMUM(A)
return A[1]
HEAP-EXTRACT-MAX(A)
if A.heap-size < 1
error "heap underflow"
max = A[1]
A[1] = A[A.heap-size]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 1)
return max
HEAP-EXTRACT-MAX的时间复杂度 T(n)=O(lgn)
HEAP-INCREASE-KEY(A, i, key)
if key < A[i]
error "new key is smaller than current key"
A[i] = key
while i > 1 and A[PARENT(i) < A[i]]
exchange A[i] with A[PARENT(i)]
i = PARENT(i)
HEAP-INCREASE-KEY时间复杂度为O(lgn), 它的一个实例如下图:
MAX-HEAP-INSERT(A, key)
A.heap-size = A.heap-size + 1
A[A.heap-size] = -∞
HEAP-INCREASE-KEY(A, A.heap-size, key)
MAX-HEAP-INSERT时间复杂度为T(n)=O(lgn)