基本概念:
堆也就是一棵完全二叉树
堆的分类:
大堆:所有的父节点都大于子节点
小堆:所有的父节点都小于子节点
堆的逻辑结构(便于理解想象出来的):
堆的实际存储方式:
本文主要讲解堆在数组中的存储。
堆在数组的存储方式下父亲节点和子节点的关系:
左孩子节点的数组下标一定是奇数,右子节点的数组下标一定是偶数
求子节点
leftchild = parents*2+1;
rightchild = parents*(2+1);
求父节点(因为再c/c++中"/"代表整除,再加上右节点是偶数,因此两个节点的下标减一除二都是父节点的下标)
parent = (child - 1)/ 2
代码实现:
堆的结构体的定义:
typedef int HeapDataType;//以便修改堆的存储数据的类型,防止麻烦的修改
typedef struct Heap
{
HeapDataType* a;
int size;//记录当前存储数据的个数
int capacity;//记录空间的容量,以便扩容
}HP;
堆的初始化:
void HeapInit(HP* php)
{
assert(php);
php->size = 0;
php->a = NULL;//在push中再申请空间
php->capacity = 0;
}
堆的销毁:
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->size = php->capacity = 0;
}
交换数据:
参数传址调用来修改内容(形参只是原来数据的拷贝)
//写成函数是为了方便后面的复用
void Swap(HeapDataType* p1, HeapDataType* p2)
{
HeapDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
堆的插入数据(这里创建大堆):
插入的思路:
先将数据尾插到指针当中,再调整整个数组,使其的逻辑结构成为一个堆
如何调整数组?
确保插入数据前的整个数组都是一个堆的前提下,每次插入数据时,通过向上调整(将插入的数据不断和其父节点相比较,如果大于其父节点,那么交换两个数据,直到整个数组都是一个堆)
实现向上调整:
void AdjustUp(HeapDataType* a, int child)
{
//时间复杂度O(logN)
int parent = (child - 1) / 2;
//最好不用parent >= 0,因为如果child = 0时,parent仍然会等于0
while (child > 0)
{
//创建大堆
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else break;
}
}
代码实现:
void HeapPush(HP* php, HeapDataType x)
{
assert(php);
//插入数据
//查看容量,看是否需要扩容
if (php->capacity == php->size)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;//选择表达式
HeapDataType* tmp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
//调整堆
AdjustUp(php->a, php->size -1);
}
堆的删除堆顶数据:
删除数据的思路:
1.直接整体挪动数据来覆盖掉堆顶的数据,在进行调整?
不可以,首先不断移动数据效率过低.其次是整体挪动会打乱原来堆的整体结构(堆顶待删除节点的左子树和右子树全部被打乱)
2.将堆顶数据和最后一个数据进行交换,然后删除最后一个数据,这样维持了根节点的左右子树的结构,然后进行调整即可
实现向下调整:
这里由于在向下调整时需要判断左右子节点的大小,再与较大的子节点进行交换.
这里用假设法,先假设左节点为较大的节点,再进行比较,如果不是的话,就选择右节点进行判断是否需要交换
void AdjustDown(HeapDataType* a, int size, int parent)
{
//假设左子节点大
int child = parent * 2 + 1;
while (child < size)
{
//判断左右孩子的大小,取大的
if (a[child] < a[child + 1])
{
child++;
}
//孩子大于父亲,那么交换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
}
parent = child;
child = parent * 2 + 1;
}
}
代码实现:
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
//交换第一个数据和最后一个数据
Swap(&php->a[0], &php->a[php->size - 1]);
//无所谓是否删除最后一个数据,将存储数据个数的size-1即可
php->size--;
//向下调整
AdjustDown(php->a,php->size,0);
}
堆的创建:
向上建堆法:
将每个数据利用上面讲过的插入数据的方式插入到堆的最后,然后通过向上调整的方法来完成建堆
void HeapCreate(HP* php, HeapDataType* a, int n)
{
assert(php);
//创建空间
HeapDataType* tmp = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
php->a = tmp;
//转移数据
memcpy(php->a, a, sizeof(HeapDataType) * n);
php->size = php->capacity = n;
//时间复杂度(O(N*logN))
//向上建堆
for (int i = 1;i < n;i++)
{
AdjustUp(php->a, i);
}
}
时间复杂度的计算:
假设数组中一共有n个数据,行数为h,第m行的数据个数为个。那么:
最坏的打算,每一行的每个数据都需要调整。
第一层,不调整
第二层,调整一次
......
第h-1层,调整h-2次
第h层,调整h-1次
通过错位相减法计算再带入h和n的关系化简再利用大O渐进表示法可得可得:
向下建堆法:
向下调整法:
如果二叉树的某个节点的两个子树都是满足大堆(假设是建大堆),那么该节点可以和两个子节点中较大的节点进行比较,如果小于则交换,循环直至建成一个堆,这种方法叫做向下调整法。
代码实现:
void AdjustDown(HeapDataType* a, int size, int parent)
{
//假设左子节点大
int child = parent * 2 + 1;
while (child < size)
{
//判断左右孩子的大小并且右孩子必须在数组的有效范围内,取大的
if (a[child] < a[child + 1] && child +1 <size)
{
child++;
}
//孩子大于父亲,那么交换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
}
parent = child;
child = parent * 2 + 1;
}
}
思路分析:
因为向下调整的前提是两个子树都已经是堆了,那么调整需要从最后一个节点的父节点开始,向前面的节点依次向下调整,即可完成向下建堆。
代码实现:
void HeapCreate(HP* php, HeapDataType* a, int n)
{
assert(php);
HeapDataType* tmp = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
php->a = tmp;
//将数组的数据拷贝过去
memcpy(php->a, a, sizeof(HeapDataType) * n);
php->size = php->capacity = n;
//向下调整建堆
//计算时间复杂度(O(N))
for (int i = (n - 1 - 1) / 2;i >= 0;i--)
{
AdjustDown(php->a,n,i);
}
}
时间复杂度计算:
假设数组中一共有n个数据,行数为h,第m行的数据个数为个。那么:
最坏的打算,每一行的每个数据都需要调整。
第一层,调整h-1次
第二层,调整h-2次
......
第h-1层,调整1次
第h层,调整0次
通过错位相减法的计算和带入h和n的关系化简可得:,再利用大O渐进表示法可得:。
向上调整建堆和向下调整建堆的区别:
向上调整法下面的节点调整次数多,向下调整法上面节点调整次数多,而二叉树的结构又是上面节点少,下面节点多,因此更推荐使用向下调整法来进行建堆
堆排序:
首先先要进行建堆,那么问题来了:如果要排出升序的序列的话,应该用大堆还是小堆呢?
我们大多数人可能会想,应该排出小堆,因为小堆上面的数据是最小的,然后Pop掉第一个数据,不断循环,可这样话Pop掉第一个数据之后会打乱所有的堆的结构,并且时间复杂度太高。
因此我们可以参考前面Pop数据的方式,建出大堆,然后将堆顶也就是最大的数据和最后一个数据交换,然后堆内的数据减1,这样的话根节点的两个子树都仍是一个大堆,然后选择向下调整来建堆,重复进行,最终完成排序
代码实现:
void HeapSort(HeapDataType* a, int n)
{
//对该数组进行向下建堆
for (int i = (n - 1 - 1) / 2;i >= 0;i--)
{
AdjustDown(a, n, i);
}
//记录需要交换的次数,n个数据交换n-1次,此时
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
//交换完成之后,end也就刚好成了新的堆的数据的个数(即原来堆顶的数据放到最后一个位置了,然后认为堆的数据少1)
AdjustDown(a, end, 0);
end--;
}
}
TopK问题:
TopK问题即是选出数据中的最大或者最小的k个数据(如世界财富榜前十位,国服前十李元芳等等)
这里以计算最大的数据来举例
方法一:
当数据较少,可以直接输入数据时,可以通过向下建堆法来将这些数据进行建大堆,然后取堆顶的数据Pop掉然后再向下调整这样不断循环,就可以得到前k个数据
时间复杂度:
建堆:
Pop前k个数据(循环k次,每次将堆顶的数据和最后一个数据交换后然后再向下调整):
总体为:
方法二:
当数据非常多时,采用上面的方法会导致时间复杂度过高,效率太低
因此我们选择别的方法:
首先,将前k个数字进行建立一个小堆,这样堆顶的数据在堆里是最下的,然后依次遍历剩余的每个数据,如果大于堆顶的数据,那么就替换它入堆,然后再向下调整,得到一个新的小堆。最后得到的就是最大的k个数
时间复杂度:
前k个数据建堆:
N-K次循环然后入堆向下调整(最坏的结果,数据本来为升序,每次遍历的数据都需要入堆,每次向下调整的复杂度为):
代码实现:
这里为了模拟大量的数据,首先在一个文件中写入了大量的随机数
void HeapTest5()
{
//手动输入所要总体数据的个数n和选择前k个数据
printf("请输入n和k::");
int k, n;
scanf("%d%d", &n, &k);
//动态申请空间
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail");
exit(-1);
}
//后面要利用函数rand()来获得随机数,但rand()获得的随机数在重复运行时是固定的,而函数srand()中输入的种子数可以影响rand()获得的随机数,因此为了模拟随机性,在每次运行时要在srand()中输入不同的种子数,而函数time(0)可以在每次运行时获得当前的时间戳,这样就保证了rand()获得数据的随机性
srand(time(0));
//在文件中写入n个数据
FILE* fin = fopen("data.txt", "w");
if (fin == NULL)
{
perror("fin fail");
exit(-1);
}
for (int i = 0;i < n;i++ )
{
int val = rand();
fprintf(fin, "%d ", val);
}
//关闭文件
fclose(fin);
//在文件中读取n个数据
FILE* fout = fopen("data.txt", "r");
if (fout == NULL)
{
perror("fout fail");
exit(-1);
}
//读取数据
for(int i = 0;i<k;i++)
{
fscanf(fout, "%d", &minHeap[i]);
}
//建小堆
for (int i = (k - 1 - 1) / 2;i >= 0;i--)
{
AdjustDown(minHeap, k, i);
}
//继续读取数据
int val;
while (fscanf(fout, "%d", &val) != EOF)
{
//如果读取的数据大于堆顶的数据,就入堆
if (val > minHeap[0])
{
minHeap[0] = val;
AdjustDown(minHeap, k, 0);
}
}
//打印所得到topK个数据
for (int i = 0;i < k;i++)
{
printf("%d ", minHeap[i]);
}
fclose(fout);
free(minHeap);
}