目录
1. 堆概念及结构
通过上篇博文的讲解我们得知完全二叉树和满二叉树是可以通过数组来进行存储的,它们间的父子关系可以通过下标来表示。这里再强调下物理结构是是在内存当中实实在在存储的,在物理上是数组,但是在逻辑上要把它看出二叉树。
- 普通二叉树用数组可以存储吗?
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。因为空间利用率高,不会造成浪费。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。如果不是完全二叉树或满二叉树的话,推荐用链式存储。
1.1 堆的概念
堆是一个完全二叉树,它的所有元素按照完全二叉树的顺序存储方式存储在一个一维数组中。堆分为两种:小根堆、大根堆
- 小堆:每一个父结点的值均小于等于其对应的子结点的值,而根结点的值就是最小的。
- 大堆:每一个父结点的值均大于等于其对应的子结点的值,而根结点的值就是最大的。
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值。
- 堆总是一棵完全二叉树。
1.2 堆的结构
通过物理结构可以得知以下两点:
- 有序的一定是堆
- 无序的可能是堆
2.堆的实现
2.1 创建堆结构
- 思路:我们堆的结构时数组,因此创建对结构就要采用动态开辟数组的方式。
//创建堆结构 typedef int HPDataType;//定义堆中存储数据类型 typedef struct Heap { HPDataType* a;//用于存储数据 size_t size;//记录队中由多少个数据 size_t capacity;//记录堆的容量 }HP;
2.2 初始化堆
- 思路:对堆进行初始化,那么传过来的结构体指针不能为空,首先要断言。剩下的操作跟之前顺序表,栈初始化没两样。
- Heap.h
//初始化堆 void HeapInit(HP* php);
- Heap.c
//初始化堆 void HeapInit(HP* php) { assert(php); php->a = NULL; php->size = php->capacity = 0; }
2.3 打印堆
- 思路:堆的打印很简单,堆的物理结构就是数组,打印堆的实质不还是类似于先前顺序表的打印嘛,依次访问下标打印即可。
- heap.h
//堆的打印 void HeapPrint(HP* php);
- Heap.c
//堆的打印 void HeapPrint(HP* php) { assert(php); for (size_t i = 0; i < php->size; i++) { printf("%d ", php->a[i]); } printf("\n"); }
2.4 堆的销毁
- 思路:对于动态开辟的内存在使用完毕后要即使进行销毁,包括元素置0.
- Heap.h
//堆的销毁 void HeapDestroy(HP* php);
- Heap.c
//堆的销毁 void HeapDestroy(HP* php) { assert(php); free(php->a);//释放动态开辟的空间 php->a = NULL; //置空 php->size = php->capacity = 0; //置0 }
2.5 堆元素交换
- 思路:堆的交换还是比较简单的,跟之前写的没什么区别,记得传地址。
- Heap.h
//交换 void Swap(HPDataType* pa, HPDataType* pb);
- Heap.c
//交换 void Swap(HPDataType* pa, HPDataType* pb) { HPDataType tmp = *pa; *pa = *pb; *pb = tmp; }
2.6 堆向上调整算法
- 思路:此算法是为了确保插入数据后的堆依然是符合堆的性质而单独封装出来的函数。
堆插入数据实质是再数组的最后放入一个数据,次数据加入进去,还是要确保整个数组满足堆的性质,因此需要对整个数组进行调整。例如:
为了确保在插入数字10后依然是个小根堆,所以要将10和28交换,依次比较父结点parent和子结点child的大小,当父小于子结点的时候,就返回,反之就一直交换,直到根部。
由前文的得知的规律,parent = (child - 1) / 2,我们操控的是数组,但要把它想象成二叉树。
- Heap.c
//向上调整算法 void AdjustUp(HPDataType* a, size_t child) { size_t parent = (child - 1) / 2; while (child > 0) { //if (a[child] > a[parent]) //大根堆 if (a[child] < a[parent]) //小根堆 { Swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } }
2.7 堆向下调整算法
- 思路:删除堆时一般是将堆头与队尾交换,删除堆尾,再对剩余的数进行调整使其满足堆的性质。
此时我们看到,这个二叉树整体上不符合堆的性质,但是其根部的左子树和右子树均满足堆的性质。 接下来,就要进行向下调整,确保其最终是个堆。只需三步。
- 找出左右孩子中最小的那个;
- 跟父亲比较,如果比父亲小,就交换;
- 再从交换的孩子位置继续往下调整。
- Heap.c
//向下调整算法 void AdjustDown(HPDataType* a, size_t size, size_t root) { int parent = root; int child = 2 * parent + 1; while (child < size) { //1、确保child的下标对应的值最小,即取左右孩子较小那个 if (child + 1 < size && a[child + 1] < a[child]) //得确保右孩子存在 { child++; //此时右孩子小 } //2、如果孩子小于父亲则交换,并继续往下调整 if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = 2 * parent + 1; } else { break; //如果中途满足堆的性质,直接返回 } } }
2.8 堆的插入
- 注意:堆的插入不像先前顺序表一般,可以头插,任意位置插入等等,因为是堆,要符合大根堆或小根堆的性质,不能改变堆原本的结构,所以尾插才是最适合的,并且尾插后还要检查是否符合堆的性质。
- 思路:在插入之前就要先判断该堆的容量是否还够插入数据,先检查要不要扩容,扩容完毕后。我们可以发现,插入的数据只会影响到从自己本身开始到根,也就是祖先,只要这条路上符合堆的性质,插入即成功。
- 核心思想:向上调整算法。当我们看到插入的数据比父亲小时,此时交换数字,令父亲为孩子再次与父亲比较,若还小,再次交换,直到根部了。当然这是最坏的情况,如果在中间换的过程中满足了堆的性质,那么就不需要再换了,直接返回即可。这就叫向上调整算法,直接套用上面的函数即可。
- Heap.h
//堆的插入 void HeapPush(HP* php, HPDataType x);
- Heap.c
//堆的插入 void HeapPush(HP* php, HPDataType x) { assert(php); //检测是否需要扩容 if (php->size == php->capacity) { //扩容 size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2; HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity); if (tmp == NULL) { printf("realloc fail\n"); exit(-1); } php->a = tmp; php->capacity = newcapacity; } php->a[php->size] = x; php->size++; //保持继续是堆,向上调整算法 AdjustUp(php->a, php->size - 1); }
2.9 堆的删除
- 思路:在上文堆的插入中,我们明确插完依旧是堆,而这里堆的删除同样也要确保删除后依旧是堆,注意:堆的删除是删除堆顶的数据。以小根堆为例,删除堆顶的数据,也就是把最小的数据删掉,那么还要保证依旧是堆。主要思路为:
- 首先,交换第一个数据和最后一个数据
交换后,此时的堆就不符合其性质了,因为原先最后一个数据肯定是比第一个数据大的,现在最后一个数据到了堆顶,就不是堆了,但是根结点的左子树和右子树不受影响,单独看它们依旧是堆。
- 接着,--size,删除堆顶数据
因为此时堆顶的数据已经到了堆尾,只需要像顺序表那样--size,确保有效数据减1也就是确保了堆顶的删除。
- 最后,采用向下调整算法,确保其是堆结构
- 算法复杂度分析
第一个数据和最后一个数据交换是O(1),而向下调整算法的时间复杂度为O(logN),因为向下调整是调整高度次,根据结点个数N可以推出高度约为logN。
- Heap.h
//堆的删除 删除堆顶的数据 void HeapPop(HP* php);
- Heap.c
//堆的删除 删除堆顶的数据 void HeapPop(HP* php) { assert(php); assert(php->size > 0);//确保size>0 Swap(&php->a[0], &php->a[php->size - 1]); //交换堆头和堆尾 php->size--; //向下调整,确保仍然是堆结构 AdjustDown(php->a, php->size, 0); }
2.10 堆的判空
- 思想:若size为0,直接返回即可。
- Heap.h
//堆的判空 bool HeapEmpty(HP* php);
- Heap.c
//堆的判空 bool HeapEmpty(HP* php) { assert(php); return php->size == 0; //size为0即为空 }
2.11 获取堆的元素个数
- 思路:直接返回size的值
- Heap.h
//堆的元素个数 size_t HeapSize(HP* php);
- Heap.c
//堆的元素个数 size_t HeapSize(HP* php) { assert(php); return php->size; }
2.12 获取堆顶
- 思路:直接返回堆顶即可。前提是得断言size>0
- Heap.h
//获取堆顶元素 HPDataType HeapTop(HP* php);
- Heap.c
//获取堆顶元素 HPDataType HeapTop(HP* php) { assert(php); assert(php->size > 0); return php->a[0]; }
3. 总代码
3.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; //用于存储数据 size_t size; //记录堆中有效元素个数 size_t capacity; //记录堆的容量 }HP; //初始化堆 void HeapInit(HP* php); //堆的销毁 void HeapDestroy(HP* php); //堆的打印 void HeapPrint(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);
3.2 Heap.c
#define _CRT_SECURE_NO_WARNINGS 1 #include"Heap.h" //初始化堆 void HeapInit(HP* php) { assert(php); php->a = NULL; php->size = php->capacity = 0; } //堆的销毁 void HeapDestroy(HP* php) { assert(php); free(php->a); php->a = NULL;//动态开辟置空 php->size = php->capacity = 0;//置0 } //堆的打印 void HeapPrint(HP* php) { assert(php); for (size_t i = 0; i < php->size; i++) { printf("%d ", php->a[i]); } printf("\n"); } //交换 void Swap(HPDataType* pa, HPDataType* pb) { HPDataType tmp = *pa; *pa = *pb; *pb = tmp; } //向上调整算法 void AdjustUP(HPDataType* a, size_t child) { size_t 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 AdjustDown(HPDataType* a, size_t size, size_t root) { int parent = root; int child = 2 * parent + 1; while (child < size) { //1、确保child的下标对应的值最小,即取左右孩子较小那个 if (child + 1 < size && a[child + 1] < a[child]) { child++;//此时右孩子小 } //2、如果孩子小于父亲则交换,并继续往下调整 if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = 2 * parent + 1; } else { break; } } } //堆的插入 void HeapPush(HP* php, HPDataType x) { assert(php); //检测是否需要扩容 if (php->size == php->capacity) { //扩容 size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2; HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity); if (tmp == NULL) { printf("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 HeapPop(HP* php) { assert(php); assert(php->size > 0);//保证size>0 Swap(&php->a[0], &php->a[php->size - 1]);//交换堆头和堆尾 php->size--; //向下调整,确保仍然是堆结构 AdjustDown(php->a, php->size, 0); } //堆的判空 bool HeapEmpty(HP* php) { assert(php); return php->size == 0; //size为0即为空 } //堆的元素个数 size_t HeapSize(HP* php) { assert(php); return php->size; } //获取堆顶元素 HPDataType HeapTop(HP* php) { assert(php); assert(php->size > 0); return php->a[0]; }
3.3 Test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void TestHeap1()
{
HP hp;
HeapInit(&hp);
//插入数据
HeapPush(&hp, 1);
HeapPush(&hp, 5);
HeapPush(&hp, 3);
HeapPush(&hp, 0);
HeapPush(&hp, 8);
HeapPush(&hp, 9);
//打印
HeapPrint(&hp);
//销毁
HeapDestroy(&hp);
}
void TestHeap2()
{
HP hp;
HeapInit(&hp);
//插入数据
HeapPush(&hp, 1);
HeapPush(&hp, 5);
HeapPush(&hp, 3);
HeapPush(&hp, 0);
HeapPush(&hp, 8);
HeapPush(&hp, 9);
HeapPrint(&hp);//打印
//删除堆顶数据
HeapPop(&hp);
HeapPrint(&hp);//打印
//销毁
HeapDestroy(&hp);
}
int main()
{
//TestHeap1();
TestHeap2();
return 0;
}