一、快速复习
-
结构定义:
- 堆是顺序存储的完全二叉树
- 其底层容器一般是顺序表
- 大堆:任何一个父节点都大于等于子节点;
- 小堆:任何一个父节点都小于等于子节点;
-
相关函数:
-
简单函数:empty, size, top
-
push:先将元素插入到堆尾,再向上调整
-
adjust_up:先求父节点的下标;while(child > 0)子节点与父节点比较,子节点<父节点:交换并向上跟踪父子节点;else break;复杂度:O(log(N)) (树高)
-
pop:先交换堆顶堆尾元素,再将堆尾元素弹出,最后向下调整堆顶元素
-
adjust_down:先求左孩子的下标;while(child < size)选出左右孩子中最小的节点与父节点比较(注意检查child+1<size);最小子节点<父节点:交换并向下跟踪父子节点;else break;复杂度:O(log(N)) (树高)
-
调堆:
- 方法一:遍历数组,逐个向上调整;
- 方法二(最优):从最后一个非叶节点(size-2)/2向前遍历,逐个向下调整。
向上调整最坏情况下,N节点都要调整到堆顶,因此复杂度:O(N*log(N))
向下调整最坏情况下,只需要将N/2个节点(最后一层除外)调整到堆底即可,进过计算复杂度:O(N)
-
-
应用
-
堆排序
-
要先写一个向下调整函数。
-
先调堆,向下调整建堆复杂度:O(N)
- 升序:调大堆
- 降序:调小堆
-
利用堆删除思想来进行排序,向下调整N次复杂度:O(N*log(N))
-
记录堆尾下标end,同时end是删除堆尾元素后的size值;
-
交换堆顶(0)堆尾(end)元素——将最大值交换到序列尾。
-
向下调整,但此时的调整范围到end——恢复堆结构选出最大值
-
–end——进行下一轮选择交换。
-
-
综上,堆排序的时间复杂度为O(N*log(N))
-
-
TopK问题
从N个数中找出最大/最小的前K个(一般K是远小于N的)
最大建小堆,最小建大堆,相反的。
-
用前K个数建立一个K个数的小堆:循环k次,将前k个数依次插入堆中。
-
剩下的N-K个数(从k开始循环n-k次),依次跟堆顶的数据比较。如果比堆顶的数据大,就替换堆顶的数据,再向下调整。
-
最后堆里的K个数就是最大的K个数。
-
-
二、基本操作
1.结构的声明及定义
typedef int HPDataType;
typedef struct Heap{
HPDataType *arr;
int size;
int capacity;
}Heap;
Heap hp;
HeapInit(&hp);
2.向上调整
void AdJustUp(HPDataType *arr, int child){
int parent = (child - 1) / 2;//性质五
while (child > 0){
if (arr[child] < arr[parent]){
Swap(&arr[parent], &arr[child]);
child = parent;
parent = (child - 1) / 2;
}
else{
break;
}
}
}
- 循环条件:(child > 0),不能写成parent>=0(child=0时parent恒等于0)
- 交换条件:(arr[child] < arr[parent]),子在前父在后,方便改大小堆(小堆<,大堆>)
- 向上调整的时间复杂度:O(log(N)) (树的高度)
3.向下调整
void AdJustDown(HPDataType *arr, int size, int root){
assert(arr != NULL);
int parent = root;
int child = parent * 2 + 1;//性质五,左孩子
while (child < size){
//选出值最小(小堆)/最大(大堆)的子节点与父节点比较
// 1、选出左右孩子中小的那个
if (child + 1 < size && arr[child + 1] < arr[child]) {
child++;
}
// 2、如果孩子小于父亲,则交换,并继续往下调整
if (arr[child] < arr[parent]){
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else{
break;
}
}
}
- 注意检查 child + 1 < size,否则可能出现越界访问。
- 修改两处变大小堆:
- arr[child + 1] < arr[child](兄弟节点比较)
- arr[child] < arr[parent](父子节点比较)
向下调整的时间复杂度:O(log(N)) (树的高度)
4.创建堆
这里所说的创建堆实际上是把一个无序的数组通过向上调整或向下调整算法整理成堆。
//方法一:向上调整
/*for (int i = 0; i < n; i++){
AdJustUp(arr, i);
} */
//方法二:向下调整
for (int i=(n-2)/2; i >= 0; i--)
{
AdJustDown(arr, sz, i);
}
向上调整建堆:
- 方法类似于依次插入堆元素
- 时间复杂度:O(N*log(N))
向下调整建堆:
- 向下调整的前提是左右子树都是堆。因此我们倒着从最后一个非叶子节点(最后一个节点的父节点)开始按顺序向下调整每一个子树,这样就能保证向下调整前左右子树都是堆。
- 时间复杂度:O(N)
5.插入堆元素
void HeapPush(Heap *ph, HPDataType val){
assert(ph != NULL);
//扩容
if (ph->size == ph->capacity){
size_t newcapacity = ph->capacity == 0 ? 5 : ph->capacity * 2;
HPDataType *tmp = (HPDataType*)realloc(ph->arr, sizeof(HPDataType)*newcapacity);
if (tmp == NULL){
perror("HeapPush");
exit(1);
}
ph->arr = tmp;
ph->capacity = newcapacity;
}
ph->arr[ph->size] = val;
ph->size++;
AdJustUp(ph->arr, ph->size - 1);//实现堆的逻辑结构
}
6.删除堆顶元素
void HeapPop(Heap *ph){
assert(ph != NULL);
assert(!HeapEmpty(ph));
Swap(&ph->arr[0], &ph->arr[ph->size - 1]);
ph->size--;
AdJustDown(ph->arr, ph->size, 0);
}
- 注意判断堆是否为空
- 删除堆顶元素的步骤:
- 将堆顶元素和数组中的最后一个元素交换
- size–删除最后一个元素
- 将堆顶元素向下调整
7.其他
//初始化
void HeapInit(Heap *ph){
assert(ph != NULL);
ph->arr = NULL;
ph->size = ph->capacity = 0;
}
//销毁
void HeapDestroy(Heap *ph){
assert(ph != NULL);
free(ph->arr);
ph->arr = NULL;
ph->size = ph->capacity = 0;
}
//打印堆元素
void HeapPrint(Heap* ph){
assert(ph != NULL);
for (int i = 0; i < ph->size; i++){
printf("%d ", ph->arr[i]);
}
printf("\n");
}
//交换
void Swap(HPDataType* pa, HPDataType* pb){
HPDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//判空
bool HeapEmpty(Heap *ph){
assert(ph != NULL);
return ph->size == 0;
}
//获取堆的大小
size_t HeapSize(Heap *ph){
assert(ph != NULL);
return ph->size;
}
//获取堆顶元素的值
HPDataType HeapTop(Heap *ph)
{
assert(ph != NULL);
assert(!HeapEmpty(ph));
return ph->arr[0];
}
三、TopK问题
从N个数中找出最大/最小的前K个(一般K是远小于N的)
方式一:排序后找最值
时间复杂度:O(N*logN)
缺点:过于复杂,杀鸡焉用牛刀。
方式二:大堆法
- 向下调整调堆 O(N)
- Pop K次,每次取堆顶的数据即可。O(logN*K)
- 求最小TopK用小堆
时间复杂度:O(N+logN*K)
缺点:假设N非常大,N是10亿,内存中存不下这些数,他们存在文件中。找出TopK个数。
方式一和方式二就都不能用了。
方式三:小堆法
- 用前K个数建立一个K个数的小堆。
- 剩下的N-K个数,依次跟堆顶的数据比较。如果比堆顶的数据大,就替换堆顶的数据,再向下调整。
- 最后堆里的K个数就是最大的K个数。
- 求最小TopK用大堆
时间复杂度:K + (N-K)logK —> O(NlogK) —> O(N)
这里为什么用小堆,不用大堆?
- 用小堆,较大的数会向下调整 ,前100个数都进的来。
- 用大堆,最大的数挡在堆顶,前100个数可能进不来,只能选出最大的数。
//以下是一段测试代码
void TestTopk()
{
size_t n = 10000;
int* a = (int*)malloc(sizeof(int)*n);
srand((unsigned)time(0));
for (size_t i = 0; i < n; ++i)
{
a[i] = rand() % 1000000;
}
//随机抽取10个数将其置为TopK
a[5] = 1000000 + 1;
a[1231] = 1000000 + 2;
a[531] = 1000000 + 3;
a[5121] = 1000000 + 4;
a[115] = 1000000 + 5;
a[2305] = 1000000 + 6;
a[99] = 1000000 + 7;
a[76] = 1000000 + 8;
a[423] = 1000000 + 9;
a[0] = 1000000 + 1234;
PrintTopK(a, n, 10);
}
//小堆法解决TopK问题
void PrintTopK(HPDataType *a, int n, int k){
assert(a != NULL);
int i = 0;
Heap hp;
HeapInit(&hp);
// 1. 建堆--用a中前k个元素建堆
for (i = 0; i < k; i++){
HeapPush(&hp, a[i]);
}
// 2. 将剩余n-k个元素依次与堆顶元素比较,若大于堆顶元素则交换
for (i = k; i < n; i++){
if (a[i] > HeapTop(&hp)){
//HeapPop(&hp);
//HeapPush(&hp, a[i]);
hp.arr[0] = a[i];
//3. 向下调整
AdJustDown(hp.arr, hp.size, 0);
}
}
HeapPrint(&hp);
}
四、堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 建堆
- 升序:建大堆
- 降序:建小堆
- 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
void HeapSort(HPDataType *arr, int n){
//1. 建堆
//方法一:向上调整
/*for (int i = 0; i < n; i++){
AdJustUp(arr, i);
} */
//方法二:向下调整O(N)
for (int i=(n-2)/2; i >= 0; i--)
{
AdJustDown(arr, n, i);
}
//2. 利用堆删除思想来进行排序O(N*log(N))
for (int end = n - 1; end > 0; end--){
Swap(&arr[0], &arr[end]);
AdJustDown(arr, end, 0);
}
}
堆排序的时间复杂度:O(N*log(N))