数据结构初阶--堆排序+TOPK问题
堆排序
堆排序的前提
堆排序:是指利用堆这种数据结构所设计的一种排序算法。堆排序通过建大堆或者小堆来进行排序的算法。
举个例子:给定我们一个数组{2, 3,4, 2,4,7},我们可把这个数组在逻辑上看成是一种堆的结构,然后进行建堆,建大堆(或建小堆)我们就可以在堆顶选出一个最大(最小)的数,通过不断的选数,我们就可以把顺序弄出来了。
如何建堆?在上一篇博客中我已经跟大家说过了,就是这样的:
堆的构建有两种方法:
第一种:从第二个节点往后开始向上调整
第二种:从最后一个非叶子节点开始向下调整
第一种:从第二个叶子节点开始向上调整,把前面两个节点构成的堆建成大堆(小堆),如何依次调整第三个节点,第四个节点……直到调整最后一个,与堆的插入有些相似,只不过我们原来是有一组数,用一个动图给大家演示一下:
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093348067-582161060.gif)
代码实现如下:
int i = 0;
//建小堆 排降序 建大堆 排升序
for (i = 1; i < n; i++)
{
//建大堆 向下调整
AdjustUp(i);
}
第二种:从最后一个非叶子节点开始向下调整,从下往上,先把下面的子树建成大堆(小堆),最后就是堆顶向下调整了,看一下动图演示:
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093408011-1882222596.gif)
代码实现如下:
//找到最后一个父亲节点
int parent = (n - 2) / 2;
int i = 0;
//建小堆 排降序 建大堆 排升序
for (i = parent; i >= 0; i--)
{
//建大堆 向下调整
AdjustDown(n, i);
}
堆排序的思想
如果把待排序序列分为未排序区间和有序区间,堆排序大的思想是每次选一个数放到有序区间,没经历一个循环有序区间就会加一,无序区间减一,循环结束序列也就有序了,像这样:
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093421603-1890973714.png)
可以发现堆排序的思路和选择排序很像,没错,思路确实一样,只不过选择排序每次要遍历无序区间去找当前无序区间的最大值(升序找最大值,降序找最小值),而堆排序呢是把无序区间看做一个堆,堆顶自然是这个堆的最值了,每次循环只需要将堆顶元素取出来和无序区间最后一个数交换以达到有序区间加一的目的,然后在对这个堆(注意此时堆的size减一)向下调整,这样做之后下次循环继续取堆顶元素和无序区间最后一个元素交换然后继续循环,直到无序区间就剩一个元素,此时整个序列就有序了。
对于堆排序在实现的时候要知道:
- 待排序序列需要升序排列那么要建大堆
- 待排序序列需要降序排列那么要建小堆
为什么要有上面的两条规定呢?要升序排列就不能建小堆,要降序排列就不能建大堆吗?
答案是可以,但是不推荐,请看下图:
![2](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093436621-1630344867.png)
相反如果要通过建小堆完成让数组升序排列的话,因为小堆堆顶元素是最小值,而堆顶这个位置也是排序之后最小值的位置就是说堆顶元素不用在移动了,那么我们的堆要从后面一位重新建立,在建立一个小堆,找出最小值,再往后面一位找出最小值直到整个数组成升序排列,乍一看好像没有问题,但是和建大堆不同的是建小堆每次都要从下一位重新建堆才能选出最值,这个操作的复杂度为O(nlogn),要比建大堆每次只用将堆顶元素向下调整的时间复杂度为O(logn)慢很多,所以虽然可以升序建小堆但是因为相对没有建大堆速度快所以我们选择建大堆。
对应的降序建小堆也是同理。
总结:
- 待排序序列需要升序排列那么要建大堆
- 待排序序列需要降序排列那么要建小堆
堆排序的基本步骤以及代码
步骤一 :构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
a.假定给定的无序序列结构如下,将通过方法二从最后一个非叶子节点开始向下调整,将该堆变成一个大堆
![3](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093536098-564120048.png)
b.我们对整个无序序列进行调整,将其建立成大堆的形式,此时我们从最后一个非叶子结点开始进行调整,找到第一个非叶子结点8,比较它的左右子节点,找出左右子节点的最大值,与父结点进行比较,如果比父节点大就交换,得到调整后的结构
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093605457-1753618331.png)
c.找到第二个非叶子结点16,由于它的左右子结点25和18中25的元素大,所以将16和25进行交换,得到如下序列,此时向下调整还没有结束,以16为目标结点,比较其与左右子节点的大小关系,发现已经成堆,结束调整(切记向下调整还没有完毕,需要以交换的结点为目标继续查看是否需要继续向下调整)
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093618042-1269488620.png)
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素25和末尾元素8进行交换,此时25为有序序列,前面为无序
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093634906-439821240.png)
b.重新调整结构,使其继续满足堆定义,以16为第一个非叶子结点进行向下调整,紧接着以8为第二个非叶子结点进行调整,将18和8互换,此时18是左右孩子最大的值,不需要再向下调整
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093647820-830855282.png)
c.将堆顶元素18和末尾元素15进行交换,此时18,25为有序序列,前面为无序
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093700992-1342220835.png)
d.重新调整结构,使其继续满足堆定义,以15为第一个非叶子结点进行向下调整,将16和15进行交换
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093716991-1147603162.png)
e.将堆顶元素16和末尾元素8进行交换,此时16,18,25为有序序列,前面为无序
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093730090-610591665.png)
d.重新调整结构,使其继续满足堆定义,以8为第一个非叶子结点进行调整,将15和8互换,调整完毕之后,交换15和8的值,得到最终的有序序列,堆排序过程结束
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093744170-1949676065.png)
再简单总结下堆排序的基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
堆排序的代码实现
typedef int HPDataType;
//小堆的实现
typedef struct Heap
{
HPDataType* a;
int capacity;
int size;
}HP;
void HeapSort(HPDataType* a, int n)
{
//找到最后一个父亲节点
int parent = (n - 1 - 1) / 2;
int i = 0;
//建小堆 排降序 建大堆 排升序
for (i = parent; i >= 0; i--)
{
//建大堆 向下调整
AdjustDown(a, n, i);
}
i = n - 1;
while (i >= 0)
{
Swap(&a[0], &a[i]);
AdjustDown(a, i, 0);
i--;
}
}
堆排序时间复杂度分析
这里时间复杂度分析分为两部分——建堆和n次向下调整
建堆:时间复杂度是O(n)
n次向下调整:向下调整一次是O(logn),n次就是O(n*logn)
n*logn+n≈n*logn
综上,堆排序时间复杂的是O(n*logn)
TOPK问题
TOPK问题的概念
TOPK问题:找出N个数里面最大/最小的前K个问题。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
TOPK问题实现的原理
原理:TOPK问题采用堆来实现,用一个大小为K的小堆,然后往堆顶插入数据,如果当前的数比堆顶的数大就把堆顶的数换下来并进行向下调整,否则就不做处理。
如果要选取n个数中最大/最小的前k个值,步骤如下:
1.先用前k个数建成k个数的小堆。
2.剩下n-k个数,依次跟堆顶的数据进行比较,如果比堆顶的数据大,就进堆进行向下调整。
3.最后堆里的k个数就是最大的k个数。
举个例子:10选5
![](https://img2023.cnblogs.com/blog/2976263/202211/2976263-20221129093800683-1247774298.gif)
TOPK问题代码实现
核心代码
void PrintTopK(int* a, int n, int k)
{
assert(a);
HP hp;
HeapInit(&hp);
// 1. 建堆--用a中前k个元素建堆
for (int i = 0; i < k; ++i)
{
HeapPush(&hp, a[i]);
}
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
for (int i = k; i < n; i++)
{
if (a[i] > hp.a[0])
{
hp.a[0] = a[i];
AdjustDown(hp.a, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", hp.a[i]);
}
}
完整代码以及测试
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int capacity;
int size;
}HP;
void Swap(int* p1, int* p2)
{
int tmp;
tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
assert(a);
int parent = (child - 1) / 2;
while (child >= 0)
{
if (a[child] < a[parent])//< 建小堆 > 建大堆
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HeapPush(HP* hp, HPDataType x)
{
assert(hp);
if (hp->capacity == hp->size)
{
int newCapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;
HPDataType* tmp = (HPDataType*)realloc(hp->a, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
hp->a = tmp;
hp->capacity = newCapacity;
}
hp->size++;
hp->a[hp->size - 1] = x;
//向上调整
AdjustUp(hp->a, hp->size - 1);
}
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
//选小孩子
if (child + 1 < n && a[child + 1] < a[child])//< 建小堆 > 建大堆
{
child++;
}
if (a[child] < a[parent])//< 建小堆 > 建大堆
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void PrintTopK(int* a, int n, int k)
{
assert(a);
HP hp;
HeapInit(&hp);
// 1. 建堆--用a中前k个元素建堆
for (int i = 0; i < k; ++i)
{
HeapPush(&hp, a[i]);
}
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
for (int i = k; i < n; i++)
{
if (a[i] > hp.a[0])
{
hp.a[0] = a[i];
AdjustDown(hp.a, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", hp.a[i]);
}
}
void TestTopk()
{
int n = 10000;
int* a = (int*)malloc(sizeof(int) * n);
srand(time(0));
for (int i = 0; i < n; ++i)
{
a[i] = rand() % 1000000;
}
a[5] = 1000000 + 1;
a[1231] = 1000000 + 2;
a[531] = 1000000 + 3;
a[5121] = 1000000 + 4;
a[115] = 1000000 + 5;
a[2335] = 1000000 + 6;
a[9999] = 1000000 + 7;
a[76] = 1000000 + 8;
a[423] = 1000000 + 9;
a[3144] = 1000000 + 10;
PrintTopK(a, n, 10);
}
int main()
{
TestTopk();
return 0;
}