企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
堆排序是一种树形选择排序,是对直接选择排序的有效改进。 堆的构建--》堆排: 初始状态-<从最后一个结点开始,使该子树成堆(最小/大的数移到根节点),不断循环>-初始堆(小/大顶)--输出堆顶元素(堆顶与堆的最后一个元素交换位置)--最后一个数移至堆顶--重新建--输出堆顶元素 以下为《数据结构(C++版)习题解答与实验指导》实例 ![](https://box.kancloud.cn/2016-03-08_56de49ff15de1.jpg) ![](https://box.kancloud.cn/2016-03-08_56de49ff864e8.jpg) ![](https://box.kancloud.cn/2016-03-08_56de49ffa5af8.jpg) 1、算法思想 堆:具有n个元素的序列(k1,k2,...,kn),当且仅当满足 ![](https://box.kancloud.cn/2016-03-08_56de49ffcc04b.jpg) 时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(最大项)。 若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如: (a)大顶堆序列:(96, 83,27,38,11,09) (b)小顶堆序列:(12,36,24,85,47,30,53,91) ![](https://box.kancloud.cn/2016-03-08_56de49ffdd9df.jpg) 初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。 因此,实现堆排序需解决两个问题: 1. 如何将n 个待排序的数建成堆; 2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。 首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。 调整大顶堆的方法: 1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。 2)将根结点与左、右子树中较大元素的进行交换。 3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2). 4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2). 5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。 称这个自 根结点 到 叶子结点 的调整过程为筛选。如图: ![](https://box.kancloud.cn/2016-03-08_56de49ffee3ce.jpg) 再讨论对n 个元素初始建堆的过程。 建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。 1)n 个结点的完全二叉树,则最后一个结点是第![](image/56a4af218ef1b.jpeg) 个结点的子树。【向下取整】 2)筛选从第 个结点为根的子树开始,该子树成为堆。 3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。 如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49) ![](https://box.kancloud.cn/2016-03-08_56de4a0014329.jpg) 2、代码实现 ~~~ package sort; public class HeapSort { public static void main(String[] args) { int[] src = { 66, 89, 8, 123, 9, 44, 55, 37, 200, 127, 98 }; System.out.println("初始值:"); print(src); HeapSort(src, src.length); System.out.println("堆排后:"); print(src); } /** * 大顶堆排序算法 */ private static void HeapSort(int src[], int length) {// 大顶堆排序算法 // 初始化堆,从最后一个节点 i= (length-1) / 2 for (int i = (length - 1) >> 1; i >= 0; --i) HeapAdjust(src, i, length); for (int i = length - 1; i > 0; --i) {// 从堆尾往前调整 src[i] = src[0] + (src[0] = src[i]) * 0;// 弹出堆顶最大值,堆尾值填补 HeapAdjust(src, 0, i);// 重新调整堆 } } /** * src[i+1,…. ,j]已经成堆,调整 i 的子树为堆. * * @param src是待调整的堆数组 * @param i是待调整的数组元素的位置 * @param j是待调整数组的长度 */ private static void HeapAdjust(int src[], int i, int j) {// 对下标为i的节点,调整其子树为堆 int temp = src[i]; int child = 2 * i + 1; // 左孩子的位置。(child+1 为当前调整结点的右孩子的位置) while (child < j) {// 防止第一次length为偶数12时,i=5与child=11非父子关系 if (child + 1 < j && src[child] < src[child + 1]) { // 定位较大的的孩子结点 ++child; } if (src[i] < src[child]) { // 如果较大的子结点大于父结点 src[i] = src[child]; // 那么把较大的子结点往上移动,替换它的父结点 i = child; // 更新i,以便使新子树为堆(子树结构可能改变) src[i] = temp; // 父结点放到比它大的孩子结点上(temp值未变) child = 2 * i + 1;// 若child<length,则继续循环建堆 } else { // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出 break;// 防止死循环 } } print(src); } private static void print(int a[]) { for (int i : a) { System.out.print(i + " "); } System.out.println(); } } ~~~ 3、算法分析 堆排序也是一种不稳定的排序算法。  堆排序优于简单选择排序的原因: 直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次比较。事实上,后面的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。 堆排序的**最坏时间复杂度为O(nlogn)**。堆序的平均性能较接近于最坏性能。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。因为其运行时间主要耗费在建初始堆和调整建新堆时进行的反复“筛选”上。 堆排序在最坏的情况下,其时间复杂度也为O(nlogn)。相对于快速排序来说,这是堆排序的最大优点。此外,堆排序仅需一个记录大小的供交换用的辅助存储空间。 建堆时对于每个非终端结点来说,其实最多进行两次比较和互换操作,比较次数不超过4n 次,因此整个构建堆的时间复杂度为O(n)。 设树深度为k![](https://box.kancloud.cn/2016-03-08_56de4a002b99f.jpg) ,从根到叶的筛选,元素比较次数至多2(k-1)次,交换记录至多k 次。所以,在建好堆后,排序过程中的筛选次数不超过下式: ![](https://box.kancloud.cn/2016-03-08_56de4a0039f4a.jpg) 因此堆排序最坏情况下,**时间复杂度也为:O(nlogn )**。 4、算法改进 堆排序可通过树形结构保存部分比较结果,可减少比较次数。 源码地址:[https://github.com/zxiaofan/Algorithm/blob/master/src/sort/HeapSort.java](https://github.com/zxiaofan/Algorithm/blob/master/src/sort/HeapSort.java)