❤️ 堆排序
💟 堆排序实现思路
堆排序(HeapSort)是利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。
堆分为大根堆和小根堆,并且堆必须为完全二叉树,使用物理上使用数组实现。
- 大堆:父节点总是大于子节点的堆
- 小堆:父节点总是小于子节点的堆
✏️ 实现思路:
1、建堆
- 升序:建大堆
- 降序:建小堆
2、利用堆删除思想来进行排序
🖊 疑问解决:
1、堆排序时为什么升序要建大堆?降序建小堆?
- 升序建小堆时,如图:
- 建好小堆后,我们需要对其调整至升序的状态
①:升序用小堆建好后,只有第一个数据已经排好,为最小的数据,但是剩下的数据是没有排好的,次小的数据也不一定就在下一位,所以需要重新建堆。
②:倘若将剩下的数据看做堆,再重新向下调整,但此时的二叉树不能被看成小堆了已经。如图:
因为向下调整的前提是:左右子树必须是个堆。
③:所以此时就需要重新建堆,选出次小的元素,然后重复①,②、③。
这里的时间复杂度为O(N^2)
- 升序建大堆时,如图:
- 建好大堆后,我们需要对其调整至升序状态
①:这里我们可以将最后一个叶子节点的值与堆顶的值进行交换,交换后如图:
②:然后将交换后的最后一个节点不看做堆里面的数据,然后向下调整,选出次大的数据到堆顶,之后再重复①、②
这里的时间复杂度为O(N*logN)
综合以上两种方法,得出升序建大堆时时间复杂度较小,所以升序建大堆,降序同理得建小堆。
2、建堆时算法的选择,是使用向上调整还是向下调整?
向上调整建堆时间复杂度分析
向上调整建堆是从堆顶开始依次向上建堆,我们以满二叉树来分析其时间复杂度
我们假设数的高度为h,分析每一层数据需要向上调整的次数
第1层,2^0个节点,需要向上调整0层
第2层,2^1个节点,需要向上调整1层
…
第h-1层,2^(h-2)个节点,需要向上调整h-2层
第h层,2^(h-1)个节点,需要向上调整h-1层
总的调整次数 = 每一层节点个数 ×这一层节点最坏向下调整的次数 N = 2^h-1
利用错位相减法算出其时间复杂度为 O(N*logN)
向下调整建堆时间复杂度分析
向下调整建堆是从最后一个节点的父节点开始依次向下建堆,我们以满二叉树来分析其时间复杂度
我们假设数的高度为h,分析每一层数据需要向下调整的次数
第1层,2^0个节点,需要向下调整h-1层
第2层,2^1个节点,需要向下调整h-2层
…
第h-1层,2^(h-2)个节点,需要向下调整1层
第h层,2^(h-1)个节点,不需要向下调整
所以总的调整次数 = 每一层节点个数 ×这一层节点最坏向下调整的次数
T(N) = 2^0×(h-1) + 2^1×(h-2) + …… + 2^(h-2)×1 N = 2^h-1
利用错位相减法可以计算出T(N) = N-log(N+1)
所以向下调整的时间复杂度为O(N)
很明显,向下调整的时间复杂度低于向上调整的时间复杂度,因此选择向下调整建堆
💜 堆排序实现
void UpAdjusting(HeapDataType* a, size_t child) { size_t parent = (child - 1) / 2; //这里的循环判断条件不能为 parent>=0(在取<时,会多走一次循环条件) 或者 parent>0(最后一次不会进入,即parent为0时) while (child > 0) { #ifdef SmallHeap if (a[child] < a[parent]) { //交换父子节点 Swap(&a[child], &a[parent]); //迭代 child = parent; parent = (child - 1) / 2; } else { break; } #endif //SmallHeap } void DownAdjusting(HeapDataType* a, size_t size,size_t parent) { size_t minchild = parent * 2 + 1; while (minchild < size) { //找出左右孩子最小的一个 if (minchild + 1 < size && a[minchild + 1] < a[minchild]) { minchild++; } //判断大小并交换 #ifdef SmallHeap if (a[minchild] < a[parent]) { //交换 Swap(&a[minchild], &a[parent]); //迭代 parent = minchild; minchild = parent * 2 + 1; } else { break; } #endif } void HeapSort(int* a,int size) { //i = size-1-1/2是找到最下面的第一个父节点,size-1是最后一个节点,找父节点是size-1-1 for (int i = (size-1-1) / 2; i >= 0; --i) { DownAdjusting(a, size, i); //传i是因为每一棵树都要重新建堆 } //第一个节点和最后一个节点交,然后再恢复小堆结构 int i = 1; while (i < size) { Swap(&a[0], &a[size - i]); DownAdjusting(a, size - i, 0); ++i; } }
💚 堆排序时间复杂度分析
堆排序的时间复杂度主要来自于两部分:
- 对数据进行建堆
- 对数据调整至有序状态
一、对数据进行建堆
for (int i = (size-1-1) / 2; i >= 0; --i) { DownAdjusting(a, size, i); //传i是因为每一棵树都要重新建堆 }
由上面的分析可以得出对数据建堆的时间复杂度为O(N)
二、对数据调整至有序状态
int i = 1; while (i < size) { Swap(&a[0], &a[size - i]); DownAdjusting(a, size - i, 0); ++i; }
while循环的时间复杂度为O(N),向下建堆的时间复杂度为O(logN)
所以步骤二的时间复杂度为O(N×logN)
总的时间复杂度为O(N+N×logN) = O(N×logN)