堆排序的复杂度是 O(nlogn) O ( n l o g n ) ,因为使用了一种特殊的数据结构,因此称之为堆。后面将会解释。
定义:设 T=(V,E) T = ( V , E ) 是一棵完全二叉树,映射 a:V→M a : V → M 将树中的节点映射的一个有序集合 M M ,每个顶点的值 都在集合 M M 中。
解释一下, 表示顶点的集合, E E 表示边集合。
1. 堆特征
对某个顶点 ,如果其直接后代节点小于等于顶点
u
u
上的值,即
则称其具备堆特征(heap property)。上面用记号 (u,v) ( u , v ) 这种写法来表示一条边,后面类推。
说通俗点,就是说二叉树中任意一个节点值都大于等于它的孩子节点值,则该顶点具备了堆特征。所以堆特征,是对于一个节点来说的,它是节点的性质。
2. 堆
如果树
T
T
中的所有顶点都具备堆特征,则称 是一个堆(heap)。即
3. 半堆
如果树
T
T
中除了根节点外的所有顶点都具备堆特征,则称 是一个半堆(semi-heap)。
图1 含有10个节点的堆
需要注意的是,叶节点属于平凡情况,本身固有 堆特征。
4. 堆排序
在实现中,并不需要借助指针来表示树结构,因为完全二叉树可以使用数组高效的表示。
4.1 堆排序算法
(a) (b) (c)
(d) (e)
图2 从堆中取最大元素并将剩余节点恢复成堆
如果序列被调整为堆,则可立即从根节点取得一个最大元素(图2(a))。为了能够获取下一个最大值,剩余节点必须要重新调整为一个新的堆。
重新调整可以按照下面的方式进行:
设 b b 是深度最大的一个叶节点,为了方便,选取最后一个节节点,即最下层的最右边那个。把 的值写入根节点,然后删除 b b ,参考图2(b)。这时候树变成了一个半堆,因为根节点已经不具备堆特征了。
将半堆调整为堆是相当容易的。具体做法如下。
如果根节点具备堆特征,什么都不用做,否则将其与最大后代(设为 )交换,如图2(c)。这时候很有可能 v v 也丢失了堆特征。同样的,可以将以 为根节点的半堆调整为堆。当顶点都具备了堆特征后,这个过程就停止,最终的情况是到达了叶节点,因为叶节点固有堆特征。
这里把这个过程称之为 sink(v)
4.2 sink 算法
sink(v)
算法的目的是把半堆调整为堆。
输入:根节点为
v
v
的半堆
输出:堆
算法:
while not hasHeapProperty(v):
选择最大后代 w
exchange a(v) and a(w)
v = w
4.3 buildHeap算法
sink
算法也可以将任意的一个树调整为堆。自底向上,针对树中内节点(不是叶节点的节点)调用sink
算法,当然,因为叶节点固有堆特征,因此可以忽略而不必再调用sink
。所有不是叶节点的节点,通常称之为内节点。
输入:任意一棵完全二叉树 ,深度为
d(T)
d
(
T
)
输出:堆
算法:
for i = d(T)-1 to 0: # 从倒数第二层开始
for v in d(v) == i: # 从右往左数
sink(v)
4.4 heapSort算法
输入:一棵完全二叉树
T
T
,根节点为
输出:降序排列的数组。
算法:
buildHeap
while not isLeaf(r):
output a(r)
b = getLastVertex()
a(r) = a(b)
delete b
sink(r)
output a(r)
4.5 实现
图3 用数组表示的含有10个节点的完全二叉树
对于用数组表示的完全二叉树来说,具有以下性质。
- 根节点存储在索引为 0 的位置
- 位置为 v v 的节点的两个后代的位置分别是 和 2v+2 2 v + 2
- 位置 0,...,n/2−1 0 , . . . , n / 2 − 1 的节点是内节点,位置 n/2,...,n−1 n / 2 , . . . , n − 1 都是叶子节点
在实现这个算法时,可以借助一些技巧而不使用任何额外的空间。在heapSort过程中,a(r)并不需要真的输出,实际编写过程中,把它存放到最后一个被删除的叶节点中。删除叶节点 b b 的意思只是表示在调整堆的过程中叶节点不用再考虑。
总结一下heapSort:
- 输出根节点的值
- 选择最后一个叶节点 b b
- 把 的值写入根
- 删除 b b <script type="math/tex" id="MathJax-Element-38">b</script>
- sink(r)
5. C代码实现
void exchange(int a[], int i, int j){
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
void sink(int a[], int v, int n){
int w = 2*v + 1;// 第一个直接后代
while(w < n){
if(w+1 < n && a[w+1] > a[w]) ++w;
if(a[v] >= a[w]) return; // 具备堆特征
exchange(a, v, w);
v = w;
w = 2*v + 1;
}
}
void buildHeap(int a[], int n){
for(int v = n/2 - 1; v >= 0; --v){
sink(a, v, n);
}
}
void helpSort(int a[], int n){
buildHeap(a, n);
while(n > 1){
--n;
exchange(a, 0, n);
sink(a, 0, n);
}
}