本篇博客将详细讲述二叉树的概念,堆的概念及结构以及堆的代码实现,以及二叉树,堆的相关应用。Top K 问题,堆排序的实现以及二叉树链式结构的实现将在之后的博客更新。你可在目录中找到你想重点阅读的内容。堆的完整代码实现在文章结尾处。
堆的其余文章在主页:
堆排序--TOP-K问题
堆排序的讲解
目录
一、树的概念及结构
1.1 树的概念
1.2 树的相关概念
1.3 树的表示
1.4 树在实际中的运用
二、二叉树概念及结构
2.1 概念
2.2 现实中的二叉树:
2.3 特殊的二叉树:
2.3.1 满二叉树
2.3.2 完全二叉树
2.4 二叉树的存储结构
2.4.1 顺序存储
a. 完全二叉树
a.1 完全二叉树规律:
b. 非完全二叉树
2. 链式存储(此篇省略,只说概念,后续会更新)
三、二叉树的顺序结构
3.1 二叉树的顺序结构
3.2 堆的概念及结构
a.堆的应用:
四、堆的实现
4.1 分析
4.1.1 所要实现的功能:Heap.h
4.1.2 堆的插入(向上调整)
4.1.3 堆的删除(向下调整)(堆的删除是删除根节点)
4.2 代码实现
4.2.1堆的初始化与堆的销毁
4.2.2 堆的插入
4.2.3 堆的删除
4.2.4 取堆顶的数据,堆的个数,堆的判空。
五、完整代码
1.My_Heap.h
2.My_Heap.c
3.测试用例
一、树的概念及结构
1.1 树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
根结点: 无前驱节点;
树是递归定义的:除根节点外,其余结点被分成M(M>0)个互不相交(子树之间不能有交集)的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。一颗N个结点的树有N-1条边。
拆解:一个根,n颗子树(n>=0)构成,
根A
子树:B/C/D
1.2 树的相关概念
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G...等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;(并查集就是森林)
1.3 树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。这里就了解最常用的孩子兄弟表示法。
a.孩子兄弟表示法
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
如图所示:
1.4 树在实际中的运用
最常用的例如文件目录结构
二、二叉树概念及结构
2.1 概念
一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成从下图可以看出:
1.二叉树不存在度大于2的结点
2.二叉树的子树有左右之分,次序不能颠倒,二叉树是有序树
注意:每个结点最多有两个孩子不等价于度为2的树,度为2的树 就是 二叉树。
2.2 现实中的二叉树:
2.3 特殊的二叉树:
1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
(每一层都是满的)
2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。(前h-1层是满的,最后一层不一定满,但是从左到右必须连续)
2.3.1 满二叉树
如图:假设一颗满二叉树的高度为h,第一层结点数:2^0,第二层:2^1,第三层:2^2......可得到第h层的结点数为:2^(h-1)
因此,一个满二叉树的总结点数: 2^0+2^1+2^2+...+2^(h-1) = 2^h - 1
假设有N个结点,可得到的高度为:h = log2(N+1)
2.3.2 完全二叉树
理解了完全二叉树概念之后可知道:
假设高度为h
节点的总数量区间:[2^(h-1)-1,2^h-1]
高度区间: [(log2N )+ 1,log2(N+1)](当N足够大时,高度几乎就无区别)
2.4 二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
2.4.1 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为非完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
a. 完全二叉树
a.1 完全二叉树规律:
可发现一个规律:父子节点对应下标:
leftchild = parent*2+1,rightchild = parent*2+2
再者,由于数组下标都为整数:(不区分左右孩子)
parent = (child -1)/2 (可自行代入值计算)
b. 非完全二叉树
存入数组中,必须留空,造成空间浪费,才能实现如完全二叉树的规律,不适合数组结构存储,只适合链式存储。
2. 链式存储(此篇省略,只说概念,后续会更新)
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前阶段还只能讲二叉链,后面会讲到高阶数据结构如红黑树等会用到三叉链。
三、二叉树的顺序结构
3.1 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
3.2 堆的概念及结构
注意:每棵树的根,都是这棵树的最大/小值。兄弟之间,左孩子不一定小于右孩子
堆排序的时间复杂度:O(N*log2N)
要求:
大堆:任意一个父亲 >= 孩子
小堆:任意一个父亲 <= 孩子
a.堆的应用:
例如给全国的川菜馆进行一个排名,如果有100万家店,需要排几次? 20次
b.思考几个问题:
1.有序数组一定是堆? 是的
2.堆一定有序吗? 不一定
四、堆的实现
4.1 分析
4.1.1 所要实现的功能:Heap.h
typedef int HPDataType;
typedef struct Heap
{
HPDataType* _a;
int _size;
int _capacity;
}Heap;//堆的初始化
void HeapInit(Heap* php);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
我主要针对于堆的插入与删除讲解
4.1.2 堆的插入(向上调整)
思路:将插入值,依次与他的父节点进行比较,若它的值小于(小堆)/大于(大堆),他的父节点的值,那么就进行值的交换。
仔细看图,我们需要往在这已经存在的 小堆 中插入26到数组的最后(设数组大小为size,那么26的下标:size - 1,而在这里 插入26后 size = 11,因此26的下标为 10),我们知道小堆的定义是:父节点 <= 孩子,因此对于26,我们先通过在之前找到的完全二叉树规律,可知26的父节点下标:int((10-1)/2) = 4。即对应值28, 26<28 ,交换26,28的对应下标的值,此时26对应的下标为:4 ,28对应的下标为:10。26应继续与它的父节点进行值比较,新父节点下标:1,对应值:18, 26>18,此时不用再交换,顺序正确。
知道了如何插入,那么如果插入的值比堆的所有值都小,什么情况下退出呢?
如图序号1-7,详细展现了,如果一个比堆内所有值都小的数进入堆后如何变化的图解。
看完此图后,序号7时,14已经到达堆顶,他的下标也变成了0,因此我们循环退出的条件就是下标child == 0,为什么不使用parent?parent再减就数组越界了。
4.1.3 堆的删除(向下调整)(堆的删除是删除根节点)
思路:将插入值先与堆内最后一个数值进行交换,再删除掉最后一个值。再将插入值与他的孩子节点依次值比较,若插入值大于(小堆)/ 小于(大堆)孩子节点的值时,与孩子节点的值进行交换。为什么不可挪动数据覆盖?(将size+1,从最后一个值开始依次往后挪动),这样会直接破坏整个堆结构,想要实现,时间复杂度太大。
如图:
4.2 代码实现
4.2.1堆的初始化与堆的销毁
//堆的初始化
void HeapInit(Heap* hp)
{
assert(hp);
hp->_a = NULL;
hp->_size = 0;
hp->_capacity = 0;
}
// 堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->_a);
hp->_a = NULL;
hp->_size = hp->_capacity = 0;
}
4.2.2 堆的插入
//交换子节点与父节点的值
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 = (parent - 1) / 2;//parent = (child - 1) / 2
}
else
{
break;
}
}
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
if (hp->_size == hp->_capacity)
{
int newCapacity = hp->_capacity = 0 ? 4 : hp->_capacity * 2;
HPDataType* obj = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * 2);
if (obj == NULL)
{
perror("realoc fail");
exit(-1);
}
hp->_a = obj;
hp->_capacity = newCapacity;
}hp->_a[hp->_size] = x;
hp->_size++;
AdjustUp(hp->_a, hp->_size - 1);//调整堆结构
}
4.2.3 堆的删除
void Adjustdown(HPDataType* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child <= size - 1)
{//这里可能发生越界若不写(child + 1) <= size - 1
if ((child + 1) <= size - 1 && 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* hp)
{
assert(hp);
assert(hp->_size > 0);
Swap(&hp->_a[0], &hp->_a[hp->_size - 1]);
hp->_size--;
Adjustdown(hp->_a, hp->_size , 0);//记得传入插入数据的值
}
4.2.4 取堆顶的数据,堆的个数,堆的判空。
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
assert(hp->_size > 0);
return hp->_a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->_size;}
// 堆的判空
int HeapEmpty(Heap* hp)
{
assert(hp);
return hp->_size == 0;
}
五、完整代码
1.My_Heap.h
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>
typedef int HPDataType;
typedef struct Heap
{HPDataType* _a;int _size;int _capacity;
}Heap;//堆的初始化
void HeapInit(Heap* php);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
2.My_Heap.c
#include"My_Heap.h"
//堆的初始化
void HeapInit(Heap* hp)
{assert(hp);hp->_a = NULL;hp->_size = 0;hp->_capacity = 0;
}
// 堆的销毁
void HeapDestory(Heap* hp)
{assert(hp);free(hp->_a);hp->_a = NULL;hp->_size = hp->_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 = (parent - 1) / 2;}else{break;}}
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{assert(hp);if (hp->_size == hp->_capacity){int newCapacity = hp->_capacity = 0 ? 4 : hp->_capacity * 2;HPDataType* obj = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * 2);if (obj == NULL){perror("realoc fail");exit(-1);}hp->_a = obj;hp->_capacity = newCapacity;}hp->_a[hp->_size] = x;hp->_size++;AdjustUp(hp->_a, hp->_size - 1);
}
void Adjustdown(HPDataType* a, int size, int parent)
{int child = parent * 2 + 1;while (child <= size - 1){if ((child + 1) <= size - 1 && 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* hp)
{assert(hp);assert(hp->_size > 0);Swap(&hp->_a[0], &hp->_a[hp->_size - 1]);hp->_size--;Adjustdown(hp->_a, hp->_size , 0);
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{assert(hp);assert(hp->_size > 0);return hp->_a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{assert(hp);return hp->_size;}
// 堆的判空
int HeapEmpty(Heap* hp)
{assert(hp);return hp->_size == 0;
}
3.测试用例
int main()
{int a[] = {15,18,19,25,28,34,65,49,27,37};Heap hp;HeapInit(&hp);for (int i = 0; i < sizeof(a) / sizeof(int); ++i){HeapPush(&hp, a[i]);}//int k = 3;//while (k--)//{// printf("%d\n", HeapTop(&hp));// HeapPop(&hp);//}while (!HeapEmpty(&hp)){printf("%d ", HeapTop(&hp));HeapPop(&hp);}printf("\n");return 0;
}
结语:
随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。解题的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。 你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。