朋友们、伙计们,我们又见面了,本期来给大家解读一下二叉树--堆的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
目录
前言:
1.堆的概念及结构
2.堆的实现
2.1创建堆
2.2初始化堆
2.3向堆中插入数据
2.4向上调整算法
2.5判断堆是否为空
2.6删除堆中的数据
2.7向下调整算法
2.8有效元素个数、堆顶元素
2.9代码测试
2.10完整代码
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
如果有一个关键码的集合K = {},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:且(i = 0,1,2,3,4,......n-1)被称为小堆或且 (i = 0,1,2,3,4,......n-1)被称为大堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。图示:简单的来讲:
1.大堆就是每一个孩子节点都要比它的父亲节点小,这只是规定了父亲节点和孩子节点之间的大小关系,并没有规定兄弟节点之间的关系,两个兄弟节点的大小不确定。
2. 小堆就是每一个孩子节点都要比它的父亲节点大,这只是规定了父亲节点和孩子节点之间的大小关系,并没有规定兄弟节点之间的关系,两个兄弟节点的大小不确定。
因此堆并不一定是有序的。
堆的性质:
堆是一种特殊的完全二叉树,因此实现堆的思路就是使用顺序表,同样的我们也是分模块来写,创建头文件:Heap.h、源文件:Heap.c和Test.c,堆在这里主要实现的接口是:初始化、插入,删除(删除堆顶元素)、判空、有效元素个数、堆顶元素、销毁。话不多说我们直接开始:
注意:这里实现的是以小堆为例
堆的创建就是创建一个顺序表,然后再进行堆的一系列操作:
头文件:Heap.h
//创建堆 //顺序表 typedef int HPDataType; typedef struct Heap { HPDataType* a; int size = 0; //有效元素个数 int capacity; //容量 }Heap;
这些接口实现都比较简单,直接上手即可:
头文件:Heap.h
//初始化 void HeapInit(Heap* php);
源文件:Heap.c
//初始化 void HeapInit(Heap* php) { php->a = NULL; php->size = 0; php->capacity = 0; }
插入堆在这里插入的是堆的最后面,对应的也就是顺序表的最后一个位置,当插入进去之后还面临一个问题:我们创建的就是小堆,那么插入的数据如果小于它的父亲,就需要向上调整,如果调整之后还小于它新的父亲的,那么还是得继续向上调整,直到符合小根堆的逻辑
因此插入过程可以分为这么几步:
1. 先为堆开辟一块空间。
2. 检测容量,不够还需扩容。
3. 插入,判断是否需要进行向上调整。
头文件:Heap.h
//插入 void HeapPush(Heap* php, HPDataType x);
源文件:Heap.c
//插入 void HeapPush(Heap* php, HPDataType x) { assert(php); //插入数据的过程 //容量不够则需要扩容 //判断空间是否不足 if (php->size == php->capacity) { int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity; //为堆开辟空间 HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity); if (tmp == NULL) { perror("realloc fail"); return; } php->capacity = newcapacity; php->a = tmp; } //插入数据 php->a[php->size] = x; php->size++; //向上调整 AdjustUp(php->a, php->size - 1); }
向上调整算法我们再来分装一个函数
向上调整我们的第一步首先要找它的父亲节点(在顺序表的逻辑下:如果它是左孩子,那么它的父亲节点的下标就是它的下标-1然后除2,如果它是右孩子,那么它的父亲节点的下标就是它的下标-2然后除2,但是呢?我们进行的是整数除法,没有余数,因此无论是左孩子还是右孩子都是使用-1除2的方法进行计算)。第二步判断孩子是否小于父亲,小于的话孩子和父亲就要交换位置,大于的话表示符合小堆逻辑,直接跳出即可,然后再让交换后的父亲节点变为新的孩子节点,然后再求新的父亲节点,依次类推,直到调整到堆顶,堆顶元素的下标为0,那么就表示孩子节点的下标为0时就不需要再调整了,退出即可:
因此孩子与父亲之间的关系可以总结为:
int Child = Parent*2+1; //左孩子 int Child = Parent*2+2; //右孩子 int Parent = (Child-1)/2; //父亲
源文件:Heap.c
//交换函数 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; } } }
堆为空的条件就是堆中的有效元素个数为0。
头文件:Heap.h
//判空 bool HeapEmpty(Heap* php);
源文件:Heap.c
//判空 bool HeapEmpty(Heap* php) { assert(php); return php->size == 0; }
删除堆元素时并不是删除最后一个元素,这样删除并没有什么意义,因此删除堆中的元素时删除的是堆顶的元素,那么这里就有两种方法:
1. 和顺序表的头删一样,直接挪动数据进行覆盖,但是这样做时间复杂度高,而且挪动数据本来就是一件代价比较大的事情,再加上如果挪动数据进行覆盖的话就会将原本的父子关系打乱,父子关系打乱就有点扯淡了。
2. 将堆顶的数据和最后一个数据交换,然后删除掉最后一个数据,这时将剩下的元素进行向下调整。
所以删除数据可以分为这几步:
1. 先判断堆是否为空,为空就不能再删除。
2. 再交换顶部和最后的数据。
3. 再将最后一个元素删除。
4. 再判断是否需要向下调整。
头文件:Heap.h
//删除 void HeapPop(Heap* php);
源文件:Heap.c
void HeapPop(Heap* php) { assert(php); //判断堆是否为空 assert(!HeapEmpty(php)); //交换堆顶的数据和最后的数据 Swap(&php->a[0], &php->a[php->size - 1]); //删除最后的数据 php->size--; //向下调整 AdjustDown(php->a, php->size, 0); }
向下调整算法我们再来分装一个函数
向下调整算法首先我们需要根据孩子与父亲的下标关系找到堆顶元素的左右孩子,然后找出左右孩子中最小的孩子,然后将其交换,然后再将孩子的位置更新为父节点,再继续找新的父亲节点的左右孩子的最小的节点,再进行交换,依次类推,直到交换到最后一个孩子节点(孩子节点小于等于总结点个数)。
找左右孩子中的最小的我们可以使用假设法,假设左孩子为最小的,然后进行判断,如果不合适,就给左孩子加1,从而得到右孩子,这里还需要注意,如果没有右孩子呢?那就只能和左孩子比较,也就不需要进行比较了(左孩子加一小于等于总结点个数)。
所以我们在设置向下调整算法的时候需要传堆、元素个数、堆顶的下标。
源文件:Heap.c
//向下调整 void AdjustDown(HPDataType* a, int n, int parent) { //假设左孩子为左右孩子中最小的节点 int child = parent * 2 + 1; while (child < n) //当交换到最后一个孩子节点就停止 { if ( child + 1 < n //判断是否存在右孩子 && a[child + 1] < a[child]) //判断假设的左孩子是否为最小的孩子 { child++; //若不符合就转化为右孩子 } //判断孩子和父亲的大小关系 if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); //更新父亲和孩子节点 parent = child; child = parent * 2 + 1; } else { break; } } }
注意:
1. 这里的向上调整算法和向下调整算法只适用于堆的操作。
2. 向上调整算法和向下调整算法的时间复杂度是O(logN)。
向上调整和向下调整的最好情况是一次都不调整,最坏的情况是调整一个完全二叉树的高度次,又因为完全二叉树的结点个数为一个范围[ 2^(h-1), 2^h-1]。所以将高度于h于结点个数N结合在一起可以算出高度为[ logN+1,log(N+1)],因此算法的时间复杂度为O(logN)。
1. 返回堆顶元素时先要判断堆是否为空,堆顶元素的下标就是0,因此直接返回下标为0的元素。
2. 有效元素的个数就是顺序表中的size,直接返回即可。
头文件:Heap.h
//有效元素个数 int HeapSize(Heap* php); //获取堆顶元素 HPDataType HeapTop(Heap* php);
源文件:Heap.c
//有效元素个数 int HeapSize(Heap* php) { assert(php); return php->size; } //获取堆顶元素 HPDataType HeapTop(Heap* php) { assert(php); //判断堆是否为空 assert(!HeapEmpty(php)); return php->a[0]; }
插入测试:
写完了堆的基本接口之后我们可以调试通过监视窗口来观察一下,我们将一个数据依次插入到堆中,观察他们的变化:
源文件:Test.c
#include "Heap.h" void HeapTest() { Heap hp; //初始化 HeapInit(&hp); //插入 int arr[] = { 8,5,6,2,1,0,3,4,7,9 }; for (int i = 0; i < sizeof(arr) / sizeof(int); i++) { HeapPush(&hp, arr[i]); } } int main() { HeapTest(); return 0; }
删除测试:
#include "Heap.h" void HeapTest() { Heap hp; //初始化 HeapInit(&hp); //插入 int arr[] = { 8,5,6,2,1,0,3,4,7,9 }; for (int i = 0; i < sizeof(arr) / sizeof(int); i++) { HeapPush(&hp, arr[i]); } //删除 HeapPop(&hp); } int main() { HeapTest(); return 0; }
头文件:Heap.h
#pragma once // 堆 #include
#include #include #include //创建堆 //顺序表 typedef int HPDataType; typedef struct Heap { HPDataType* a; int size; //有效元素个数 int capacity; //容量 }Heap; //初始化 void HeapInit(Heap* php); //销毁 void HeapDestroy(Heap* php); //插入 void HeapPush(Heap* php, HPDataType x); //删除 void HeapPop(Heap* php); //判空 bool HeapEmpty(Heap* php); //有效元素个数 int HeapSize(Heap* php); //获取堆顶元素 HPDataType HeapTop(Heap* php);
源文件:Heap.c
#define _CRT_SECURE_NO_WARNINGS 1 #include "Heap.h" //初始化 void HeapInit(Heap* php) { php->a = NULL; php->size = 0; php->capacity = 0; } //销毁 void HeapDestroy(Heap* php) { free(php->a); php->a = NULL; php->size = php->capacity = 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; } } } //插入 void HeapPush(Heap* php, HPDataType x) { assert(php); //插入数据的过程 //容量不够则需要扩容 //判断空间是否不足 if (php->size == php->capacity) { int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity; //为堆开辟空间 HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity); if (tmp == NULL) { perror("realloc fail"); return; } php->capacity = newcapacity; php->a = tmp; } //插入数据 php->a[php->size] = x; php->size++; //向上调整 AdjustUp(php->a, php->size - 1); } //向下调整 void AdjustDown(HPDataType* a, int n, int parent) { //假设左孩子为左右孩子中最小的节点 int child = parent * 2 + 1; while (child < n) //当交换到最后一个孩子节点就停止 { if ( child + 1 < n //判断是否存在右孩子 && a[child + 1] < a[child]) //判断假设的左孩子是否为最小的孩子 { child++; //若不符合就转化为右孩子 } //判断孩子和父亲的大小关系 if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); //更新父亲和孩子节点 parent = child; child = parent * 2 + 1; } else { break; } } } //删除 void HeapPop(Heap* php) { assert(php); //判断堆是否为空 assert(!HeapEmpty(php)); //交换堆顶的数据和最后的数据 Swap(&php->a[0], &php->a[php->size - 1]); //删除最后的数据 php->size--; //向下调整 AdjustDown(php->a, php->size, 0); } //判空 bool HeapEmpty(Heap* php) { assert(php); return php->size == 0; } //有效元素个数 int HeapSize(Heap* php) { assert(php); return php->size; } //获取堆顶元素 HPDataType HeapTop(Heap* php) { assert(php); //判断堆是否为空 assert(!HeapEmpty(php)); return php->a[0]; }
源文件:Test.c
#define _CRT_SECURE_NO_WARNINGS 1 #include "Heap.h" void HeapTest() { Heap hp; //初始化 HeapInit(&hp); //插入 int arr[] = { 8,5,6,2,1,0,3,4,7,9 }; for (int i = 0; i < sizeof(arr) / sizeof(int); i++) { HeapPush(&hp, arr[i]); } //删除 //HeapPop(&hp); } int main() { HeapTest(); return 0; }
朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!