堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。
如果有一个关键码的集合K = { k0,k1 ,k2 ,…,kn-1?},把它的所有元素按完全二叉树的顺序存储方式存储
在一个一维数组中,并满足:?
则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
堆的实现
堆向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整
成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算
法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的
子树开始调整,一直调整到根节点的树,就可以调整成堆。
int a[] = {1,5,3,8,7,6};
?
?建堆时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的
就是近似值,多几个节点不影响最终结果):
?因此:建堆的时间复杂度为O(N)。
堆的插入
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。
堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调
整算法。
?
堆的代码实现
先看看具体需要实现哪些功能
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;//大小
int capacity;//最大容量
}Heap;
//堆的初始化
void HeapInit(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
//堆的打印
void HeapPrint(Heap*hp);
//向上调整(大堆/小堆)
void AdjustUp(HPDataType* a, int n, int child);
//向下调整(大堆/小堆)
void AdjustDown(HPDataType*a,int n,int root);
//交换
void swap(HPDataType*a, HPDataType*b);
下面来看看具体实现
堆的初始化
void HeapInit(Heap* hp, HPDataType* a, int n)
{
assert(hp&&a);
hp->a = (HPDataType*)malloc(sizeof(HPDataType)*n);
hp->size = n;
hp->capacity = n;
for (int i = 0; i < n; i++)
{
hp->a[i] = a[i];
}
for (int i=(n-2)/2;i>=0;i--)
{
AdjustDown(hp->a,hp->size,i);
}
}
??堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
hp->size = 0;
hp->capacity = 0;
free(hp->a);
hp->a = NULL;
}
堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
//扩容
if (hp->size == hp->capacity)
{
hp->capacity *= 2;
hp->a = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * hp->capacity);
}
hp->a[hp->size] = x;
hp->size++;
//向上调整
AdjustUp(hp->a,hp->size,hp->size-1);
}
?堆的删除
// 堆的删除
void HeapPop(Heap* hp)
{
assert(hp);
//先把第一个和最后一个交换
swap(&hp->a[0], &hp->a[hp->size - 1]);
//删掉最后一个
hp->size--;
//向下调整
AdjustDown(hp->a,hp->size,0);
}
获取堆顶的数据?
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
return hp->a[0];
}
?堆的数据个数
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
堆的判空?
// 堆的判空
int HeapEmpty(Heap* hp)
{
assert(hp);
return hp->size == 0 ;
}
堆的打印?
//堆的打印
void HeapPrint(Heap*hp)
{
assert(hp);
for (int i = 0; i < hp->size; i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
?
向上调整
//向上调整(大堆)
void AdjustUp(HPDataType* a, int n, int child)
{
assert(a);
int parent = (child - 1) / 2;
while (child>0&&a[child]>a[parent])
{
swap(&a[parent],&a[child]);
child = parent;
parent = (child - 1) / 2;
}
}
//向上调整(小堆)
void AdjustUp(HPDataType* a, int n, int child)
{
assert(a);
int parent = (child - 1) / 2;
while (child>0&&a[child]<a[parent])
{
swap(&a[parent],&a[child]);
child = parent;
parent = (child - 1) / 2;
}
}
向下调整?
//向下调整(大堆)
void AdjustDown(HPDataType*a,int n,int root)
{
assert(a);
int parent = root;
int child = root*2+1;
if (child<n&&a[child+1] > a[child ])
{
child++;
}
while (child<n&&a[child]>a[parent])
{
swap(&a[child],&a[parent]);
parent = child;
child = 2 * parent + 1;
if (child<n && a[child+1] > a[child ])
{
child++;
}
}
}
//向下调整(小堆)
void AdjustDown(HPDataType*a,int n,int root)
{
assert(a);
int parent = root;
int child = root*2+1;
if (child<n&&a[child+1] < a[child ])
{
child++;
}
while (child<n&&a[child]<a[parent])
{
swap(&a[child],&a[parent]);
parent = child;
child = 2 * parent + 1;
if (child<n && a[child+1] <a[child ])
{
child++;
}
}
}
交换?
//交换
void swap(HPDataType*a, HPDataType*b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
堆的应用
堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
升序:建大堆
降序:建小堆
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
?下面是借用大堆来实现排升序的代码
//建大堆排升序
void HeapSort(int *a,int n)
{
//先建大堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(a , n, i);//大堆
}
//再排序
for (int i = n-1; i > 0; i--)
{
swap(&a[0], &a[i]);
AdjustDown(a, i, 0);//大堆
}
}
TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能
数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
?
//建小堆来取最大前K个数
void PrintTopK(int* a, int n, int k)
{
assert(a);
Heap hp;
// 创建一个K个数的小堆
HeapInit(&hp,a,k);
for (int i=k;i<n;i++)
{
if (HeapTop(&hp) < a[i])
{
HeapPop(&hp);
HeapPush(&hp, a[i]);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
}
//测试
void TestTopk()
{
int n = 10000;
int* a = (int*)malloc(sizeof(int)*n);
srand(time(0));
for (size_t i = 0; i < n; ++i)
{
a[i] = rand() % 1000000;
}
a[5] = 1000000 + 1;
a[1231] = 1000000 + 2;
a[531] = 1000000 + 3;
a[5121] = 1000000 + 4;
a[115] = 1000000 + 5;
a[2335] = 1000000 + 6;
a[9999] = 1000000 + 7;
a[76] = 1000000 + 8;
a[423] = 1000000 + 9;
a[3144] = 1000000 + 10;
PrintTopK( a, n, 10);
}