前言
对堆排序进行一下升级,我们之前使用堆需要先实现一下整个堆的函数而且还要将数据导入到堆结构单独开辟的空间内,非常没必要所以我们为什么不直接将我们的原数组内的元素直接当做堆来进行堆排序的所需呢?
说搞就搞
堆的应用
简易实现堆
之前我们通过实现堆的代码然后用堆代码来实现我们的堆排序,这样的堆排序的
空间复杂度O(N)
时间复杂度为O(N)
代码如下:
void HeapSort(int* a, int size) { Heap hp; HeapCreat(&hp); for (int i = 0; i < size; ++i) { HeapPush(&hp, a[i]); } size_t j = 0; while (!HeapEmpty(&hp)) { a[j] = HeapTop(&hp); j++; HeapPop(&hp); } HeapDestory(&hp); }
而且我们想使用这样的堆排序甚至要专门搞个堆来实现.非常痛苦.
其实我们完全可以通过稍微更改一下向上和向下调整函数来实现堆排序
调整后的函数我通过注释的方式进行讲解吧=-=.毕竟之前就讲过
void Swap(HPDataType* a, HPDataType* b) { HPDataType tmp = *a; *a = *b; *b = tmp; } void AdjustDown(HPDataType* a, size_t size, size_t root)//root就是我们要调整的父亲节点. { size_t parent = root; size_t child = parent * 2 + 1;//公式得到的child是parent的左孩子 while (child < size) { // 1、根据我们需要的情况去调整我们的大于小于号 if (child + 1 < size && a[child + 1] < a[child]) { ++child; } // 2、孩子与父亲比较为真则交换,并继续往下调整 if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } void AdjustUp(HPDataType* a, size_t child) { size_t parent = (child - 1) / 2;//公式无论是左孩子还是右孩子都可以得到我们的parent while (child > 0) { if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } }
我们得到调整后的函数后我们就可以将传来的数组调整成堆形式然后通过一个手段(会讲的啦)来得到我们想要的排序结果.
我们主要讲一下我们用向下调整和向上调整形成堆的时间复杂度.
堆的排序我们要使用向下调整的方式来实现.
首先我们先得到我们的堆可以通过向上调整和向下调整两种方式得到.
首先我们先将一下向上调整的方式的代码和时间复杂度讲一下.
for (int i = 1; i < n; i++) { AdjustUp(a, i); }
向上调整的函数直接将所有的元素都检查一下并将所有位置都进行调整,下面这种的调整也许大家更容易理解
for (int i = n; i >0; i--) { AdjustUp(a, i); }
我们来讲一下他的时间复杂度.
假设我们每次的AdjustUp都是最坏的情况及从尾一直走到头,及执行了他树高度的次数,我们知道他的N值通过公式的h = log2(N+1)(以2为底的log).
再因为for循环每次循环次数从n~1而每层节点数如下.
我们以满二叉树为例,因为我们的cpu的运算能力就算少哪几个节点或多几个节点对时间影响不大.
所以我们可以列出公式如下:
这就是很经典的等比数列乘以等差数列了,我们直接用错位相减的方式可得:
然后又有N= 2h -1
所以我们的时间复杂度就为O(N*logN).
向下调整的函数的时间复杂度我们也稍微讲一下.
先看看我们使用的方式吧.
for (int i = (n - 1 - 1) / 2; i >= 0; --i) { AdjustDown(a, n, i); //void AdjustDown(HPDataType* a, size_t size, size_t root)//root就是我们要调整的父亲节点. }
相信大家注意到了我们这个int i = (n-1-1)/2这个怪怪的表达式了.
我们这个是需要从最后一个非叶节点开始调整的,n的第一次-1是为了找到最后一个节点,再-1是因为我们要找父节点的公式所以可以解读长((n-1)-1)/2,其中的n-1就是为了得到最后一个节点的下标.
既然读懂了这个步骤的操作我们就来看看他的时间复杂度吧.
类似于我们的向上调整这个时间的复杂度我们也用满二叉树来讲解.
我们的向下调整建堆就比向上调整建堆减少了一层.
所以根据最坏次数得到的时间复杂度如下公式
假设有n个元素h层
依旧是错位相减即可(高中知识啊).
最后得
所以时间复杂度为O(N)
堆排序
OK.现在我们所有的方式的建堆都得到了.
现在我们想用这些堆来实现堆排序我们需要做什么呢.
这时候我们要通过向下调整来得到我们的排序结果.
使用向下调整的原因也很简单.因为向上调整函数会破坏我们已经创建好的堆结构.而向下调整不会.
不过我们不能普普通通的使用AdjustDown函数我们需要更改结尾位置
int end = n - 1; while (end > 0) { Swap(&a[0], &a[end]); AdjustDown(a, end, 0); end--; }
为什么我们要end–呢?
其实很简单我们要通过更改end的位置将根节点(a[0])传输到从数值的倒数第一的位置再到倒数第二的位置…这样我们的排序就完成了.🤪
TOPK问题
TOPK问题分为两种,
第一种总体N和K的数值差距不大 如: 从50个人里找到前10.
第二种总体N和K的数值相差巨大 如:从10亿个人里找到前十
第一种我们直接通过排序或者建堆然后用HeapPop函数来得到我们的值
不过如果是第二种情况呢?
总不能建立一个10亿的堆吧.我们这10亿个数据都存储在文件中的时候我们如何得到前K的数据呢?
其实我们可以建一个只有K个数值的小堆,然后我们遍历那个10亿的数据发现比小堆根节点根节点大的就将其进行调换并重新将堆格式进行调整.
这样我们就得到了K个最大的值,如果还想进行排序就简单多了.
结尾
下一篇我的计划是将分治思想.也不知道会有多大的篇幅.🤪