C语言堆与二叉树的顺序结构与实现
一. 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
二. 堆的概念及结构
如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: = 且 >= ) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值
- 堆总是一棵完全二叉树
三. 堆的实现
将要实现的接口及堆的创建:
(由于堆的特点,这里使用数组结构实现)
//将要实现的是大堆 typedef int HPDataType; //创建堆结构体 typedef struct Heap { HPDataType* arr;//数组存储 size_t capacity;//容量 size_t size;//堆里的元素个数 }HP; //初始化堆 void HeapInit(HP* php); //销毁堆 void HeapDestroy(HP* php); //交换 void swap(HPDataType* pa, HPDataType* pb); //插入堆,后面插入 void HeapPush(HP* php, HPDataType x); //删除堆元素,第一个元素 void HeapPop(HP* php); //判空 bool HeapEmpty(HP* php); //堆的大小 size_t HeapSize(HP* php); //返回堆顶元素 HPDataType HeapTop(HP* php);
堆的初始化
void HeapInit(HP* php) { assert(php);//堆必须存在 php->arr = NULL; php->capacity = php->size = 0; }
堆的销毁
void HeapDestroy(HP* php) { assert(php); //销毁数组 free(php->arr); php->arr = NULL; php->capacity = php->size = 0; }
交换函数
void swap(HPDataType* pa, HPDataType* pb) { HPDataType temp = *pa; *pa = *pb; *pb = temp; }
堆的插入
这里的插入是在堆的最后面插入,堆不能任意位置插入会破坏堆的结构,这里最后面插入也会面临一个问题,插入必须还是大堆,那就要使用向上调整法
接下来插入一个70,由于70最大,所以会使用到向上调整法,如下图:
将新插入进来的元素与父节点对比,如果父节点小于子节点,就交换,依次往下进行,否则就不用交换,终止向上调整
//向上调整法 void AdjustUp(HPDataType* arr, size_t child) { //父节点 HPDataType parent = (child - 1) / 2; //交换 while (child > 0)//用child控制,parent会死循环 { //如果父节点更大,说明需要更换 if (arr[parent] < arr[child]) { swap(&arr[parent], &arr[child]); } //孩子和父亲都往上走 child = parent; parent = (child - 1) / 2; } } void HeapPush(HP* php, HPDataType x) { assert(php); //扩容 if (php->size == php->capacity) { size_t newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity; HPDataType* temp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * newcapacity); assert(temp); php->arr = temp; php->capacity = newcapacity; } php->arr[php->size] = x; ++php->size; //需要将孩子穿过去,注意size是在最后一个位置的后一个位置 AdjustUp(php->arr, php->size-1); }
堆的删除
堆的删除删除的是堆顶的元素,但是不能盲目的将第一个元素删除,然后将后面的元素往前覆盖,因为当数组里的元素没有顺序时,就会破坏了堆的结构,所以这里需要用到向下调整算法
在插入的基础上,删除掉堆顶的数,也就是70,此时就要使用到向下调整法,如下图:
因为我们删除的是堆顶的元素,所以可以这样
先将堆顶元素和最后一个元素进行交换,再删除交换后的堆尾的元素,此时的堆顶元素因为不知道大小,所以将其和自己的两个孩子中最大的比较,如果堆顶的元素小就交换,依次往下进行,否则就不交换,结束向下调整
void AdjustDown(HPDataType* arr, size_t parent, size_t size) { //假设左孩子最小 HPDataType child = (parent * 2) + 1; //使用child控制,parent会越界 while (child < size) { //如果右孩子更小则让右孩子去比较,注意右孩子是否存在 if (arr[child] < arr[child + 1] && child + 1 < size) { ++child; } //将父亲和孩子比较,父亲更大则交换 if (arr[parent] < arr[child]) { swap(&arr[parent], &arr[child]); parent = child; child = (parent * 2) - 1; } //当出现父亲小于孩子时,说明不用往下遍历了 else { break; } } } void HeapPop(HP* php) { assert(php); //堆不能为空 assert(php->size > 0); //首尾元素的交换 HPDataType temp = php->arr[0]; php->arr[0] = php->arr[php->size - 1]; php->arr[php->size - 1] = temp; //删除交换后的尾元素 --php->size; //向下调整 AdjustDown(php->arr, 0, php->size); }
判空堆是否为空
bool HeapEmpty(HP* php) { assert(php); return php->size == 0; }
返回堆的大小
size_t HeapSize(HP* php) { assert(php); return php->size; }
返回堆顶元素
HPDataType HeapTop(HP* php) { assert(php); //得要有元素 assert(php->size > 0); return php->arr[0]; }
四. 堆排序(具有缺陷型)
利用以上接口实现堆排序(具有缺陷),具有O(n)的空间复杂度
int main() { int arr[] = { 2,5,6,4,54,23,1,45,67,98 }; int size = sizeof(arr) / sizeof(arr[0]; HP hp;//创建堆 HeapInit(&hp);//初始化 //堆插入元素,时间复杂度为O(nlogn),空墨盒加墨复杂度O(n) for (int i = 0; i < size; i++) { HeapPush(&hp, arr[i]); } //堆排序,时间复杂度为O(nlogn) while (!HeapEmpty(&hp)) { printf("%d ", HeapTop(&hp));//拿堆顶元素 HeapPop(&hp);//删除堆顶元素 } //使用完销毁堆 HeapDestroy(&hp); return 0; }