堆排序(heapsort) 和 归并排序 一样, 它的时间复杂度也是 O(nlgn)
.
回忆一下, 归并排序 使用的 算法设计方法: 分治法.
而 堆排序 使用另一种算法设计技术: 使用 堆(heap) 的数据结构进行信息管理.
虽然 堆 这一词源自 堆排序, 但是目前它已经被引申为 “垃圾收集存储机制”, 比如 Java 中内存模型的 堆 结构.
1 堆
在介绍堆之前, 需要了解 树 的基础知识. 若已了解, 直接看 1.2.
1.1 树的基础知识
对于一棵有根树 T
(根为r
) 中的 结点x
, x
的 孩子个数 称之为 度(degree), 从 r
到 x
的简单路径 称之为 深度(depth). 树T
的最大深度 也是 树的高度(height).
满二叉树(full binary tree):
每个结点 是 叶子结点(leaf) 或者 度(degree)为 2
.
对于 满二叉树 中的任一一个结点, 要么度是0
, 要么度是1
, 不存在度为1
的结点.
完全k叉树(complete k-ary tree):
所有 叶子结点的 深度 相同, 且 所有 内部结点 的 度 都是 k
.
看下图:
图 (1) 是满二叉树(除叶子结点外, 内部结点的度都为2), 但 不是 完全二叉树(叶子结点的深度不同).
图 (2) 既不是 满二叉树, 也不是 完全二叉树.
图 (3) 既是 满二叉树, 也是 完全二叉树.
以上 满二叉树 和 完全二叉树 的定义是 <算法导论> 中的定义.
国内的教材和 <算法导论> 中的定义不一样.
国内 “满二叉树” 和 <算法导论> 中的 “完全二叉树” 定义是一样的.
国内 “完全二叉树” 定义:
叶子结点只能出现在最下层和次下层, 且最下层的叶子结点集中在树的左部.
上图 (2) 中的 二叉树 符合 国内 “完全二叉树” 的定义.
1.2 堆的性质
如下图所示, (binary) heap, (二叉)堆 是一个数组A[0..9]
, 它可以被看成一个 近似的完全二叉树(nearly complete binary tree).
树中的每一个结点 对应 数组中的一个元素. 除了最顶层外, 该树是完全充满的, 而且 从左向右 填充.
通常, 数组的长度 A.length
并一定是 堆 的大小, 堆 的大小我们用 heapSize
表示. 其中, 1<= heapSize <= A.length
.
虽然 A[0..length-1]
都有数据, 但只有 A[0..heapSize-1]
中存放的事堆的有效元素.
对于一个堆(近似完全二叉树)来说, 给定一个结点的下标 i
, 我们很容易能得到 它 的 父节点, 左孩子, 和 右孩子 的下标.
i
的父节点的下标:
parent(i)
return (i - 1)/2
i
的左孩子的下标:
left(i)
return 2i + 1
i
的右孩子的下标:
right(i)
return 2*(i + 1)
二叉堆有两种形式:
- 最大堆(max-heap) 也叫 大根堆, 满足
A[parent(i)] >= A[i]
, 即满足 结点x
的值 大于等于 其孩子结点的值. - 最小堆(min-heap) 也叫 小根堆, 满足
A[parent(i)] <= A[i]
, 即满足 结点x
的值 小于等于 其孩子结点的值.
在 堆排序算法中, 我们使用的是 最大堆.
2 维护堆的性质
我们假定 根结点 为 left(i)
和 right(i)
的二叉树都是 最大堆.
但这时 A[i]
可能小于其孩子(A[left(i)]
或 A[right(i)]
), 这就违背了 最大堆 的性质.
我们使用 maxHeapify
来维护最大堆性质.
maxHeapify
通过让 A[i]
的值在最大堆中"逐级下降", 从而使下标为 i
的根结点的子树 重新遵循 最大堆 的性质.
考虑下图情况 (a):
以 A[4]
为根的堆是 最大堆, 以 A[5]
为根的堆也是 最大堆. 但以 A[i]
(i=2) 为根的堆 不是最大堆.
由于 A[i]
违反了 最大堆 的性质, 所以需要对 A[i]
结点进行调整以使其满足最大堆性质.
(备注: 这个图是我从书上剪切过来的, 下标是从1
开始的, 但这并不妨碍理解算法.)
为了把 A[i]
放到合适合适的位置, 我们需要用 A[i]
和 A[left(i)]
, A[right(i)]
这三者做对比, 用最大的结点做 根.
如上图(b)所示, 14
比 4
, 7
都要大, 因此用 14
作为 根, 把 4
和 14
互换位置, 并将 i
指向 left(i)
.
此时, i=4
, 但 A[i]
仍然不满足最大堆性质. 再用 A[i]
和 A[left(i)]
, A[right(i)]
这三者做对比, 用最大的结点做 根.
如上图(c)所示, 8
比 2
, 4
都要大, 因此用 8
作为 根, 把4
和8
互换位置, 并将 i
指向 right(i)
.
此时, 整个堆已经满足 最大堆性质, maxHeapify
完成.
这个过程用代码描述:
private int left(int i) {
return 2 * i + 1;
}
private int right(int i) {
return 2 * (i + 1);
}
public void maxHeapify(int[] a, int i, int heapSize) {
int l = left(i);
int r = right(i);
int largestIndex;
if (l < heapSize && a[l] > a[i]) {
largestIndex = l;
} else {
largestIndex = i;
}
if (r < heapSize && a[r] > a[largestIndex]) {
largestIndex = r;
}
if (largestIndex != i) {
int temp = a[i];
a[i] = a[largestIndex];
a[largestIndex] = temp;
maxHeapify(a, largestIndex, heapSize);
}
}
对于n
个结点, 树高h
的堆来说, maxHeapify
的时间复杂度是O(h)
或 O(lgn)
.
3 建堆
我们可以 自底向上 利用 maxHeapify
把数组转换成 最大堆.
具体操作步骤:
从倒数第二层的最右边的非叶子结点的下标(heapSize / 2 - 1
)开始遍历, 每次下标减一, 遍历到下标为 0
后结束. 每次迭代时调用 maxHeapify
把当前堆构造成最大堆.
heapSize / 2 - 1
在数组a[0..heapSize]
中的位置 是 最后一个非叶子结点的位置;
heapSize/2
, heapSize/2 +1
…, heapSize
这些下标都是叶子结点.
构建 最大堆 的代码实现:
public void buildMaxHeap(int[] a) {
int heapSize = a.length;
// i 从倒数第二层最右边的 非叶子结点开始构造
for (int i = (heapSize / 2 - 1); i >= 0; i--) {
maxHeapify(a, i, heapSize);
}
}
buildMaxHeap
需要O(n)
时间, maxHeapify
需要O(lgn)
时间, 因此buildMaxHeap
的时间复杂度是 O(nlgn)
.
O(nlgn)
只是一个粗略的上界, 根据<算法导论>严格的推导, buildMaxHeap
更精确的时间复杂度是 O(n)
.
4 堆排序算法
堆排序算法思想:
先构建一个最大堆;
从 i = heapSize-1
遍历到 i = 1
, 每次迭代: 先交换 a[0]
和 a[i]
, 再 heapSize--
, 再用 maxHeapify(a, i, heapSize)
重新构建堆.
每次迭代都会把最大元素放在 有效堆 的最后, 同时把 有效堆 的大小(heapSize
)缩小 1
. 这样得到的效果是每次迭代把 有效堆 中 一个最大的元素排好序.
堆排序 的代码实现:
public void heapSort(int[] a) {
buildMaxHeap(a);
int heapSize = a.length;
for (int i = a.length - 1; i >= 1; i--) {
int temp = a[i];
a[i] = a[0];
a[0] = temp;
heapSize = heapSize - 1;
maxHeapify(a, 0, heapSize);
}
}
heapSort
的时间复杂度是 O(nlgn)
.
堆排序代码的实现很简单. 主要是要理解 最大堆结构和数组的关系 以及 维护最大堆性质 和 建堆 这两个操作.
附录: 堆排序代码
public class HeapSort2 {
private int parent(int i) {
return (i - 1) / 2;
}
private int left(int i) {
return 2 * i + 1;
}
private int right(int i) {
return 2 * (i + 1);
}
public void maxHeapify(int[] a, int i, int heapSize) {
int l = left(i);
int r = right(i);
int largestIndex;
if (l < heapSize && a[l] > a[i]) {
largestIndex = l;
} else {
largestIndex = i;
}
if (r < heapSize && a[r] > a[largestIndex]) {
largestIndex = r;
}
if (largestIndex != i) {
int temp = a[i];
a[i] = a[largestIndex];
a[largestIndex] = temp;
maxHeapify(a, largestIndex, heapSize);
}
}
public void buildMaxHeap(int[] a) {
int heapSize = a.length;
// i 从第一个非叶子结点开始构造
for (int i = (heapSize / 2 - 1); i >= 0; i--) {
maxHeapify(a, i, heapSize);
}
}
public void heapSort(int[] a) {
buildMaxHeap(a);
int heapSize = a.length;
for (int i = a.length - 1; i >= 1; i--) {
int temp = a[i];
a[i] = a[0];
a[0] = temp;
heapSize = heapSize - 1;
maxHeapify(a, 0, heapSize);
}
}
public static void main(String[] args) {
int[] a = {4, 5, 1, 2, 7, 11, 3};
HeapSort2 heapSort = new HeapSort2();
heapSort.heapSort(a);
System.out.println(Arrays.toString(a));
}
}
Reference
[1]. 算法导论(第三版)