1. 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中通常 把堆使用顺序结构的数组来存储 ,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构(完全二叉树),一个是操作系统中管理内存区域分段。
2. 堆的概念
堆是将数组数据看作一颗完全二叉树。递增递减数组一定是堆,堆不一定是递增递减数组。在实际意义中,堆可以实现堆排序,时间复杂度是 O(N*longN)
,提高查找效率,解决top k问题。
堆的性质:
1)堆总是一棵完全二叉树
2)堆中某个节点的值总是不大于或不小于其父节点的值
3. 堆的分类
堆可以分为大堆和小堆:
大堆要求: 任意一个父亲 <= 孩子
小堆要求: 任意一个父亲 >= 孩子
4. 堆的实现(数组小堆)
这里将着重利用父子节点之间的关系完成堆的构建以及各类接口的实现:
leftchild = parent*2+1 # 奇数
rightchild = parent*2+2 # 偶数
parent = (child-1)/2 # 不区分左右孩子
如果要实现大堆,就将对应的向下调整算法和向上调整算法的判定更改即可实现。下面将其分为3个模块进行实现小堆Heap.h,Heap.c,test.c
4.1 接口设计(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 HeapDestory(HP* php);
// 小堆
// 插入,时间复杂度是O(logN)
void HeapPush(HP* php, HPDataType x);
// 规定删除堆顶,即根节点
// 挪动数据覆盖删除跟会出问题
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
size_t HeapSize(HP* php);
bool HeapEmpty(HP* php);
4.2 接口实现(Heap.c)
1)初始化销毁
void HeapInit(HP* php)
{assert(php);php->a = NULL;php->capacity = php->size = 0;
}void HeapDestory(HP* php)
{assert(php);free(php->a);php->a = NULL;php->capacity = php->size = 0;
}
2)插入,时间复杂度是O(logN)
因为扩容操作只有 HeapPush
使用,因此不将其作为单独函数。
插入的过程中要保证堆符合大堆或者小堆的规则,因此,要对其进行调整,这里将引入向上调整算法,并将其作为单独函数。
向上调整算法主要利用父子下标的特性, 在数据插入成为叶子时,将其与父节点比较,直到满足堆的规则 (这里小堆要满足父节点小于等于子节点 )
void AdjustUp(HPDataType* 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;}}
}// 插入,时间复杂度是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\n");exit(-1);}php->a = tmp;php->capacity = newCapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1);
}
因为后续要重复进行交换,所以将其作为单独一个函数
void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}
3)删除,规定删除堆顶,即根节点
删除是删头,可以选出最大最小的数据,尾删没有多大意义。
有人会想到挪动数据覆盖删除根节点,但会出问题,而且数组不一定有序,挪动后整棵树的父子关系就会全乱了,并且也可能就不是堆了。
因此,这里采用的办法是首先进行首尾交换,然后尾删,这样左右子树依旧是小堆,然后再用向下调整算法,从而将堆构建完成。
void AdjustDown(HPDataType* a, int size, int parent)
{int child = parent * 2 + 1;while (child < size){if (child + 1 < size && a[child] > a[child + 1]){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(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);// 不需要进行覆盖php->size--;AdjustDown(php->a, php->size, 0);
}
4)堆顶数据、堆大小、堆空
这三个操作就相对简单,返回堆顶元素要检查堆是否为空。
HPDataType HeapTop(HP* php)
{assert(php);// 空assert(php->size > 0);return php->a[0];
}size_t HeapSize(HP* php)
{assert(php);return php->size;
}bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}
4.3 完整代码
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 HeapDestory(HP* php);
// 小堆
// 插入,时间复杂度是O(logN)
void HeapPush(HP* php, HPDataType x);
// 规定删除堆顶,即根节点
// 挪动数据覆盖删除跟会出问题
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
size_t HeapSize(HP* php);
bool HeapEmpty(HP* php);
Heap.c
#include "Heap.h"// 初始化销毁
void HeapInit(HP* php)
{assert(php);php->a = NULL;php->capacity = php->size = 0;
}void HeapDestory(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 (child > 0){if (a[child] < a[parent]){Swap(&a[child], & a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}// 插入,时间复杂度是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\n");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 size, int parent)
{int child = parent * 2 + 1;while (child < size){if (child + 1 < size && a[child] > a[child + 1]){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(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}HPDataType HeapTop(HP* php)
{assert(php);// 空assert(php->size > 0);return php->a[0];
}size_t HeapSize(HP* php)
{assert(php);return php->size;
}bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}
test.c
#include "Heap.h"int main()
{int a[] = { 2,7,0,21,56,786,1,3 };HP hp;HeapInit(&hp);for (int i = 0; i < sizeof(a) / sizeof(int); i++){HeapPush(&hp, a[i]);}int k = 7;printf("打印小堆顶前7个数据:\n");while (k--){printf("%d ", HeapTop(&hp));HeapPop(&hp);}printf("\n堆大小为:%d, 堆是否为空:%d\n", HeapSize(&hp), HeapEmpty(&hp));printf("销毁小堆!\n");HeapDestory(&hp);return 0;
}
运行效果
在学会堆的知识后,我们应该怎么应用呢?
下面将介绍两个使用的堆使用:
堆排序&TopK问题:堆的实际应用:堆排序&TopK问题