上一篇文章我们讲到了二叉树的顺序存储,我们提到它的顺序存储只适用于完全二叉树,那么我们在二叉树的基础上加一些限制条件(某个节点的值总是不大于或不小于其父节点的值)就可以变成堆(一种完全二叉树)。
目录
堆的概念
树中所有的父亲都小于或等于孩子称为小根堆;所有父亲都大于或等于孩子称为大根堆。
了解了大小根堆的概念我们思考,如何插入数据保证它依然是原根堆的结构呢?
我们以大根堆为例,假如我下一个要插入20,这是最简单的情况,因为此时插入只需要在数组的尾部插入即可,不会破坏大根堆的结构,但如果我继续插入60呢,这时我们就需要在尾部插入之后将60的位置进行向上调整,这里就用到了我们上一篇文章讲到的父亲和孩子之间的关系: parent = (child-1)/2,找到父亲的位置,对父亲和儿子对应的值进行比较,60大于25,交换二者位置,然后继续向上比较,60大于56,继续交换,最后跟70比较,60小于70,不进行调整,调整结束。
由此我们可以看出,插入一个数据最多需要调整的次数就是它的高度次,即log(N+1)
到这我们就可以进行代码实现一下:
向上调整函数
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType x = *p1;
*p1 = *p2;
*p2 = x;
}
void AdjustUp(HPDataType* a,int child)
{
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;
}
}
}
为了方便测试看结果,我们写一个基本的堆结构,包括初始化函数
结构
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
初始化函数
void HeapInit(HP* php)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
php->size = 0;
php->capacity = 4;
}
销毁函数
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
插入数据函数
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * php->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
到这我们就测试可以看看是否符合我们想要的大根堆结构
可以看出确实是我们想要的大堆,那么我们继续完善
删除函数
接下来我们要写删除函数,首先需要知道我们堆是要删堆顶的数据,也就是根
那么我们接下来要思考的就是如何删除根,因为删除根后依然要保持原来的大堆结构
注意这里切记不可以以挪动数据的方式进行删除,因为会面临两个问题,第一:挪动数据,复杂度为O(N),效率低下;第二:挪动数据之后,剩下的数据不构成堆,关系打乱,原来的兄弟关系变成了父子关系
所以,我们这里采用一种神奇的方式就是让根和堆底的最后一个元素互换,然后尾删,这时只需要再写一个向下调整的函数就可,所以我们接下来的重点就是写向下调整的函数
先理清思路:拿父亲跟左右孩子中较大的那个比较,如果这个孩子比父亲大则交换,反之结束。
ok接下来我们就可以代码实现了:
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 HeapPop(HP* php)
{
assert(php);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
//向下调整
AdjustDown(php->a,php->size, 0);
}
判空、取顶、取大小函数
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
HPDataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
测试走一波看看实力:
到这堆的结构差不多结束了,下面我们认识一下堆排序。
堆排序
给定一个数组,把它建成一个堆,实现堆排序,这里我们实现的是升序,先说思路是先调整数据建堆,然后向下调整排序。下面分别介绍建堆和排序的细节:
建堆
建立在我们已经写好向上和向下调整的函数,我们可以向上调整建堆或者向下调整建堆(前提是左右子树都是大堆或小堆),这里以向上调整建堆为例,展示效果:
向上调整建堆不难理解,如何向下调整建大堆呢,我们可以从后往前调整,什么意思呢?我们可以先找到最后一个叶子的根,将这两个看成一个小树进行向下调整,调整好之后,往回走,再排它前一个小树,依然是向下调整,直到最后调整整个树,这是一个逐步放大的过程,如下图:
(ps:图有些抽象,希望可以帮助你理解,这里通过不同颜色的切换来代表每一次的向下调整) 其实说白了就是让根不断地上移。
通过对比向上和向下调整建堆你可能会觉得向上调整建堆更好,但是其实向下调整建堆效率更高。
我们先通过代码实现一下向下调整建堆:
//向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
//这里解释一下不直接写成n-2的原因:n-1代表最后一个数据的下标,再-1除2是找出最后一个数据的父亲
{
AdjustDown(a, n, i);
}
向上调整的时间复杂度是O(N*logN)
向下调整的时间复杂度是O(N)
下面解释一下这里的时间复杂度:
首先是向上调整,根据代码for循环执行n次,for循环内部向上调整最多需要执行的次数跟树的高度相同logN,所以,向上调整的时间复杂度就是二者的乘积O(N*logN)
下图解释的是向下调整的时间复杂度:
排升序
排升序需要建大堆,因为如果建成小堆,按照小堆特点,第一个数就是最小的,此时第一个数位置确定,不需要将它看作堆的一部分,但是如果将剩下的数据继续看成小堆,关系全会打乱。如果我们建大堆,向下调整,把较大的数都依次调整到后面,这样在不改变堆的结构的同时完成了排序。
继续上面我们已经向上调整建好了大堆,然后需要做的就是向下调整排序
这里理一下思路:向下调整第一次,最大的那个数移到了最后一位,此时这个数的位置就确定了下来了,就不需要再挪动了,也就是不需要将这个数看作堆中的一部分,所以我们可以记录一下数组尾下标end,调整好一个数之后,end--就可,直到end=0结束,那么接下来就可以代码实现了:
void HeapSort(int* a, int n)
{
//排升序建大堆
//向上调整建堆
//for (int i = 1; i < n; i++)
//{
// AdjustUp(a, i);
//}
//向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
//这里解释一下不直接写成n-2的原因:n-1代表最后一个数据的下标,再-1除2是找出最后一个数据的父亲
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[end], &a[0]);
AdjustDown(a, end, 0);
--end;
}
}
到这我们就实现了堆排序,不难算出堆排序的时间复杂度是O(N*logN)
TopK问题
void PrintTopK(const char*file, int k)
{
//建堆--用a中前k个元素建小堆
int* topk = (int*)malloc(sizeof(int) * k);
assert(topk);
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
//读出前k个数据建堆
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &topk[i]);
}
for (int i = (k - 2) / 2; i >= 0; --i)
{
AdjustDown(topk, k, i);
}
//将剩余n-k个元素依次与堆顶的元素交换,不满足则替换
int val = 0;
int ret = fscanf(fout, "%d", &val);
while (ret != EOF)
{
if (val > topk[0])
{
topk[0] = val;
AdjustDown(topk, k, 0);
}
ret = fscanf(fout, "%d", &val);
}
for (int i = 0; i < k; i++)
{
printf("%d ", topk[i]);
}
printf("\n");
free(topk);
fclose(fout);
}
void TestTopK()
{
//造数据
int n = 10000;
srand(time(0));
const char* file = "data,txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (size_t i = 0; i < n; i++)
{
int x = rand() % 10000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
PrintTopK("data,txt", 10);
}
int main()
{
TestTopK();
return 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;
}
}
}
结束
到这,我们关于堆的介绍就基本结束了,下面是整个过程用到的代码:
Heap.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//大堆
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapPush(HP* php, HPDataType x);
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);
void AdjustDown(HPDataType* a, int n, int parent);
void AdjustUp(HPDataType* a, int child);
Heap.c
#include"Heap.h"
void HeapInit(HP* php)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
php->size = 0;
php->capacity = 4;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType x = *p1;
*p1 = *p2;
*p2 = x;
}
void AdjustUp(HPDataType* a,int child)
{
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* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * php->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
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;
}
}
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
//向下调整
AdjustDown(php->a,php->size, 0);
}
HPDataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
Test.c
#define _CRT_SECURE_NO_WARNINGS
#include"Heap.h"
#include<time.h>
//int main()
//{
// HP hp;
// HeapInit(&hp);
// HeapPush(&hp, 4);
// HeapPush(&hp, 18);
// HeapPush(&hp, 42);
// HeapPush(&hp, 12);
// HeapPush(&hp, 2);
// HeapPush(&hp, 3);
//
// while (!HeapEmpty(&hp))
// {
// printf("%d ", HeapTop(&hp));
// HeapPop(&hp);
// }
// printf("\n");
// return 0;
//}
//堆排序相关代码
//void HeapSort(int* a, int n)
//{
// //排升序建大堆
// //向上调整建堆
// //for (int i = 1; i < n; i++)
// //{
// // AdjustUp(a, i);
// //}
//
// //向下调整建堆
// for (int i = (n - 1 - 1) / 2; i >= 0; i--)
// //这里解释一下不直接写成n-2的原因:n-1代表最后一个数据的下标,再-1除2是找出最后一个数据的父亲
// {
// AdjustDown(a, n, i);
// }
//
// int end = n - 1;
// while (end > 0)
// {
// Swap(&a[end], &a[0]);
// AdjustDown(a, end, 0);
// --end;
// }
//}
//
//int main()
//{
// int a[10] = { 2,1,5,7,6,8,0,9,4,3 };
// HeapSort(a, 10);
//
// return 0;
//}
//TopK问题相关代码
//void PrintTopK(const char*file, int k)
//{
// //建堆--用a中前k个元素建小堆
// int* topk = (int*)malloc(sizeof(int) * k);
// assert(topk);
// FILE* fout = fopen(file, "r");
// if (fout == NULL)
// {
// perror("fopen error");
// return;
// }
// //读出前k个数据建堆
// for (int i = 0; i < k; i++)
// {
// fscanf(fout, "%d", &topk[i]);
// }
// for (int i = (k - 2) / 2; i >= 0; --i)
// {
// AdjustDown(topk, k, i);
// }
// //将剩余n-k个元素依次与堆顶的元素交换,不满足则替换
// int val = 0;
// int ret = fscanf(fout, "%d", &val);
// while (ret != EOF)
// {
// if (val > topk[0])
// {
// topk[0] = val;
// AdjustDown(topk, k, 0);
// }
// ret = fscanf(fout, "%d", &val);
// }
// for (int i = 0; i < k; i++)
// {
// printf("%d ", topk[i]);
// }
// printf("\n");
// free(topk);
// fclose(fout);
//}
//void TestTopK()
//{
// //造数据
// int n = 10000;
// srand(time(0));
// const char* file = "data,txt";
// FILE* fin = fopen(file, "w");
// if (fin == NULL)
// {
// perror("fopen error");
// return;
// }
// for (size_t i = 0; i < n; i++)
// {
// int x = rand() % 10000;
// fprintf(fin, "%d\n", x);
// }
// fclose(fin);
// PrintTopK("data,txt", 10);
//}
//int main()
//{
// TestTopK();
// return 0;
//}