一、前言
①前言
在上一篇博客里,我们所写的堆排序的问题在于,需要额外占用O(n)的空间,并且需要预先写很多堆的函数接口,显示这不是一个理想的堆排序。在这篇博客里,我将向大家展现堆排序真正的C语言实现方式。
②阅读须知
本篇博客基于【玩转二叉树②】,相应接口直接使用而不再本文赘述。所以请大家一定先参阅前一篇博客。
二、堆的创建
1.向上调整算法
①O(1)的空间复杂度
既然要将空间复杂度降为O(1),那我们就不能额外申请堆空间,而是需要在原数组上建堆。下面向大家展现两种建堆(向上调整,向下调整)的方式:
②向上调整算法介绍
其基本思想就是「❉」HeapShiftup()函数。我们要做的就是遍历数组,将数组中的每一个元素“压入堆”中,这个压入的过程实际上是对堆的【逻辑结构】的维护。由于数组的空间一定是足够的,所以不需要考虑扩容问题。
③向上调整动图图解
【过程分析】
- 以数组[1, 2, 3]为例进行分析
- 首先只有1个元素1,位于堆顶
- 压入元素2,元素2通过上浮操作到底应该到的位置(元素3同理)
④源码解析
【重难点分析】
- 我们从数组的第二个元素开始上浮插入,所以i从1开始
- 因为堆属于一种完全二叉树,所以不用担心后面的数据被覆盖,只有当前数据和前面的元素之间才会放生交换
⑤时间复杂度分析:
计算时间复杂度时我们考虑最坏的情况:「❉」即上浮到根节点.
对每一层进行相加求和,利用高中所学的「❉」错位相减法 ,可以求出和为
2.向下调整算法
①向下调整算法介绍
向下调整算法的基本思想是「❉」HeapShiftdown()函数。向下调整算法有一个基本前提:左右子树必须是一个堆。
以下图为例进行说明,由于结点27的左右子树都是堆,所以左孩子和右孩子分别是左树和右树的最小值:
- 27与15, 19比较,15是最小值 ∴27与15交换
- 27与18, 28比较,18是最小值 ∴27与18交换
- 27与40, 25比较,25是最小值 ∴27与25交换
完成上述交换后,所有的结点都满足了堆的逻辑结构。构造成功
②源码解析
【重难点剖析】
由于叶节点没有子节点,所以单个元素自身可以看成是大根堆,也可以看成是小根堆。因此我们从叶节点的上一层开始遍历。
最后一个叶节点的父节点:(arrSize - 1) - 1 >> 1;【叶节点与父节点的下标关系】
我们从最后一个结点的叶节点开始进行「❉」HeapShiftdown() 操作
下标减减到达前一个结点(见上图金茶色箭头),由此对每一个结点都进行下沉操作,最终构造出堆
③时间复杂度分析
同样考虑最坏的情况,照下图进行推导
【总结】
我们可以看到向下构造的时间复杂度为O(N),低于向上构造的时间复杂符O(N*logN)。所以我们选用向下构造法构造我们的堆。
三、堆排序的实现
①如何利用堆进行排序?
【问】如果我们想实现升序,可以创建小根堆吗?
【答】不可以。虽然堆顶元素就是最小值,因为我们是在原数组操作,所以不能用HeapPop函数,所以无法再继续排序。
②结论
- 实现升序需要创建大根堆
- 实现降序需要小根堆
③源码剖析
【重难点剖析】
- 我们不能从堆顶进行操作,只能弹出堆尾的元素,这样才不会影响之前的逻辑结构
- 对于大根堆,堆顶元素就是最大值,与堆尾元素进行交换,再将sz减减
- 对堆顶元素HeapShiftDown,维护堆的逻辑结构
- 重复上述过程,实现对数组的升序排列
④堆排序的本质
堆排序的本质上其实是选择排序,先选取最值放在末尾,再选取次最值放在倒数第二的位置,以此类推。
对于传统选择排序来说,选出当前元素中最值的时间复杂度为O(N),所以选择排序总的时间复杂度为O(N^2)。
对于堆排序来说,选出当前元素中的最值所需要的时间为O(logn),时间花在ShiftupDown函数对堆结构的维护。所以堆排序总的时间复杂度为O(N*logn)。
四、TOP-K问题
①问题描述
求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
②解决办法①:排序
问题在于数据量很大比如几百亿的时候,内存存储不下这么大的数据量,所以排序的方法不可行
③解决办法②:构建大小为K的堆
我们每次只需从磁盘中读取一个元素,并继续维护我们的堆,这样最终就可以得出TOP-K。利用堆这种数据结构可以极大的节省内存空间。