堆排序
参考:
https://www.bilibili.com/video/BV1Eb41147dK?from=search&seid=7630499509795698118
https://baike.baidu.com/item/%E5%A0%86%E6%8E%92%E5%BA%8F/2840151?fr=aladdin
https://baike.baidu.com/item/%E5%A0%86/20606834?fr=aladdin
一,堆排序概述
1,堆排序(Heapsort)
堆排序是利用堆这种数据结构所设计的一种排序算法。
2,堆(heap)
① 堆通常是一个可以被看做一棵树的数组对象。
② 堆满足以下两点性质:
Ⅰ 堆是一颗完全二叉树,其添加新节点的顺序是:从上到下,从左到右
Ⅱ 子节点<= 父节点 或 子节点 >= 父节点
③ 堆可以根据节点大小分为大根堆和小根堆,其中,根结点最大的堆为大根堆,根结点最小的堆为小根堆
3,堆影响排序结果
大根堆的排序结果是升序序列
小根堆的排序结果是降序序列
4,算法思想(升序)
堆排序的本质与选择排序一样,不过堆排序是利用维护堆来进行选择,从而将选择的时间复杂度降下来。
具体来讲,堆排序是将有序区设在数组的末尾,然后把无序区的元素构造成大根堆,然后从堆(无序区中)选择一个最大值,交换到无区序的第一个元素 / 最后一个元素p,作为新选择出来的元素添加到有序区。一直循环到无序区为空。
二,heapify 函数(堆调整 / 筛选)
1,前提说明:
由于堆是用数组(数组下标从 0 开始)表示的二叉树,所以对于结点 i ,我们就可知道其父节点与两个子节点的下标(如果有的话)
父节点 P:(i - 1) / 2
左子节点 C1:2 * i + 1
右子节点 C2:2 * i + 2
2,函数功能:
(默认子节点以下的所有最小单位的完全二叉树都已经构造成堆)
将结点 i 及其两个子节点组成的一颗最小单位的完全二叉树,构造成堆,同时递归维护 C1 和 C2 的子孙节点所组成的所有最小单位的堆。
又因为是尾递归,所以也可以用循环替换。
3,注意事项
① C1 和 C2 要判断有无超出边界,即 C1 和 C2 的下标是否小于 n
② 函数的递归出口为:子节点 i 的下标大于等于 n
因为,递归是往从父节点往叶节点遍历,所以只可能是下溢出
三,buildHeap 函数(构建堆)
1,函数功能
将一棵完整的完全二叉树构造成堆
2,函数思路
由于 heapify 会递归调用维护下面的堆,所以要构造完整的堆,只需要从下面往上构造:
即从最后一个父结点开始向上调用 heapify
3,最后一个父节点的下标
因为最后一个节点的下标为:n-1,
所以最后一个父节点的下标就是最后一个节点的父节点的下标,即 ((n-1) - 1 ) / 2
四,heapSort 函数(堆排序)
1,函数功能
利用堆进行堆排序
2,算法思想
上面有讲。
3,算法步骤
① 将根节点与堆的最后一个节点交换,每次交换后,将最后一个节点踢出堆的区间范围。
② 用 heapify 函数维护堆
没有必要用 buildHeap函数,因为此时只有根节点所在的最小单位的堆出了问题,其它的最小单位的堆都是正常的。
五,堆排序代码
递归
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 110 int a[N]; void swap(int i, int j) // 交换结点位置 { int t = a[i]; a[i] = a[j]; a[j] = t; } void heapify(int n, int i) // 堆调整 { if (2 * i + 1 >= n) return; int c = 2 * i + 1; if (c + 1 < n&&a[c] < a[c + 1]) // 求两个子节点中的较大值 c++; if (a[c] > a[i]) { swap(c, i); // 将最大值移至父节点 heapify(n, c); // 递归维护下面的结点 } } void buildHeap(int n) // 构造堆 { int last = n - 1; int p = (last - 1) >> 1; // 最后一个父结点 for (int i = p; i >= 0; i--) // 从最后一个父结点开始向上调用 heapify heapify(n, i); } void heapSort(int n) // 利用堆进行堆排序 { buildHeap(n); for (int i = n - 1; i > 0; i--) { swap(i, 0); // 交换根节点与堆的最后一个节点 heapify(i, 0); // 维护根节点所在的最小单位的堆 } } int main(void) { int n; while (scanf("%d", &n) != EOF) { for (int i = 0; i < n; i++) scanf("%d", &a[i]); heapSort(n); for (int i = 0; i < n; i++) printf("%d ", a[i]); puts(""); } }
非递归
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 110 int a[N]; void swap(int i, int j) // 交换结点位置 { int t = a[i]; a[i] = a[j]; a[j] = t; } void heapify(int n, int i) // 堆调整 { while (2 * i + 1 < n) { int c = 2 * i + 1; if (c + 1 < n&&a[c] < a[c + 1]) // 求两个子节点中的较大值 c++; if (a[c] > a[i]) { swap(c, i); // 将最大值移至父节点 i = c; } else break; } } void buildHeap(int n) // 构造堆 { int last = n - 1; int p = (last - 1) >> 1; // 最后一个父结点 for (int i = p; i >= 0; i--) // 从最后一个父结点开始向上调用 heapify heapify(n, i); } void heapSort(int n) // 利用堆进行堆排序 { buildHeap(n); for (int i = n - 1; i > 0; i--) { swap(i, 0); // 交换根节点与堆的最后一个节点 heapify(i, 0); // 维护根节点所在的最小单位的堆 } } int main(void) { int n; while (scanf("%d", &n) != EOF) { for (int i = 0; i < n; i++) scanf("%d", &a[i]); heapSort(n); for (int i = 0; i < n; i++) printf("%d ", a[i]); puts(""); } }
六,堆排序算法分析
空间复杂度
仅用常数个辅助单元,所以空间复杂度为 O(1)
时间复杂度
建堆时间为 O(n),之后有 n-1 次堆调整,调整的时间与树高有关,为 O(h),所以,时间复杂度为 O(nlog2n)
算法的稳定性
因为在 heapSort 函数中,可能把原本在前面的值相同的元素交换到后面去,所以堆排序算法是一种不稳定的排序算法。
========== ========= ========= ======= ====== ===== ==== === == =
寄扬州韩绰判官 杜牧 唐
青山隐隐水迢迢,秋尽江南草未凋。
二十四桥明月夜,玉人何处教吹箫。