目录
一.初始化
二.插入
三.删除(堆顶、根)
四.整体代码
Heap.h
Test.c
Heap.c
我们使用顺序结构实现完全二叉树,也就是堆的实现
以前学的数据结构只是单纯的存储数据。堆除了存储数据,还有其他的价值——排序。是一个功能性的数据结构
小根堆堆顶的数据一定是最小的,大根堆堆顶的数据一定是最大的
选出最大/最小,再选次大/次小……不断选最后就帮助排序。还可以解决取前几,后几的TOP-K 问题
我们以建大堆为例。建小堆只需改变 爸 < 娃 即可
一.初始化
下面多次用到交换,将交换分装成函数
Heap.h
typedef int HPDataTypt;typedef struct Heap
{HPDataTypt* a; // 数组指针,指向要开辟的存储数据的数组int size; // 当前已存储的有效数据个数int capacity; // 最大容量
}HP;void HeapInit(HP* php); // 初始化
void HeapDestroy(HP* php); // 销毁
Heap.c
void HeapInit(HP* php)
{assert(php);php->a = (HPDataTypt*)malloc(sizeof(HPDataTypt) * 4);if (php->a == NULL){perror("malloc fail");return;}php->size = 0;php->capacity = 4;
}void Swap(HPDataTypt* p1, HPDataTypt* p2)
{HPDataTypt tmp = *p1;*p1 = *p2;*p2 = tmp;
}
php 是指向主函数中,HP(结构体类型)的变量 hp 地址的指针。php 中存放的是 hp 的地址。若为空就说明结构体没有开好,所以一定不能为空,断言。
二.插入
堆的底层就是数组,可以插入数据。要把控制数组想象成控制树。原来是大根堆,插入后,要求还得是堆。
插入前是堆,插入后会影响部分祖先(跟祖先调整)
以大根堆为例,看最简单的情况:插入20,插入后不影响堆的性质。
再插入60,插入后要调整。
为保证父亲 > 娃,要交换
娃 还> 父亲,继续交换 父亲 > 娃,结束
上面的过程叫 向上调整 ,最多调整高度次,时间复杂度:O( log N )。插入一个数据,想让他再调整成堆只要 log N 次
堆的插入不像链表、顺序表,不能想往哪插就往哪插,要保持性质。
上面的尾插,如果堆原来是这样,就不能尾插20
所以插入单纯的叫 Push 就好,因为不是由接口指定在哪个位置插入。
void AdjustUp(HPDataTypt* 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, HPDataTypt x)
{assert(php);if (php->size == php->capacity){HPDataTypt* tmp = (HPDataTypt*)realloc(php->a, sizeof(HPDataTypt) * php->capacity * 2);if (tmp == NULL){perror("malloc fail");return;}php->a = tmp;php->capacity *= 2;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1); // 从孩子(插入数据)位置向上调整
}
三.删除(堆顶、根)
堆删除,删尾轻松,但无意义。
为什么删堆顶、根才有意义?老大被干掉了,老二才能冒头。
堆实现的意义,无论是排序还是 top-k ,本质是在帮我选数,选出最大/最小数
删除后,也要保证是堆。要把最大的删掉,怎么搞?
不能挪动删除(直接删)!原因:1.效率低下 2.父子兄弟关系全乱了
正确方法:(间接删) 堆顶和最后的元素换一下;--size,使换下去的最后一个(原堆顶)元素失效
1.效率高 2.最大程度的保持了父子关系
单看左右子树依旧是大堆,换上去的原最后元素大概率是比较小的,就要向下调整
看下面的新场景:为保证换了之后父亲 > 娃,现在的堆顶(原最后一个元素)要跟大的娃换。
娃中大的 > 爸,把爸换下去
继续换
最坏情况调到叶子结束。物理上是数组,怎么判断到叶子——没有娃,怎么判断没有娃呢?
把它当做爸,算左娃的下标,如果超出数组范围就没娃,所以参数要多给个数组的大小 size,用来判断 child 是否越界
最坏走高度 log N 次
void AdjustDown(HPDataTypt* a, int n, int parent)
{int child = parent * 2 + 1; // 默认左孩子大,将左孩子定为 childwhile (child < n){// 选出左右孩子中大的那一个if (child + 1 < n && a[child] < a[child + 1]) // 防止无右娃的越界风险{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);assert(!HeapEmpty(php));Swap(&php->a[0], &php->a[php->size - 1]); // 交换堆顶、最后元素php->size--; // 删除换下来的原堆顶元素AdjustDown(php->a, php->size, 0); // 向下调整,0是开始调整位置的下标// n 是有效数据个数,作为下标,用来判断 child 是否越界
}
向上调整的前提:除了 child 这个位置,前面的数据构成堆
向下调整的前提:保证左右子树都是堆
四.整体代码
Heap.h
typedef int HPDataTypt;typedef struct Heap
{HPDataTypt* a; // 数组指针,指向要开辟的存储数据的数组int size; // 当前已存储的有效数据个数int capacity; // 最大容量
}HP;void HeapInit(HP* php); // 初始化
void HeapDestroy(HP* php); // 销毁void HeapPush(HP* php, HPDataTypt x); // 插入
void HeapPop(HP* php);// 删除堆顶HPDataTypt HeapTop(HP* php); // 堆顶的数据
bool HeapEmpty(HP* php); // 探空
int HeapSize(HP* php);void AdjustUp(HPDataTypt* a, int child); // 向上调整
void AdjustDown(HPDataTypt* a, int n, int parent); // 向下调整
Test.c
void test1() // 排序
{HP hp;HeapInit(&hp);HeapPush(&hp, 2);HeapPush(&hp, 45);HeapPush(&hp, 76);HeapPush(&hp, 23);HeapPush(&hp, 5654);HeapPush(&hp, 24);HeapPush(&hp, 5);HeapPush(&hp, 242);HeapPush(&hp, 25);while (!HeapEmpty(&hp)){printf("%d ", HeapTop(&hp));HeapPop(&hp);// 选老二,必须干掉老大}HeapDestroy(&hp);
}void test2() // top-k
{HP hp;HeapInit(&hp);HeapPush(&hp, 2);HeapPush(&hp, 45);HeapPush(&hp, 76);HeapPush(&hp, 23);HeapPush(&hp, 5654);HeapPush(&hp, 24);HeapPush(&hp, 5);HeapPush(&hp, 242);HeapPush(&hp, 25);HeapPush(&hp, 5);HeapPush(&hp, 5);int k = 0;scanf("%d", &k);while (!HeapEmpty(&hp) && k--){printf("%d ", HeapTop(&hp));HeapPop(&hp);// 选老二,必须干掉老大}HeapDestroy(&hp);
}
Heap.c
void HeapInit(HP* php)
{assert(php);php->a = (HPDataTypt*)malloc(sizeof(HPDataTypt) * 4);if (php->a == NULL){perror("malloc fail");return;}php->size = 0;php->capacity = 4;
}void Swap(HPDataTypt* p1, HPDataTypt* p2)
{HPDataTypt tmp = *p1;*p1 = *p2;*p2 = tmp;
}void AdjustUp(HPDataTypt* 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, HPDataTypt x)
{assert(php);if (php->size == php->capacity){HPDataTypt* tmp = (HPDataTypt*)realloc(php->a, sizeof(HPDataTypt) * php->capacity * 2);if (tmp == NULL){perror("malloc fail");return;}php->a = tmp;php->capacity *= 2;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1); // 从孩子(插入数据)位置向上调整
}void AdjustDown(HPDataTypt* a, int n, int parent)
{int child = parent * 2 + 1; // 默认左孩子大,将左孩子定为 childwhile (child < n){// 选出左右孩子中大的那一个if (child + 1 < n && a[child] < a[child + 1]) // 防止无右娃的越界风险{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);assert(!HeapEmpty(php));Swap(&php->a[0], &php->a[php->size - 1]); // 交换堆顶、最后元素php->size--; // 删除换下来的原堆顶元素AdjustDown(php->a, php->size, 0); // 向下调整,0是开始调整位置的下标// n 是有效数据个数,作为下标,用来判断 child 是否越界
}HPDataTypt HeapTop(HP* php)
{assert(php);return php->a[0];
}bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}int HeapSize(HP* php)
{assert(php);return php->size;
}void HeapDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->size = php->capacity = 0;
}