目录
1.堆的概念及结构
如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: = 且 >= ) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
2.堆的实现
2.1堆的创建
这里我们先给出一个数组和它的逻辑结构(我们要把数组看成一个完全二叉树)
接下来构建大堆
这里给出一些公式,可以自己看图理解:
leftchild = parent * 2 + 1;
rightchild = parent * 2 + 2;
parent = (child - 1) / 2; // child是leftchild或rightchild的下标
leftchild、rightchild、parent、child都是结点下标
先将数组构建成堆
向下调整算法:
这里用最后一步来解释,请看下图
parent(下标) = 0;
leftchild = parent * 2 + 1 = 0 * 2 + 1 = 1;rightchild = parent * 2 + 2 = 2;
先判断leftchild < 6 && rightchild < 6 ,(6是数组的元素个数)
满足条件,再比较a[leftchild] 和 a[rightchild] 谁大,这里是a[leftchild]大,
然后再比较a[parent]和a[leftchild]谁大
如果是a[parent](父结点)大的话就不用再向下调整了
这里是a[leftchild]大,于是要将两个值交换,同时parent = leftchild;接着就重复上面的过程
2.2开始排序
现在我们已经建好大堆了,然后我们将第一个结点和最后一个结点交换,再进行调整
整体代码
// a是数组 n是数组元素个数 parent是父结点
void AdjustDown(int* a,int n, int parent)
{
// 找到左孩子结点的下标
int child = parent * 2 + 1;
while (child < n) //下标要小于n
{
// 判断左孩子大还是右孩子大 同时右孩子的下标也要保持 < n
if (a[child] > a[child + 1] && (child + 1) < n)
{
child = child + 1;
}
// 当父结点小于孩子时
if (a[child] < a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else // 说明该结点已经调好了
{
break;
}
}
}
// 对数组进行堆排序
void HeapSort(int* a, int n) // n为数组元素的个数
{
// 先将数组构建成堆 (n - 1)为最后一个元素的下标
int parent = ((n - 1) - 1) / 2; // 先找倒数第一个非叶子结点
for (int i = parent; i >= 0; i--)
{
// 从倒数第一个非叶子节点开始调整
AdjustDown(a, n, i); // 这里采取向下调整算法
}
// 开始排序
// 将根节点元素与最后一个节点元素交换再进行调整
int size = n - 1;
while (size > 0)
{
swap(&a[0], &a[size]); // 根节点和最后一个节点交换
AdjustDown(a, size, 0); // 除去最后一个节点,数组长度 = n - 1
size--;
}
}
3.堆排的时间复杂度
3.1建堆的时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明时间复杂度:
则结点总移动步数为:
因此:建堆的时间复杂度为O(N)
3.2堆正式排序过程的时间复杂度
排序是先第一个结点和最后一个结点交换,交换完后,再对堆顶结点进行向下调整
所以总的时间复杂度为:O(N*logN)
4.堆的应用(Top-K问题)
void CreateNDate()
{
// 造数据
int n = 10000;
srand((unsigned int)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() % 1000000; // 让数据小于1000000,便于后面测试
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
这个是造数据的代码,数据存入data.txt这个文件中(这个利用文件指针写入数据,看不懂也没太大关系,知道这个函数的作用就行)
// 查找数据中最大的前K个数
void PrintTopK()
{
int k = 0;
printf("请输入k值");
scanf("%d", &k);
int* a = (int*)malloc(sizeof(int) * k);
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
// 读取文件前K个数据存入a数组中
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &a[i]);
}
// 现将文件中前k个元素构件成小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, k, i);
}
// 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
int temp = 0;
while (fscanf(fout, "%d", &temp) != EOF)
{
if (temp > a[0])
{
swap(&temp, &a[0]);
}
AdjustDown(a, k, 0);
}
fclose(fout);
for (int i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
free(a);
a = NULL;
}
我们先调用CreateNDate()函数造数据,运行成功后,按照下面顺序,打开该项目下文件
这些是随机创建的数据,不过都比1000000这个小,于是我们修改5个数据让他们大于1000000以验证程序的准确性
修改这5个元素,修改后要记得保存
然后再调用PrintTopK()函数
1000000数据中最大的5个数,这里没进行排序,要排序就调用一下我们写的HeapSort(int* a, int n)这个堆排序函数。
Top-K问题的时间复杂度
建堆消耗时间K,后面剩余N-K个数据与堆顶进行比较,假设后面剩余的每一个元素都与堆顶进行交换,然后向下调整到完全二叉树的最后一层(向下调整到最后一层时间复杂度为logK(树的高度))
所以时间复杂度为K+ (N - K)*logK