一、堆的概念及实现
这里的堆和malloc的堆不是同一个意思,这里的堆只是一种数据结构,而后者是操作系统里的区域。这里的堆其实就是二叉树的顺序结构。是非线数据结构。
1. 堆的性质:
1.1.堆中某个节点的值总是不大于或不小于其父亲节点的值。
1.2.堆是一棵完全二叉树。
上图父亲节点总是小于孩子节点,所以这个是一个小堆,大堆反之。
那有什么作用呢? TopK问题。
因为用选数,所以可以用来排序。大堆堆顶就是最大值,小堆堆顶就是最小值。
例如要选出大众点评里上海市浦东区最好吃的本帮面排行前top10。还有王者荣耀地域英雄战力排名前top100。
二、堆的实现:
2.1.堆的创建
给定一个数组a[ ] ,里面的数据特别的杂乱,它的逻辑结构是一棵完全二叉树,不一定是一个堆,我们可以通过向下调整算法去创建一个堆。具体看实现。
int main()
{
int a[] = { 27,15,19,18,28,34,65,49,25,37 };
HP hp;
HeaPInit(&hp);
for (size_t i = 0; i < sizeof(a)/sizeof(int); i++)
{
HeaPPush(&hp, a[i]);
}
return 0;
}
此数组里的数据逻辑结构虽然并不是一个堆,但它比较幸运,因为根节点的左右子树是小堆。
2.2.向下调整算法
先介绍这个算法,但不是一来就用这个算法,这个算法有前提。例如:
虽然操作的是树,但是物理结构上是对数组的数据操作。 所以判断根节点与孩子节点交换时,直到叶子节点时,是靠数组下标乘除算的。父亲节点 == (孩子节点 - 1)/2。
拿小堆举例。前提:左右子树都是小堆。
//交换函数
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//如果孩子更小,则与父亲交换,小堆,大堆则是修改符号
void AdjustDown(HPDataType* a, int n, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < n)
{
//找左右孩子更小的那一个
if (minChild + 1 < n && a[minChild + 1] < a[minChild])
{
minChild++;
}
//交换并且向下调整
if (a[minChild] < a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;//不止交换一个,所以交换,直到while结束
minChild = parent * 2 + 1;//重新算minchild的位置
}
else
{
break;
}
}
}
从上图和算法可以看出,这个算法前提是:根节点的左右子树必须是一个堆,才能完成此算法,但是如果没有那么幸运呢?那么我们就要去就是构建一个这样的堆。
2.3.堆的构建
如何去完成呢?想办法把左子树和右子树搞成小堆就完事啦。我们可以倒着去完成,从倒数第一个非叶子节点开始调整,因为树是递归的,先从从最底下的左右子树开始调整,直到根节点的左右子树都为小堆。如果不能理解树的结构是递归的,就比较难理解为什么要倒着去调整,树的结构是递归的等下篇会讲解它的链式存储结构时会写它是如何递归的。
那么怎么找到这个节点,其实就是最后一个节点的父亲。
调堆:
for (int i = ((n - 1) - 1) / 2; i >= 0; --i)
{
AdjustDown( a, n, i);
}
//为什么是((n - 1) - 1) / 2 呢?
之后则变成了这个顺序:
int a[] = { 15,18,19,25,28,34,65,49,27,37 };
倒数第一个节点的值,为什么是((n-1)-1)/2呢?树只是我们想象出来的,它底层物理结构还是数组的,所以是求它的数组下标,(n-1) 是最后一个节点的下标,再-1是套父亲与孩子关系算式公式。如果父亲下标是i 左孩子:2*i + 1 右孩子:2*i + 1。孩子下标是i 父亲下标:(i - 1)/ 2。
看完后,其实就是有了这个AdjustDown函数,才能构建一个堆,那它的前提呢?其实不然,倒数第一个非叶子节点,它的孩子节点(最后一个节点)其实就是一个堆,因为它的左右孩子都是NULL;当然可以看成一个堆啦,树的结构是递归的也是把它展现出来。
这样调整完之后,就可以直接使用向下调整算法了。
2.4.构建堆时间复杂度
假设树有N个节点,满二叉树高度为 log₂(N+1),时间复杂度为log₂N。
但是要注意,这里不是N*log₂N,它的时间复杂度是O(N)。
2.5.堆的排序
如果排完一个小堆,找到了最小的,但是要找次小的,再找第三小的,.......,继续从它的左右子树分别进行向下调整算法构建堆的话,那么时间复杂度为O(N^2),这就不能体现出堆排序的价值所在。那么如何去找次小的呢?请往下看:
首先理清楚,如果这个小堆/大堆有10个数据,这个堆的堆顶已经是最小/最大的数据了,让这个数组的第一位与最后一位交换位置,这样堆顶就到了最后一位,再对前面9个进行向下调整算法,以此类推,一直重复,就可以得到一个排序数组了。并且它的时间复杂度为O(N*logN)。
排降序:建小堆。
排升序:建大堆。
// 升序 O(N*logN) 1000*10 100w*20
// O(N*N) 1000*1000 100w*100w
// O(N)空间复杂度,还需要再优化
// 升序
//void HeapSort(int* a, int size)
//{
// // 小堆
// HP hp;
// HeapInit(&hp);
// // O(N*logN)
// for (int i = 0; i < size; ++i)
// {
// HeapPush(&hp, a[i]);
// }
//
// // O(N*logN)
// size_t j = 0;
// while (!HeapEmpty(&hp))
// {
// a[j] = HeapTop(&hp);
// j++;
// HeapPop(&hp);
// }
//
// HeapDestroy(&hp);
//}
// 优化
// 时间复杂度O(N*logN)
// 空间复杂度O(1)
// 升序
void HeapSort(int* a, int n)
{
// 向上调整--建堆 O(N*logN)
//for (int i = 1; i < n; ++i)
//{
// AdjustUp(a, i);
//}
// 向下调整--建堆 O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
size_t end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
2.6.堆的插入
插入数组的最后一个位置,再调用向上调整算法,直到形成一个堆。
// 插入x继续保持堆形态 -- logN
void HeapPush(HP* php, HPDataType x)
{
assert(php);
// 扩容
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity*sizeof(HPDataType));
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);
}
2.7.堆的删除
堆的删除是删除堆顶元素,首先将堆顶数据与堆最后一个数据交换,交换完之后,就可以直接删了最后一个,然后再调用向下调整算法。
// 删除堆顶元素 -- 找次大或者次小 -- logN
// O(logN)
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);
}
三、TopK应用
简单来说,加入我们有1000000(N)个数,要求排出前10(K)。
方法一:排序,但是海量数据,明显就不可取,数据太大。时间复杂度为O(N*logN)。
方法二:建一个N个数的堆,不断选数,选出前K。时间复杂度O(N + K*logN)
方法三:排降序:建小堆。排升序:建大堆。最佳方案。如下:
假如要取前10位大的元素,先用10个元素建立一个小堆,用剩下的 N-K 个数依次与堆顶比较,如果这个元素比堆顶大那么就让这个元素成为堆顶,再进行向下调整算法,一直循环。
接下来我们实现一下:
void CreateDataFile(const char* filename, int N)
{
FILE* fin = fopen(filename, "w");
if (fin == NULL)
{
perror("fopen fail");
return;
}
srand(time(0));
for (int i = 0; i < N; ++i)
{
fprintf(fin, "%d\n", rand()%1000000);
}
fclose(fin);
}
void PrintTopK(const char* filename, int k)
{
assert(filename);
FILE* fout = fopen(filename, "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
int* minHeap = (int*)malloc(sizeof(int)*k);
if (minHeap == NULL)
{
perror("malloc fail");
return;
}
// 如何读取前K个数据
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &minHeap[i]);
}
// 建k个数小堆
for (int j = (k - 2) / 2; j >= 0; --j)
{
AdjustDown(minHeap, k, j);
}
// 继续读取后N-K
int val = 0;
while (fscanf(fout, "%d", &val) != EOF)
{
if (val > minHeap[0])
{
minHeap[0] = val;
AdjustDown(minHeap, k, 0);
}
}
for (int i = 0; i < k; ++i)
{
printf("%d ", minHeap[i]);
}
free(minHeap);
fclose(fout);
}
四、代码实现:
首先:计算机除号取整的原因,父亲节点 == (孩子节点 - 1)/2。
Heap.h
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HeapPrint(HP* php);
void Swap(HPDataType* p1, HPDataType* p2);//交换函数
void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);
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);
Heap.c
#include "Heap.h"
//打印堆数据
void HeapPrint(HP* php)
{
for (int i = 0; i < php->size; ++i)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
//堆初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
//堆销毁
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
//交换函数
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向上调整算法
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
// 插入x继续保持堆形态 -- logN
void HeapPush(HP* php, HPDataType x)
{
assert(php);
// 扩容
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity*sizeof(HPDataType));
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);
}
void AdjustDown(HPDataType* a, int n, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < n)
{
// 找出小的那个孩子
if (minChild+1 < n && a[minChild + 1] < a[minChild])
{
minChild++;
}
if (a[minChild] < a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;
minChild = parent * 2 + 1;
}
else
{
break;
}
}
}
// 删除堆顶元素 -- 找次大或者次小 -- logN
// O(logN)
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);
assert(!HeapEmpty(php));
return php->a[0];
}
//堆的判空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//堆的数据个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}