目录
3.1.3向上调整算法建堆和向下调整算法建堆的时间复杂度分析
1.堆的概念与结构
如果有个关键码的集合k={K0,K1,K2,K3……,Ki-1}把他的所有元素按照完全二叉树的顺序存储方式存储,在一个一维数组中并且满足Ki<=K(2*i+1)(Ki>=K(2*i+1)且Ki<=K(2*i+2)),i=0,1,2,3…则称为小堆(或大堆)。用简单的话来说,堆顶(根结点)一定是最值,如果堆顶为最小值则称为小根堆,反正为大根堆。即堆一定有父结点小于两个子结点则称之为小根堆,反之为大根堆。而堆只有小堆和大堆,而小根堆又称之为最小堆,大根堆又称之为最大堆。
堆有以下性质:堆中某一个结点的值总是不大于或不小于其父结点的值;堆总是一颗完全二叉树。
二叉树性质:对于有n个结点的完全二叉树,如果按照从上至下,从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点有(1)若i>0,位置结点的父结点序号为(i-1)/2;i=0,i为根结点 编号,无双亲结点;(2)若2*i+1<n,左孩子序号:2i+1,但是如果2i+1>=n,则下标超过了数组范围,这样则代表无左孩子;(3)若2*i+2<n,右孩子序号:2i+2,同理,如果2i+2>=n,这样就没有右孩子了。
2.堆的实现
堆我们可以用数组来实现,其结构和顺序表的结构差不多。所以定义比较简单就推出来
2.1堆的结构
//堆的结构
//能存储的数据有很多种
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size;//有效数据的个数
int capacity;//容量
}HP;
2.2堆的初始化
首先我们需要一个指针来作为实参,其次,我们需要判断传的指针是否为空(除向对中加入元素外都要判断),然后再进行数组置为NULL,size=capacity=0;,代码如下:
//堆的初始化
void HPInit(HP* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
//测试函数
void test()
{
HP hp;
HPInit(&hp);
}
int main()
{
test();
return 0;
}
如果调试发现如下:
则代码无误。
2.3堆的销毁
和顺序表销毁一样,先判断数组是否为NULL,如果不为NULL,则需释放内存空间,之后再把数组置为NULL,然后把size和capacity都置为0即可。代码如下:
//堆的销毁
void HPDesTory(HP* php)
{
//判断是否为NULL
if (php->arr)
{
free(php->arr);
}
php->arr = NULL;
php->size = php->capacity = 0;
}
//测试函数
void test()
{
HP hp;
HPInit(&hp);
HPDesTory(&hp);
}
若得到的结果为:
则代码无误。
2.4插入数据
首先我们需要判断内存空间是否足够,如果不够我们需要二倍扩容(之前博客说过原因),若没有容量,我们则置为4,并且我们需要先把capacity=newcapacity,后面再进行插入数据的操作,并且记得size++的操作。但是我们的堆只能为大根堆和小根堆,所以我们需要进行调整操作,所以我们需要另外一种算法来调整这个堆,我们称之为:向上调整算法
2.4.1向上调整算法
我们从堆的特点知道,如果我们只知道子结点的情况下我们能知道父结点,但是父结点所存储的数据只有我们比较了之后才知道需不需要去调整,所以我们需要先比较父结点的数据与新插入的数据,实现大根堆或小根堆。我们这次实现小根堆,所以我们要循环直至child==0,这样才没有父结点,才实现了我们的小根堆,注意:这里的child是下标,而不是结点。如果父结点大于子结点的情况,我们需要交换,这就需要我们的另外一种函数:交换函数了,这个函数比较简单,所以我也不做更多的解释了。
2.4.1.1交换函数
//交换函数
void Swap(HPDataType* x,HPDataType* y)
{
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
2.4.1.2向上调整算法的实现
//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{
HPDataType parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
//或者可以加个continue,之后把else去掉,只留break;
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
2.4.2插入数据的函数的实现
我们需要先进行判断内存够不够,然后再利用向上调整算法进行插入操作,代码如下:
//插入数据
void HPPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
//增容
HPDataType* tmp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * (newcapacity));
//增容失败
if (tmp == NULL)
{
perror("realloc fail!\n");
exit(1);
}
php->arr = tmp;
php->capacity = newcapacity;
}
php->arr[php->size] = x;
//第二个传的参数是php->size,因为下标这个时候最大已经是size,有size+1个数据,我们还没有执行++的操作
AdjustUp(php->arr, php->size);
++php->size;
}
//测试函数
void test()
{
HP hp;
HPInit(&hp);
HPPush(&hp, 4);
HPPush(&hp, 3);
HPPush(&hp, 2);
HPPush(&hp, 1);
HPDesTory(&hp);
}
结果为1 3 2 4则代码无误(我调试有点问题,但是代码没有问题,如果能调试正确的可能不是这个答案)。可以自己写一个打印数组元素的函数来进行模拟变化。
2.5删除堆顶数据
我们需要先进行判断数组是否为空,然后再进行交换堆顶(根结点)和下标为n-1位置的数据,然后进行size--的操作,我们这样之后需要再次调整为小根堆,而这样我们只知道根结点,由之前的知识知道其左孩子为2*parent+1,但是如果右孩子更小一些,我们则需要把child++,这就需要一个算法:向下调整算法。
2.5.1向下调整算法
我们需要有一个数组的参数,并且我们还需要知道数组的有效数据个数,否则我们无法判断是否已经越界了,如果child越界了即 child>=n则需要结束循环,但是我们需要先来一个判断数组是否为空的函数两者一起 ,有:
//判空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//向下调整算法
void AdjustDown(HPDataType* arr, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n )
{
//先找最小的
if ( arr[child] > arr[child + 1])
{
child++;
}
if (arr[parent] > arr[child])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
2.5.2删除栈顶数据的实现
我们先进行判空操作后把堆顶的元素和堆的最后一个元素交换,再进行size--的操作,最后进行使用向下调整算法的操作,有:
//删除栈顶数据
void HPpop(HP* php)
{
assert(!HPEmpty(php));
Swap(&php->arr[0], &php->arr[php->size - 1]);
--php->size;
AdjustDown(php->arr, 0, php->size);
}
//测试函数
void test()
{
HP hp;
HPInit(&hp);
HPPush(&hp, 4);
HPPush(&hp, 3);
HPPush(&hp, 2);
HPPush(&hp, 1);
HPpop(&hp);
HPpop(&hp);
HPpop(&hp);
HPDesTory(&hp);
}
调试发现我们最后剩的是2 。这是怎么回事呢?
2.5.3问题代码分析
我们调试发现第一个的时候1删除了,但是第二个的时候我们发现第二个换了位置后又换回来了,这是怎么回事?我们发现:child+1在当时已经越界了,并且我们还把2换回来了,所以问题就出现在这。我们应该改为child+1<n,所以我们改为:
//向下调整算法
void AdjustDown(HPDataType* arr, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n )
{
//先找最小的
if (child+1<n && arr[child] > arr[child + 1])
{
child++;
}
if (arr[parent] > arr[child])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
2.6取堆顶元素
这个很简单,我们直接写:
//取堆顶元素
HPDataType HPTop(HP* php)
{
assert(!HPEmpty(php));
return php->arr[0];
}
//测试函数
void test()
{
HP hp;
HPInit(&hp);
HPPush(&hp, 4);
HPPush(&hp, 3);
HPPush(&hp, 2);
HPPush(&hp, 1);
/*HPpop(&hp);
HPpop(&hp);
HPpop(&hp);*/
while (!HPEmpty(&hp))
{
printf("%d ", HPTop(&hp));
HPpop(&hp);
}
HPDesTory(&hp);
}
结果为1 2 3 4我们发现,如果我们不管怎么输入这些数,这个输出顺序好像就是排好的一样,所以我们可以把堆的思想应用到数组排序中,记住:是直接在数组上进行排序,而不是直接和建立堆来排序。
3.堆的应用
3.1数组用堆来排序
3.1.1建立小堆来实现数组的降序
我们如果需要实现升序,我们应该明白到底是小堆还是大堆来进行排序,我们看似用小堆来实现升序非常合理,但是我们如果用这种方法建立的就只能和取堆顶元素,又删除堆顶元素的方法来实现升序,这样很复杂,所以我们应该用其他角度来思考这个问题,假设一个数组里面元素为17 13 10 20 19 15,我们需要先进行小堆的排序,首先我们需要确定堆顶是最小值,则我们需要先进行向下排序算法来进行简单排序。我们可以从最后一个结点的父结点进行建堆,则child=n-1又parent=(child-1)/2则parent=(n-2)/2然后我们再把child--,再最后一个结点的父结点的兄弟结点上进行建堆,如果parent的数据大于child的数据,则我们应该需要交换,反之不需要交换,以此类推,这我们该使用的是向上调整算法还是向下调整算法呢?如果根结点是最大的一个,我们需要向下调整,我们是从下面开始建立的,所以我们需要向下调整算法来实现每一个的交换。所以有代码:
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(arr, i, n);
}
我们有点看不懂这串代码,因为这个东西确实不是很容易理解,我们可以把i看做parent,通过parent求出child来算出这个结果,如果向下调整算法你不懂为什么的话,我们可以去看下一个的向上调整算法,等我讲完之后大概你会理解一些了。
我们应当在堆上实现排序,由于是小堆,堆顶肯定是最小的元素,所以我们先进行堆顶与数组最后一个元素进行交换,再进行向下调整算法来把第二小的求出来,有:
//向下调整算法建堆
void HeapSort(HPDataType* arr, int n)
{
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(arr, i, n);
}
//堆排序
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);
end--;
}
}
为什么不是先end--再进行向下调整算法的思想呢?
因为end本来就是交换后的堆中的有效数据个数,开始为n-1,然后如果把n-1输入进去,在向下调整算法里当做size所以应当先进行向下调整操作再进行--操作。
3.1.2建立大堆来实现数组的升序
和降序一样,如果我们要实现数组的升序则需要使用大堆来实现,建立大堆我们需要先把向上调整算法的<改为>,即为:
//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{
HPDataType parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
//或者可以加个continue,之后把else去掉,只留break;
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
然后我们就该把数组里面的元素加入进堆,其中堆是从第一个结点开始向下进行建堆的(我们理解 的时候就认为向下调整向上建堆,反之向下建堆),有:
for (int i = 0; i < n; i++)
{
AdjustUp(arr, i);
}
后面的代码相同,则有:
//向上调整算法建堆
void HeapSort01(HPDataType* arr, int n)
{
for (int i = 0; i < n; i++)
{
AdjustUp(arr, i);
}
//堆排序
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);
end--;
}
}
3.1.3向上调整算法建堆和向下调整算法建堆的时间复杂度分析
我们知道,向上调整算法建堆我们在最后一行结点需要向上调整h-1次(最坏的情况),也就是说第h层需要调整h-1次,假设为满二叉树,最后一行有2^(h-1)个结点,所以我们需要调整(h-1)*2^(h-1),依次类推得到向上调整算法的时间复杂度为O(nlogn)(我们用等比数列求和和错位相减得到T(h),再由n=2^h-1得到T(n)最终约掉其他的常数求出)。向下调整算法建堆我们在第一行需要调整h-1次,有2^0个结点,最后一行则需要调整0次,我们相加后得到其结果为O(n)。所以我们知道了,如果在条件相同的情况下,我们应该用向下调整算法建堆最好。
3.2TOP-K问题
TOP-K问题:即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。
⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了
(可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决。如果我们有10亿个整数,我们需要求出前1000项,而我们的内存也只有4000个字节,则我们需要如何做?
思路:我们需要建立一个小堆来实现把第1000名放至堆顶,我们需要先把前1000个元素放入堆里面,然后把其中最小的元素放置在堆顶上,每一个添加进来的元素与堆顶进行比较,如果比堆顶大则需要用添加进来的元素把堆顶代替掉,后面又进行向下调整算法,把这次比较后最小的放置在堆顶,然后我们使用这个小堆来实现升序(由于我们每一次比较后都有调整所以堆顶为最小值),代码如下(有很多是关于文件的函数,所以我们需要注意回顾):
void CreateNDate()
{
//造数据
long n = 100000;
//生成随机数
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error!\n");
return;
}
for (long i = 0; i < n; i++)
{
long x = (rand() + i) % 1000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void TopK()
{
int k = 0;
printf("请输入K: ");
scanf("%d", &k);
//读取文件前K个数据建堆
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error!\n");
exit(1);
}
//找最大的前K个数,建小堆
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail!\n");
exit(2);
}
//读取文件前K个数据建堆
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &minHeap[i]);
}
//建堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(minHeap, i, k);
}
//变量剩下的n-k个数据,进行调整入堆
long x;
while (fscanf(fout, "%d", &x) != EOF)
{
if (x > minHeap[0])
{
minHeap[0] = x;
}
AdjustDown(minHeap, 0, k);
}
for (int i = 0; i < k; i++)
{
printf("%d ", minHeap[i]);
}
fclose(fout);
free(minHeap);
minHeap = NULL;
}
int main()
{
CreateNDate();
TopK();
return 0;
}
要记得包含头文件<stdlib.h><stdio.h>,我们还需要把向下调整算法里面的<改为>,最终结果如下(每一个电脑生成的数字不一样,但是结果为升序就可以了):
4.总结
堆这个数据结构实现起来比较难,日后需要不断去强化这方面的知识。喜欢的可以一键三连哦,下节讲解:二叉树的链式实现。
5.代码汇总
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <assert.h>
#include <ctype.h>
#include<stdbool.h>
#include<stddef.h>//NULL
//堆的结构
//能存储的数据有很多种
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size;//有效数据的个数
int capacity;//容量
}HP;
//堆的初始化
void HPInit(HP* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
//堆的销毁
void HPDesTory(HP* php)
{
//判断是否为NULL
if (php->arr)
{
free(php->arr);
}
php->arr = NULL;
php->size = php->capacity = 0;
}
//交换函数
void Swap(HPDataType* x,HPDataType* y)
{
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
向上调整算法
//void AdjustUp(HPDataType* arr, int child)
//{
// HPDataType parent = (child - 1) / 2;
// while (child > 0)
// {
// if (arr[child] < arr[parent])
// {
// Swap(&arr[child], &arr[parent]);
// //或者可以加个continue,之后把else去掉,只留break;
// child = parent;
// parent = (child - 1) / 2;
// }
// else
// {
// break;
// }
// }
//}
//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{
HPDataType parent = (child - 1) / 2;
while (child > 0)
{
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
//或者可以加个continue,之后把else去掉,只留break;
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//插入数据
void HPPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
//增容
HPDataType* tmp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * (newcapacity));
//增容失败
if (tmp == NULL)
{
perror("realloc fail!\n");
exit(1);
}
php->arr = tmp;
php->capacity = newcapacity;
}
php->arr[php->size] = x;
//第二个传的参数是php->size,因为下标这个时候最大已经是size,有size+1个数据,我们还没有执行++的操作
AdjustUp(php->arr, php->size);
++php->size;
}
//判空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//向下调整算法
void AdjustDown(HPDataType* arr, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n )
{
//先找最小的
if (child+1<n && arr[child] > arr[child + 1])
{
child++;
}
if (arr[parent] > arr[child])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
向下调整算法建堆
//void HeapSort(HPDataType* arr, int n)
//{
// for (int i = (n - 2) / 2; i >= 0; i--)
// {
// AdjustDown(arr, i, n);
// }
// //堆排序
// int end = n - 1;
// while (end > 0)
// {
// Swap(&arr[0], &arr[end]);
// AdjustDown(arr, 0, end);
// end--;
// }
//}
//删除栈顶数据
void HPpop(HP* php)
{
assert(!HPEmpty(php));
Swap(&php->arr[0], &php->arr[php->size - 1]);
--php->size;
AdjustDown(php->arr, 0, php->size);
}
//取堆顶元素
HPDataType HPTop(HP* php)
{
assert(!HPEmpty(php));
return php->arr[0];
}
//测试函数
void test()
{
HP hp;
HPInit(&hp);
HPPush(&hp, 4);
HPPush(&hp, 6);
HPPush(&hp, 8);
HPPush(&hp, 10);
for (int i = 0; i < 4; i++)
{
printf("%d ",hp.arr[i]);
}
/*HPpop(&hp);
HPpop(&hp);
HPpop(&hp);*/
while (!HPEmpty(&hp))
{
printf("%d ", HPTop(&hp));
HPpop(&hp);
}
HPDesTory(&hp);
}
//向下调整算法建堆
void HeapSort(HPDataType* arr, int n)
{
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(arr, i, n);
}
//堆排序
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);
end--;
}
}
//向上调整算法建堆
void HeapSort01(HPDataType* arr, int n)
{
for (int i = 0; i < n; i++)
{
AdjustUp(arr, i);
}
//堆排序
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);
end--;
}
}
//int main()
//{
// //test();
// int arr[] = { 17,20,10,13,19,15 };
// int n = sizeof(arr) / sizeof(arr[0]);
// HeapSort01(arr, n);
// for (int i = 0; i < n; i++)
// {
// printf("%d ", arr[i]);
// }
// return 0;
//}
//向下调整算法
//void AdjustDown(HPDataType* arr, int parent, int n)
//{
// int child = parent * 2 + 1;
// while (child < n)
// {
// //先找最小的
// if (child + 1 < n && arr[child] > arr[child + 1])
// {
// child++;
// }
// if (arr[parent] > arr[child])
// {
// Swap(&arr[child], &arr[parent]);
// parent = child;
// child = parent * 2 + 1;
// }
// else
// {
// break;
// }
// }
//}
void CreateNDate()
{
//造数据
long n = 100000;
//生成随机数
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error!\n");
return;
}
for (long i = 0; i < n; i++)
{
long x = (rand() + i) % 1000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void TopK()
{
int k = 0;
printf("请输入K: ");
scanf("%d", &k);
//读取文件前K个数据建堆
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error!\n");
exit(1);
}
//找最大的前K个数,建小堆
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail!\n");
exit(2);
}
//读取文件前K个数据建堆
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &minHeap[i]);
}
//建堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(minHeap, i, k);
}
//变量剩下的n-k个数据,进行调整入堆
long x;
while (fscanf(fout, "%d", &x) != EOF)
{
if (x > minHeap[0])
{
minHeap[0] = x;
}
AdjustDown(minHeap, 0, k);
}
for (int i = 0; i < k; i++)
{
printf("%d ", minHeap[i]);
}
fclose(fout);
free(minHeap);
minHeap = NULL;
}
int main()
{
CreateNDate();
TopK();
return 0;
}