[数据结构]树、堆、二叉树内容的详解与分析
前言
相信各位只要接触过计算机这门学科,无论是否有学到过二叉树的内容,但我们总是或多或少听说过二叉树这一话题,或许是来自学长学姐的吐槽它的难理解,又或许是来自老师的重复讲解可任听不懂,又或者正通过这篇文章了解到二叉树;下面是我对二叉树内容学习的个人理解与分析,希望对你有所帮助。
一、树的结构与概念
1.1树的概念
①概念:
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
②有一个特殊的结点,称为根结点,根节点没有前驱结点
③除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
④通过上面的三条相关概念的讲解,我们可以初步知道树可以划分为三个部分,分别是根、左子树、右子树,而我们进一步分析,任一个子树好像又可以按照上述的三个部分进行划分,即子树也含有根、左子树、右子树,那么我们对树的定义时,我们可以采用递归进行定义
1.2树的相关概念
a、节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6;
b、叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点;
c、非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点;
d、双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节
点;
e、孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点;
f、兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点;
g、树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6;
h、节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
i、树的高度或深度:树中节点的最大层次; 如上图:树的高度为4;
j、堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点;
k、节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先;
l、子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙;
m、森林:由m(m>0)棵互不相交的树的集合称为森林;
上述这些定义我们只需要对其简单理解即可,我们在后续的讲解中并不会全部使用
1.3树的表示
①树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
代码如下:
typedef int DataType;
struct Node
{
struct Node* _firstChild1;//第一个孩子的节点
struct Node* _pNextBrother;//指向其下一个兄弟节点
DataType _data;//节点中的数据域
}
②说明:
上述代码中的“struct Node* _pNextBrother”,这里我们说的"指向其下一个兄弟节点",这个兄弟节点是从左开始向右结束,并且拥有同一个父节点
如图:
如:这里的D指向的E和E指向的F,它们之间都拥有同一个父节点;
1.4树的实际应用
在我们的生活中与树结构相似的场景是较为常见的,比如我们使用的电脑中的目录树结构
二、二叉树的概念与结构
2.1二叉树的概念
①概念:
一棵二叉树是结点的一个有限集合,并且该集合有两个特点:
a、或者为空(空树)
b、有一个根节点加上两颗分别称为左子树和右子树的二叉树组成
②特点:
a、二叉树中不存在度大于2的节点
b、二叉树的子树有左右之分,次序不能颠倒,也因此二叉树是有序树
2.2特殊的二叉树
根据上面我的讲述,相信大家对二叉树有了一个简单的了解,但我们知道,任何事务的存在都有特殊的情况,二叉树也不列外,我们根据一个二叉树的叶节点的个数将二叉树进行了一个分类,又或者叫特殊的二叉树
①满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
②完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
上面的定义可能第一次阅读时不好理解,下面是我们的满二叉树和完全二叉树的举例图解,便于大家理解
2.3二叉树的性质
注意:二叉树的性质是根据经验与分析所得
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点.
- 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h -1
- 对任何一棵二叉树, 如果度为0其叶结点个数为n0, 度为2的分支结点个数为n2 ,则有n0 = n2 + 1
- 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h = log2(n+1)(PS:log2(n+1)是log以2为底,n+1为对数)
- 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
①. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
比特科技
②. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
③. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
2.4二叉树的存储
二叉树的存储一般使用两种结构进行存储,一种是顺序结构存储,一种是链式结构存储
2.4.1顺序存储
①定义:
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
②图解:
2.4.2链式存储
①定义:
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前讲解的是二叉链,在更高级的数据结构中会用到三叉链。
②图解:
三、二叉树的顺序结构及实现
3.1二叉树的顺序结构
①普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。
②现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
3.2堆的概念与分类
①概念:
②分类:
小堆:在树中,所有的父亲节点都小于孩子节点,其中根节点最小
注意:所有的父亲节点是指根节点和所有左子树和右子树中的父亲节点
大堆:在树中,所以的父亲节点都大于孩子节点,其中根节点最大
3.3堆的实现
下面我们将实现堆的各种接口,这是我们建立的堆的结构体:
typedef int HPDataTpye;
typedef struct Heap
{
HPDataTpye* a;//指向数组的指针
int size;//记录当前数组中存储元素的个数
int capacity;//数组的最大存储能力
}HP;
3.3.1堆的向下调整算法
①引言:
现在我们给一个数组随机进行赋值,最终我们得到的数组内容为:
arr=[27,15,19,18,28,34,65,49,25,37]
而此时我们可以通过这里的“向下调整算法”实现堆的建立,可我们真的可以对任意的数组通过这个“向下调整算法”实现堆的建立吗?
②引言回答:
答案是否定的,这里我们对这个随机数组进行分析,其实我们可以发现根节点下的左右子树均为小堆
③堆的向下调整算法的使用条件:根的左右子树均为堆,并且左右子树的堆的类型相同
④代码:
void Swap(int * px, int * py)//交换函数
{
int tmp = *px;
*px = *py;
*py = tmp;
}
//向下调整,前提是左右子树都是小堆或者大堆
//因为上述随机数组中,左右子树均为小堆,所以我们建立小堆
void AdjustDown(int* a, int n, int parent)//a是我们的数组;n是我们数组的元素个数;parent是我们根节点的坐标
{
int child = parent * 2 + 1;//这里我们先默认小孩子指向左孩子;
while (child < n)
{
if (a[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;
}
}
}
⑤代码部分语句解析:
int child = parent * 2 + 1;
这里是我们上面讲述的“2.3二叉树的性质”,在树中,父子之间的数组坐标之间的关系为:
①leftchild= parent*2 + 1;②rightchild = parent* 2 + 2;
③parent = (child-1)/2
while (child < n)
{
…………
}
这里的判读条件为child<n是因为当我们在建立小堆时,我们不断将parent与child进行比较交换,但最终是要停止的,而这个停止的条件是当我们的parent存在的时候,我们的child不存在,那么这个时候我们就不能在进行比较交换,则当child = parent* 2 + 1 不存在时,停下来,而这个不存在的条件“当我们的child这个节点不存”翻译成另一种话术就是“我们的child的下标超出了物理结构的数组范围,即child < n”
⑥代码分析与检验:
上述我们的代码中,我们建立的结果是将这个数组的内容在逻辑结构上建立成小堆,这是因为我们给出的案例是两个左右子树均为小堆,所以我们在写代码时的目的就是建立小堆,但注意“堆的向下调整算法”并非只能建立小堆,当我们的“随机”数组中的左右子树均为大堆时,那么我们只要将代码中的“<”替换成“>”即可,这样当我们的数组通过代码后即建立成大堆。
代码如下:
int a[] = { 12, 45, 36, 32, 21, 34, 20 };
此时我们的随机数组的根的左右子树均为大堆,那么我们只需要对堆的向下调整算法改进即可建立大堆
void AdjustDown(int* 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;
}
}
}
上述就是建立大堆的“堆的向下调整算法”
#include<stdio.h>
void Swap(int * px, int * py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
void AdjustDown(int* 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;
}
}
}
int main()
{
int a[] = { 12, 45, 36, 32, 21, 34, 20 };
AdjustDown(a, sizeof(a) / sizeof(int), 0);
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
printf("%d ", a[i]);
}
return 0;
}
图解:
3.3.2堆的创建
①问题:
上面我们讲解了“堆的向下调整算法”我们知道了,当我们的数组中的左右子树均为大/小堆时我们可以通过上述代码实现堆的建立,可这种情况还是过于特殊,那么如果一个数组是一个随机数组,数组中不包含任何大、小堆,那我们应该怎么解决这个问题,实现堆的建立呢?
②解答:
上面我们在进行“堆的向下调整算法”时,我们是将根节点与左子树的根节点进行比较,然后进行向下调整交换,然后递归进行上述内容,让左子树的根节点与它的左子树的根节点进行比较,直至全部节点都进行比较结束。
那么我们可以尝试换一种思考方式,堆的向下调整算法是从上向下进行比较交换,那么我们能否从叶节点开始与其父节点进行比较,然后每一个子树进行比较交换,然后再递归进行直至所以子树中孩子节点都与父亲节点进行了比较。
③图解:
这里我们执行的是建立大堆的"堆的向下调整算法"
④代码:
void AdjustDown(int* 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 HeapSort(int * a, int n)//将随机数组建立成堆
{
for (int i = (n - 1 - 1) / 2; i >= 0;--i)
{
AdjustDown(a, n, i);
}
}
⑤代码分析与检验:
#include<stdio.h>
void Swap(int * px, int * py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
void AdjustDown(int* 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 HeapSort(int * a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0;--i)
{
AdjustDown(a, n, i);
}
}
int main()
{
int a[] = { 0, 69, 36, 12, 21, 22, 64, 25, 49, 15 };
HeapSort(a, 10);
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
printf("%d ", a[i]);
}
return 0;
}
图解:
3.3.3建堆的时间复杂度(了解)
3.3.4堆的插入
①问题:
我们在前面学习顺序表和链表时,我们经常实现对其进行元素的插入 ,删除等操作,在堆中任然需要实现这些功能,但和顺序表和链表不同的是,当我们在堆中插入元素时,我们任然需要确保堆在插入元素之后堆任然成立
②分析:
我们想要实现堆的插入,同时又要保证堆的成立,如果我们空想的话很难有所思路,这个时候我们画图进行分析
③代码:
//前提:我们原有的堆为大堆
void AdjustUp(int* 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(HP* php, HPDataTpye x)
{
assert(php);
if (php->size == php->capacity)
{
HPDataTpye* tmp = (HPDataTpye*)realloc(php->a, php->capacity * 2 * sizeof(HPDataTpye));
if (php->a == NULL)
{
printf("realloc fail\n");
exit(-1);
}
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a,php->size - 1);
}//插入元素;这里不再是链表的情况,头插尾插,这里我们只需要保证插入之后任然是堆;
3.3.5堆的删除
①问题:
我们在前面学习顺序表和链表时,我们经常实现对其进行元素的插入 ,删除等操作,在堆中任然需要实现这些功能,但和顺序表和链表不同的是,我们在堆中的删除元素只有删除头节点才有意义,如果我们删除的是堆中的最后一个元素太过简单,只需要将数组的计数变量减一即可实现,所以我们要实现的是删除堆中的根节点,删除之后我们要保证堆的成立。
②分析:
如果我们是直接删除根节点的话,引出的问题就是我们相当于原本的堆变成了一个随机数组,那么我们就需要重复建堆的步骤,这样的话我们的时间复杂度太高,所以我们需要换一种思路。
③代码:
void HeapPop(HP* php)//删除堆顶的数据,删除之后任然保持是堆;
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
--php->size;
AdjustDown(php->a, php->size, 0);
}
3.3.6堆的销毁
后面几个接口的实现较为简单,就不在进行过多描述
void HeapDestory(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
3.3.7堆的数据个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}//判断当前堆中有多少数据;
3.3.8堆的判空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}//判断堆是否为空;
3.4堆的排序
①引言:
通过上面的讲解,我们已经可以实现对堆的一系列操作,比如堆的建立,堆的向下调整,堆的删除,堆的添加等;这个时候我们回顾一下堆的本质是什么?或者说树的本质是什么?没错,是数组,我们上面画出的那些二叉树的图片都是我们从逻辑结构上得出的,然而本质上这些节点的存储在内存中就是在一个数组中有序排列而已。既然我们知道本质是数组,那么在数组中就绕不开一个话题,就是数组的排序,在这里就是堆的排序,那么我们如何实现堆的排序呢?
②分析:
既然我们要实现堆的排序,这里我以建立升序为例进行讲解,如果我们想要建立一个升序那么我们应该建立一个什么样的堆呢?大堆还是小堆?这里我们进行一一讲解:
③代码:
void Swap(int * px, int * py)//交换函数
{
int tmp = *px;
*px = *py;
*py = tmp;
}
//堆排序:效率更高
void HeapSort(int * a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0;--i)
{
AdjustDown(a, n, i);
}
//当我们用大堆进行升序时,我们每找到一个最大值,我们将将其后移至数组最后的位置;
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);//下一步,我们要将交换后的数组的最后一个元素不在看为数组中的元素;
AdjustDown(a, end, 0);
--end;
}
}
④结论:
排升序,建大堆;排降序,建小堆;
3.4堆中的topk问题
①引言:
我们在打王者荣耀或者LOL这样的电子竞技游戏中,都有一个排名系统,比如:国服第一XXXX、XX省第一XX等等;而这些的排名的出现其实就是堆中的topk问题的实际应用,此时我们的电脑就需要处理,将那么多的玩家中选出前K名玩家进行排序,也就是获取前K名玩家的数据。
②分析:
我们从堆的角度进行分析,我们知道这k个数据是这个堆中的最大的前k个数据,那么就看建立一个k个数的小堆;
注意这里我们建立的是k个数的小堆,为什么我们建立的是小堆,而不是大堆呢?这是因为当我们建立的是小堆时,我们最先获取的是堆顶数据,也就是当前堆中的最小的数
若果当前我们的k个数的小堆已经存储结束,前k个数据已经完全进入,那么我们将堆顶的数据与余下的数据进行比较时,是永远小于我们的堆顶数据的,而若果此时余下的数据大于我们当前的堆顶数据,那么就说明前k个数据还有数据没有进入我们的小堆,那么我们就可以先进行堆顶数据的删除,然后在将这个大于堆顶的数据插入,这样我们就实现了将原本前k的数据插入我们的小堆。
③代码:
void HeapInit(HP* php, HPDataTpye* a, int n)//这里的HPDataTpye* a是一个数组指针,就是我们给了一个已知的数组,然后用这个数组对另一个数组(堆)进行初始化
{
assert(php);
php->a = (HPDataTpye*)malloc(sizeof(HPDataTpye)*n);
if (php->a == NULL)
{
printf("malloc fail\n");
exit(-1);
}
memcpy(php->a, a, sizeof(HPDataTpye)*n);//从a的位置开始,向后sizeof……那么多个空间的内容拷贝到php->a中
//建堆:大堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
//AdjustDown(a, n, i);这里我们在写的时候出了问题,这里我们传参的a是我们要对结构体中的数组进行初始化的模板数组a,并非我们堆中被初始化的数组:
AdjustDown(php->a, n, i);
}
php->size = n;
php->capacity = n;
}//初始化,但给了一个数组,用于初始化
void PrintTopK(int* a,int n,int k)//topk问题
{
HP hp;
HeapInit(&hp,a,k);//将堆进行初始化
for(int i = k;i<n;i++)
{
HeapPop(&hp);
HeapPush(&hp,a[i]);
}
HeapPrint(&hp);//将堆的内容进行打印
HeapDestory(&hp);//将堆进行销毁
}
四、二叉树的链式结构及实现
4.1前置说明
在讲解二叉树的基本操作前,需先要创建一棵二叉树,然后才能讲解其相关的基本操作。由于我们现在不知道二叉树的基本操作,所以此处手动创建一棵简单的二叉树,便于对内容的讲解。
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
}BTNode;
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->_left = node2;
node1->_right = node4;
node2->_left = node3;
node4->_left = node5;
node4->_right = node6;
return node1;
}
4.2二叉树的遍历
①前言:
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
②分类:
③举例:
④代码:
前序遍历:
void PreOrder(BTNode* root)//前序遍历
{
if (root == NULL)
{
printf("NULL");
return;
}
printf("%c ", root->data);
return PreOrder(root->left);
return PreOrder(root->right);
}
图解:
递归画图理解(建议放大观看)
中序遍历与后序遍历与前序遍历相似,建议自己画一下上面的递归画图,更加便于理解
中序遍历:
void InOrder(BTNode* root)//中序遍历
{
if (root == NULL)
{
printf("NULL");
return;
}
return PreOrder(root->left);
printf("%c ", root->data);
return PreOrder(root->right);
}
后序遍历:
void PostOrder(BTNode* root)//后序遍历
{
if (root == NULL)
{
printf("NULL");
return;
}
return PreOrder(root->left);
return PreOrder(root->right);
printf("%c ", root->data);
}
4.3二叉树的常见接口
引言:
首先我们要有一种分而治之的思想,这个思想遍历我们的二叉树接口的实现,我们举个例子:
放假返校时,学习要统计学校的返校人数,那么这个时候校长找到各个学院的院长,问他们要各个学院的返校人数;这个时候各个院长找到辅导员,问学院的各个班的返校人数;再接着辅导员找到各个班的班长,问各个班的返校人数;再接着班长找到各个宿舍长,问各个宿舍的返校人数;这个时候各个宿舍长开始反馈宿舍的返校人数,就这样逐一反馈,最终实现全校的返校人数。
这里我们发现,返校人数的统计问题从一开始的全校返校人数逐一化简,最终化简到不能再化简,化简到各个宿舍的返校人数,这个时候我们开始反馈宿舍的返校人数,然后逐一向上一级反馈,获得最终返校人数。
下面我们的各个接口的实现大多数都需要上面的思想,一定要将问题简化到不能再进行简化
4.3.1二叉树的节点个数
①分析:
我们要获取一个二叉树的节点个数,这个时候我们按照引言中获取的思考方式,那么我们要活动二叉树的节点个数,那么我们就要先获得根节点的左子树和右子树的节点个数,而在这两个左子树和右子树中,我们又要分别获得这个两个子树的左子树和右子树的节点个数,如此重复。
②代码:
实现一:
int size = 0;
void BinaryTreeSize(BTNode* root)//这里我们采取了全局变量size用来记录节点的个数,如果我们函数内定义int size = 0;那么这样的话,我们在遍历的时候,我们就会导致size又为0
{
if (root == NULL)
return;
else
size++;
BinaryTreeSize(root->left);
BinaryTreeSize(root->right);
}
实现二:
void BinaryTreeSize(BTNode* root,int* psize)//这里我们采取了全局变量size用来记录节点的个数,如果我们函数内定义int size = 0;那么这样的话,我们在遍历的时候,我们就会导致size又为0
{
if (root == NULL)
return;
else
++*psize;
BinaryTreeSize(root->left,psize);
BinaryTreeSize(root->right,psize);
}
实现三:
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
4.3.2二叉树叶子节点的个数
①分析:
1.我们首先要对叶子结点的特征进行描述,叶子节点的左右孩子节点均为NULL;
2.当我们的节点地址为NULL时,那么这个节点不存在,那么其计数为0;
3.分而治之的思想,将问题不断化简;
②代码:
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
else if (root->left == NULL && root->right == NULL)
return 1;
else
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
4.3.3二叉树第k层节点个数
①分析:
②代码:
int BinaryTreeLevelkSize(BTNode* root)
{
if (root == NULL)
return 0;
int leftDepth = BinaryTreeLevelkSize(root->left);
int rightDepth = BinaryTreeLevelKsize(root->right);
return leftDepth > rightDepth ? 1 + leftDepth : 1 + rightDepth;
}
4.3.4二叉树查找值为x的节点
①分析:
1.先判断是否是当前节点,如果是就即刻返回;
2.如果当前节点不是,那么就去其左子树中进行寻找判断,如果是就即刻返回;
3.若左子树没有寻找到,那么我们就跳转到右子树中寻找;
②代码:
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* retLeft = BinaryTreeFind(root->left, x);//这里体现的就是分而治之的思想,这个当前节点要么是要么不是,那么这个时候我们通过不断对新的节点进行判断,然后返回判断值,直到左子树节点判断完,然后再进行右子树节点的判断;
if (retLeft)
{
return retLeft;
}
BTNode* retRight = BinaryTreeFind(root->right, x);
if (retRight)
{
return retRight;
}
}
总结
以上就是我对树、堆、二叉树内容的个人理解,后续我还会对其进一步讲解,包括练习与更深层次的接口等内容
上述内容如果有错误的地方,还麻烦各位大佬指教【膜拜各位了】【膜拜各位了】