1. 二叉堆
堆是一个数组A, 它可以被看成一个近似的完全二叉树
以(a)二叉树和(b)数组形式展现的是一个最大堆. 结点上方的数字是它在数组中相应的下标.
若一个结点下标为 , 可以得到它的父结点, 左孩子和右孩子的下标:
- PARENT(
)
- return
- LEFT(
):
- return
- RIGHT(
):
- return
更加快速地计算?
- 在大多数计算机中,
LEFT
过程可以在一条指令内(左移1位)计算出 ,RIGHT
过程可以通过将 左移1位并低位加1, 快速计算出 .RIGHT
过程可以通过把 的值右移1位计算得到 . 在堆排序的好的实现中, 这三个函数通常是以宏(define)
或内联函数(inline)
实现的.
注意: 根节点的下标不是0而是1
- 所以, 代码中的所有Index都需要考虑减去1, 满足数组的初始下标为0的规律
2. 重要步骤
- MaxHeapify: 负责维护最大堆, 时间复杂度为 lg
- BuildMaxHeap: 负责从一个无序的输入数据数组中构造最大堆, 时间复杂度为
- HeapSort: 负责对一个数组进行原址排序, 时间复杂度为 lg
3. 维护堆的性质
MaxHeapify(A, i)
是用于维护最大堆性质的重要过程. 在调用该函数时, 我们假定根结点为LEFT(
)和RIGHT(
)的二叉树都是最大堆. 如果A[
]小于其孩子, 则违背了最大堆的性质. MaxHeapify(A, i)
通过让A[
]的值在最大堆中”逐级下降”, 从而使下标为
的跟结点的子树重新遵循最大堆的性质.
下图展示了MaxHeapify
的过程
代码实现为:
/// <summary>
/// 负责维护最大堆
/// </summary>
private void MaxHeapify(int[] originInts, int rootIndex, int heapSize) {
if ((rootIndex << 1) + 1 > heapSize) return;
//如果当前结点不是叶子结点
var maxClildIndex = FindMaxClildIndex(originInts, rootIndex, heapSize);
//如果根结点满足大顶堆条件, 结束维护大顶堆过程
if (maxClildIndex == rootIndex) return;
//如果根结点小于子结点
Swap(ref originInts[maxClildIndex], ref originInts[rootIndex]);
//发生替换, 需要检查新的子结点是否满足大顶堆条件
MaxHeapify(originInts, maxClildIndex, heapSize);
}
思考
递归调用可能使某些编译器产生低效的代码, 可以用循环控制结构取代递归, 用伪代码重写为:
MAX-HEAPIFY(A, i)
while true
left = LEFT(i)
right = RIGHT(i)
if left > A.heap-size
return
largest = FIND-MAX-CHILD-INDEX(A, i)
if largest == i
return
exchange A[i] with A[largest]
i = largest
4. 建堆
我们可以用自底向上的方法利用BuildMaxHeap
把一个大小n = A.Length 的数组A[1..n]转换为最大堆. 每一个叶结点都可以看成只包含一个元素的堆(此时无需建立子堆). 过程BuildMaxHeap
对树中的其它结点(非叶子结点)都调用一次MaxHeapify
.
![](/qrcode.jpg)
我们利用heapSize
来表示每一次构成大顶堆时的总结点数(即堆的大小), 堆初始化时, heapSize = A.Length
下图展示了BuildMaxHeap
的过程
代码实现为:
/// <summary>
/// 负责从一个无序的输入数据数组中构造最大堆
/// </summary>
private void BuildMaxHeap(int[] originInts, int heapSize) {
//获得最后一个结点的父结点 -> 需要维护的起始结点
var currentIndex = (heapSize - 1) >> 1;
//自底向上建立大顶堆
while (currentIndex >= 0) {
MaxHeapify(originInts, currentIndex--, heapSize);
}
}
5. 堆排序算法
初始时, 堆排序算法利用BuildMaxHeap
将输入的数组A[1..n]建成最大堆. 其中n = A.Length. 因为数组中的最大元素总是在A[1]中, 通过把它与A[n]进行替换, 可以把该元素放到正确的位置. 此时, 需要从堆中去掉该结点(可以通过设置heapSize -= 1
来实现), 再将剩下的结点构造成大顶堆(通过调用MaxHeapify(A, 1)
)
下图展示了堆排序算法
的过程
代码实现为:
/// <summary>
/// 负责对一个数组进行原址排序
/// </summary>
public int[] HeapSort(int[] originInts) {
if (originInts == null || originInts.Length == 0) {
return originInts;
}
//需要排序的堆结点数
var heapSize = originInts.Length;
PrintArray("初始数组", originInts);
//构造大顶堆
BuildMaxHeap(originInts, heapSize);
var count = 1;
while (true) {
PrintArray("第" + count++ + "个大顶堆", originInts);
//取出最大的结点放到数组尾部>>Swap(A[1], A[heapSize])
Swap(ref originInts[0], ref originInts[heapSize-- -1]);
//如果此时只剩下一个结点, 排序结束
if (heapSize == 1) {
break;
}
//维护A[1]大顶堆, 直至heapSize == 1
MaxHeapify(originInts,0, heapSize);
}
PrintArray("排序后数组", originInts);
return originInts;
}
/// <summary>
/// 查看是否满足大顶堆条件
/// </summary>
/// <returns>返回一个最大的孩子索引或者父结点</returns>
private int FindMaxClildIndex(int[] originInts, int rootIndex, int heapSize) {
var leftChildIndex = (rootIndex << 1) + 1;
//排除已经排序好的子结点
if (leftChildIndex > heapSize - 1) {
return rootIndex;
}
var rightChildIndex = (rootIndex << 1) + 2;
var maxIndex = leftChildIndex;
//如果有右孩子, 比较左孩子和右孩子
if (rightChildIndex <= heapSize - 1) {
if (originInts[rightChildIndex] > originInts[leftChildIndex]) {
maxIndex = rightChildIndex;
}
}
//比较根结点和最大的孩子
if (originInts[rootIndex] > originInts[maxIndex]) {
maxIndex = rootIndex;
}
return maxIndex;
}
/// <summary>
/// 负责打印数组
/// </summary>
private void PrintArray(string info, int[] originInts) {
Console.Write(info + ": ");
foreach (var temp in originInts) {
Console.Write(temp + " ");
}
Console.WriteLine();
}
/// <summary>
/// 负责交换两个数字
/// </summary>
private void Swap(ref int a, ref int b) {
var temp = a;
a = b;
b = temp;
}
输入: { 3, 27, 4, 5, 6, 8 , 13, 46}
运行结果为:
初始数组: 3 27 4 5 6 8 13 46
第1个大顶堆: 46 27 13 5 6 8 4 3
第2个大顶堆: 27 6 13 5 3 8 4 46
第3个大顶堆: 13 6 8 5 3 4 27 46
第4个大顶堆: 8 6 4 5 3 13 27 46
第5个大顶堆: 6 5 4 3 8 13 27 46
第6个大顶堆: 5 3 4 6 8 13 27 46
第7个大顶堆: 4 3 5 6 8 13 27 46
排序后数组: 3 4 5 6 8 13 27 46