1. 二叉堆
堆是一个数组A, 它可以被看成一个近似的完全二叉树

以(a)二叉树和(b)数组形式展现的是一个最大堆. 结点上方的数字是它在数组中相应的下标.
若一个结点下标为 i i , 可以得到它的父结点, 左孩子和右孩子的下标:
- PARENT()
- return i/2 i / 2
- LEFT(
i
i
):
- return
- RIGHT(
i
i
):
- return
更加快速地计算?
- 在大多数计算机中,
LEFT过程可以在一条指令内(左移1位)计算出 2i 2 i ,RIGHT过程可以通过将 i i 左移1位并低位加1, 快速计算出.RIGHT过程可以通过把 i i 的值右移1位计算得到. 在堆排序的好的实现中, 这三个函数通常是以宏(define)或内联函数(inline)实现的.
注意: 根节点的下标不是0而是1
- 所以, 代码中的所有Index都需要考虑减去1, 满足数组的初始下标为0的规律
2. 重要步骤
- MaxHeapify: 负责维护最大堆, 时间复杂度为 O( O ( lg n) n )
- BuildMaxHeap: 负责从一个无序的输入数据数组中构造最大堆, 时间复杂度为 O(n) O ( n )
- HeapSort: 负责对一个数组进行原址排序, 时间复杂度为 O(n O ( n lg n) n )
3. 维护堆的性质
MaxHeapify(A, i)是用于维护最大堆性质的重要过程. 在调用该函数时, 我们假定根结点为LEFT( i i )和RIGHT()的二叉树都是最大堆. 如果A[ i i ]小于其孩子, 则违背了最大堆的性质.MaxHeapify(A, i)通过让A[]的值在最大堆中”逐级下降”, 从而使下标为 i i 的跟结点的子树重新遵循最大堆的性质.下图展示了
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 = largest4. 建堆
我们可以用自底向上的方法利用
BuildMaxHeap把一个大小n = A.Length 的数组A[1..n]转换为最大堆. 每一个叶结点都可以看成只包含一个元素的堆(此时无需建立子堆). 过程BuildMaxHeap对树中的其它结点(非叶子结点)都调用一次MaxHeapify.我们利用
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 466. 堆排序的应用
7. 参考
- 在大多数计算机中,
本文详细介绍了堆排序算法,包括二叉堆的概念、重要步骤、维护堆的性质、建堆过程、堆排序的代码实现及应用。堆排序的时间复杂度为O(nlgn),适用于原址排序。此外,文章还讨论了如何使用最大堆实现最大优先队列。

32万+

被折叠的 条评论
为什么被折叠?



