- 堆表面是数组,内核是完全二叉树/满二叉树
- 在插入删除的时候要注意操作过后堆是否还是一个堆,要进行交换等操作。(向上调整)
- 逻辑上控制二叉树,物理上控制数组!!!
接下来我们用【小堆】来介绍堆的代码实现。
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);
int HeapSize(HP* php);
bool HeapEmpty(HP* php);
Heap.c
HeapInit初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
HeapDestroy销毁
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
HeapPush插入数据
- 这里要注意在一个堆后插入一个数,这个堆是否还是堆的问题。所以要将插入的数和他的父亲进行比较。
- 同时要考虑,如何使一个随机的数组排列成堆?这个问题在test.c中就可解决。
Swap函数实现
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
AdjustUp函数实现
- 理解判断条件:child > 0
- 首先比较插入的数和它的父亲,然后进行判断。
- 若满足判断条件,就交换两个数的位置,并让child向上调整到parent的位置,让parent向上调整到它的父亲的位置,依此类推。
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0) 不能这样写,因为parent不可能<0
while (child > 0) //用child判断
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
//往上走
child = parent;
parent = (child - 1) / 2;
//child = (child - 1) / 2;
//parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
HeapPush函数实现
- 时间复杂度为O(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);
}
HeapPop删除数据
- 删除根
- 不能用挪动覆盖法,堆会彻底乱掉,且时间复杂度是O(N)。
- 方法:首尾交换再尾删,然后向下调整。 这样左右子树依旧是小堆。时间复杂度为O(logN)。
- 如何判断叶子? 叶子*2+1>size
Swap函数实现
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
AdjustDown函数实现
- 越界问题:这里要注意在while里一定要加上【child+1<size】这个判断条件,因为如果child是完全二叉树叶子的左孩子,而且没有右孩子的情况,那么【if (a[child + 1] < a[child])】这个child+1就会越界。
void AdjustDown(int* a, int size, int parent)
{
//假设左孩子小,如果事实是右孩子小,那么就更新child
int child = parent * 2 + 1;
while (child < size)
{
if (child+1 < size && a[child + 1] < a[child])
{
++child;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
HeapPop函数实现
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
//首尾交换
Swap(&php->a[0], &php->a[php->size - 1]);
//删除
php->size--;
//向下调整
AdjustDown(php->a, php->size, 0);
}
HeapTop取堆顶元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
HeapSize堆元素个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
HeapEmpty判断是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
Test.c
首先测试一下插入。
- ⭐要注意这里使用了for循环,将数组中的数一个一个插入,然后排列成堆。这样就能保证这个数组是一个堆啦!
int main()
{
int a[] = { 4,6,2,1,5,8,2,9};
HP hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
{
HeapPush(&hp, a[i]);
}
while (!HeapEmpty(&hp))
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
printf("\n");
return 0;
}
测试取数组中前k个小的元素。
- 这里先将数组里的数据排列成一个小堆,然后用while循环, 取一个堆顶元素删一个。
int main()
{
int a[] = { 4,6,2,1,5,8,2,9};
HP hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
{
HeapPush(&hp, a[i]);
}
int k = 3;
while (k--)
{
printf("%d\n", HeapTop(&hp));
HeapPop(&hp);
}
return 0;
}
输出小堆
int main()
{
int a[] = { 4,6,2,1,5,8,2,9};
HP hp;
HeapInit(&hp);
for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
{
HeapPush(&hp, a[i]);
}
while (!HeapEmpty(&hp))
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
printf("\n");
return 0;
}