大家好啊✨
先简单介绍一下自己
本人目前大一在读,专业是计算机科学与技术。
写博客的目的是督促自己记好每一章节的笔记,同时也希望结交更多同仁,大家互相监督,一起进步!☀️


那么今天,就开始数据结构第三讲的学习—-二叉树的初级和进阶
注意,本篇文章并不会介绍二叉树的增删查改,因为它在实际案例中意义不大。其次,本篇文章并不会讲二叉树的链式存储,该内容会在后面的文章中体现。
但是,只要把本篇文章理解透彻,应对学校里的相关考试题目是没有问题的!
如果各位同仁觉得本篇文章还不错的话,不妨先收藏起来,以后也好复习,也当做是给我的小小鼓励了!


文章可能过长,但全是干货,请大家耐心品读


文章目录

  • 一、树的概念及结构
    • 1.1树的结构
    • 1.2树的相关概念
    • 1.3树的表示
    • 1.4树在实际中的应用
  • 二、二叉树的概念及结构
    • 2.1概念
    • 2.2现实中的二叉树
    • 2.3特殊的二叉树
    • 2.4二叉树的性质
    • 2.5小练习
      • 2.5.1小练习第一题
      • 2.5.2小练习第二题
      • 2.5.3小练习第三题
  • 三、二叉树链式结构的实现
    • 3.1前中后序遍历
    • 3.2计算二叉树的总结点数
    • 3.3计算二叉树叶子结点数
  • 四、总结

一、树的概念及结构

1.1树的结构

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的

下面是树的三个特点:

•有一个特殊的结点,称为根结点,根节点没有前驱结点
•除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。 每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
•因此,树是递归定义的。

只有文字,不太方便理解,下面用图解来深入了解一下!

注意:树形结构中,子树之间不能有交集,否则就不是树形结构!


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)棵互不相交的树的集合称为森林

温馨提示:上面黄色背景的概念是必须要熟练掌握的,其他的只需了解即可。

还需要注意的一点是:有一些教材中,在计算数的深度(高度)时,会把根结点所在的那一层记为第0层。
这种说法也是正确的,但我建议大家还是按照第一种方法来。原因在于如果根结点所在层数为0,当一棵树的结点数为空(即没有根结点时)该怎么规定这棵树的高度呢? 好像我们还没有听说过有高度为-1的树吧


1.3树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法

这里说明一下为什么不采用其他的方法:
当采用其他方法定义一棵树时,一个树的结构体成员数量是不确定的(当一个结点的孩子或者度过多时,很难将其完美地表示出来)。所以这里主要介绍三种比较简单有效的方法

方法一:顺序表存孩子的指针(这种方法主要在C++中采用:

struct TreeNode{int data;vector<struct TreeNode*>childs;};

即在一个结构体中存放一个数据,再存放一个大小并不固定的数组,用来存储其他的结点。这种方法也可以在C语言中运用,但其操作要比C++麻烦许多。

方法二:左孩子右兄弟

typedef int DataType;struct Node{ struct Node* _firstChild1; // 第一个孩子结点 struct Node* _pNextBrother; // 指向其下一个兄弟结点 DataType _data; // 结点中的数据域};

定义一棵树的结构体时,当一个结点有多个孩子时,只存储左边第一个孩子的结点,再用一个兄弟指针存放旁边一个孩子的结点

以下图为例:

这种方法是非常实用的一种方法,也是我最推荐的一种方法哦✨

方法三:双亲表示法

这种方法是用数组来存储每一个结点的父亲每个结点从上到下,从左到右依次从0开始编号,对应数组中的每个下标。每个节点中只存储其父亲的数组下标。


这种方法也是非常实用的一种方法,只不过它所应用的场景是初学者没有见过的。它往往出现在并查集中,这个还需要各位小伙伴们在以后的学习中慢慢体会,这里就先不讲这个了。


1.4树在实际中的应用

想必大家都很好奇,树在现实生活中到底有什么用途呢?其实,我们手机和电脑里保存文件的各种目录就是树结构的一个很好地应用。

如上图所示,最开始的一阶目录就相当于树的根结点,它里面包含的下一阶目录就相当于每一个节点的孩子。当文件为空或者该文件所在目录为最后一级时,这个文件就相当于叶节点。

更加深入的内容,相信大家在以后的学习中会遇到的,我们这里就不再继续讨论了。


二、二叉树的概念及结构

2.1概念

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

从上图可以看出:

  1. 二叉树不存在度大于2的结点
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

注意:对于任意的二叉树都是由以下几种情况复合而成的:


2.2现实中的二叉树

我们现实生活中的二叉树长什么样子呢?这里有两张图:


当普通人看到这两棵树时的反应往往是:giao,好对称的两棵树啊!

但是当一个程序猿看到它们时的反应就会是:giao,二叉树成精了!

当然,这两张图只是让大家换个方式理解二叉树,我们接着往下看!

2.3特殊的二叉树

  1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 2^k-1,则它就是满二叉树。


那么,我们可以得到一个结论:
如果一棵满二叉树的高度为h,则其节点数为2^h-1
如果一个满二叉树的节点数为N,则其高度为log(以2为底N+1的对数)

  1. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。注意:完全二叉树要求当树的高度为h时,前h-1层必须是满的,最后一层可以不满,但要求最后一层的节点必须从左到右依次排列,不能断开。

2.4二叉树的性质

1.若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个节点。
2.若规定根节点的层数为1,则深度为h的二叉树的最大节点数为2^n-1。
3. 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为 n1,则有 n0=n2 +1
4.若规定根节点的层数为1,则具有n个节点的满二叉树的深度为log(以2为底n+1的对数)。

以上性质1、2、4都已经在上文中出现,这里主要讲解一下性质三。

这个结论是一个高手总结出来的,而且非常有用,在很多题目中都能用到!

2.5小练习

2.5.1小练习第一题

某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A 不存在这样的二叉树
B 200
C 198
D 199

根据上面的二叉树性质,度为0的结点(即叶子结点)的个数为度为2的结点的个数+1.所以该题答案为199+1=200.

其实这道题跟399个结点半毛钱关系都没有,我们只需要掌握二叉树性质就能秒杀它,所以,上面的性质真的是十分重要!

2.5.2小练习第二题

在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n
B n+1
C n-1
D n/2

2.5.3小练习第三题

一棵完全二叉树的节点数位为531个,那么这棵树的高度为( )
A 11
B 10
C 8
D 12


对于这种类型的题,当写到这一步的时候,就只需将选项中的值带入到式子中去,如果解出一个合理的值,那就是正确答案,本题的答案是B,小伙伴们可以自己动手解一下哦!

三、二叉树链式结构的实现

3.1前中后序遍历

在学习二叉树的前中后序之前,我们需要知道每一棵树(一整棵树也好,一颗子树也好)都由三部分组成—-根、左子树、右子树

以下图为例:

这里可能大家会疑惑为什么存在空这种说法,好像学校里老师从来没这样说过。
但是,既然每一个结构体变量里都要存储左右子树,那么到了叶节点的时候,指向左右子树的指针就只能指向空了,所以叶节点也有自己的左右子树,只不过它们都为空
而当一棵树的左右子树都为空时,也就不用再继续向下找它的子树了。

还需要注意的是,在处理树的问题的时候,我们通常采用分治(分而治之)的方法,这就需要我们不断地将一棵树分为根和它的子树,直到不可再分。
不知道大家有没有想到,这种方式好像和递归有些相似?

哈哈,现实也确实是这样的,在处理树的问题时,我们大多时候都离不开递归,大家慢慢体会吧!
下面来举例讲解前、中、后序:

前序—-按照根->左子树->右子树的顺序遍历整个树

大家这里注意到,遍历时,出现了多次NULL,这时就是所谓的“不可再分”。而我们平常在课本或者习题中见到的都是把其中的NULL去掉之后的样子,本质是一样的!

中序—-按照左子树->根->右子树的顺序遍历整棵树

后序—-按照左子树->右子树->根的顺序遍历整棵树

下面我们用代码分别把上面三种情况表示出来:

先创建一个结构体,分别存储一个节点的左子树、右子树和结点中所存储的值:

typedef char BTDataType;typedef struct BinaryTreeNode{struct BinaryTreeNode* left;//存储节点的左孩子struct BinaryTreeNode* right;//存储节点的右孩子BTDataType data;//存储结点所代表的值}BTNode;

然后运用递归对树进行前中后序的遍历:

void PrevOrder(BTNode* root)//前序{if (root == NULL){printf("NULL ");return;}printf("%c ", root->data);//先打印根,然后是左子树和右子树PrevOrder(root->left);PrevOrder(root->right);}void InOrder(BTNode* root)//中序{if (root == NULL){printf("NULL ");return;}PrevOrder(root->left);//先打印左子树,然后是根和右子树printf("%c ", root->data);PrevOrder(root->right);}void PostOrder(BTNode* root)//后序{if (root == NULL){printf("NULL ");return;}PrevOrder(root->left);//先打印左右子树,再打印根PrevOrder(root->right);printf("%c ", root->data);}

接下来仅需要为结点开辟空间,再存储相应的值,最后把节点相互连接起来即可:

int main(){BTNode* A = (BTNode*)malloc(sizeof(BTNode));A->data = 'A';A->left = NULL;A->right = NULL;BTNode* B = (BTNode*)malloc(sizeof(BTNode));B->data = 'B';B->left = NULL;B->right = NULL;BTNode* C = (BTNode*)malloc(sizeof(BTNode));C->data = 'C';C->left = NULL;C->right = NULL;BTNode* D = (BTNode*)malloc(sizeof(BTNode));D->data = 'D';D->left = NULL;D->right = NULL;BTNode* E = (BTNode*)malloc(sizeof(BTNode));E->data = 'E';E->left = NULL;E->right = NULL;A->left = B;A->right = C;B->left = D;B->right = E;PrevOrder(A);printf("\n");InOrder(A);printf("\n");PostOrder(A);return 0;}

运行结果如下:

可以看到,结果和我们之前分析的相同!
仅仅看代码,可能大家还是没有彻底理解递归的整个过程,我们用一个图例来体会一下:

那对于递归的知识掌握得还不是很深的小伙伴可以尝试自己画一下代码的递归展开图,会帮助自己更快地理解其中奥秘❗️❗️❗️


3.2计算二叉树的总结点数

接下来再来看一下如何计算二叉树中所有结点的个数:(该二叉树和前中后序遍历中为同一棵二叉树)

方法一:遍历计数法:

typedef struct BinaryTreeNode{struct BinaryTreeNode* left;//存储节点的左孩子struct BinaryTreeNode* right;//存储节点的右孩子BTDataType data;//存储结点所代表的值}BTNode;int TreeSize(BTNode* root){if (root == NULL){return;}int size = 0;size++;TreeSize(root->left);TreeSize(root->right);}int main(){BTNode* A = (BTNode*)malloc(sizeof(BTNode));A->data = 'A';A->left = NULL;A->right = NULL;BTNode* B = (BTNode*)malloc(sizeof(BTNode));B->data = 'B';B->left = NULL;B->right = NULL;BTNode* C = (BTNode*)malloc(sizeof(BTNode));C->data = 'C';C->left = NULL;C->right = NULL;BTNode* D = (BTNode*)malloc(sizeof(BTNode));D->data = 'D';D->left = NULL;D->right = NULL;BTNode* E = (BTNode*)malloc(sizeof(BTNode));E->data = 'E';E->left = NULL;E->right = NULL;A->left = B;A->right = C;B->left = D;B->right = E;printf("TreeSize:%d\n", TreeSize(A));return 0;}

上面的代码就是对一棵二叉树进行遍历,运用递归,若节点不为空,就进行计数。可是,这种方法得出的结果真的是正确的吗?

可见,该方法得到的结果错误!

上面的代码错误的原因在于:每次递归调用TreeSize函数时,都会重新创建一个size,即每次进行计数的size都不一样,也就达不到累加的效果

为了避免出现上面的错误,我们可以使用一个全局变量的size,这样,就能把每次递归的值加到size上。如下:

int size = 0;void TreeSize(BTNode* root){if (root == NULL){return;}size++;TreeSize(root->left);TreeSize(root->right);}

随后,直接打印size即可:

但是,这种方法就万无一失了吗❓❓❓
接下来,我们来同时用这种方法求一下A和B的结点数:

TreeSize(A);printf("TreeSize:%d\n", size);TreeSize(B);printf("TreeSize:%d\n", size);


正确的答案应该是5 3,很明显,编译器输出的结果错误。原因在于,每次调用函数用的都是同一个size,这就导致计算B时累加了A的结点数
为了避免累加,就需要在计算下一棵树的结点之前,将size置为0.

TreeSize(A);printf("TreeSize:%d\n", size);size = 0;TreeSize(B);printf("TreeSize:%d\n", size);


这样就得到正确答案了!
但是,这种方法还有一个潜在的漏洞。当有多个线程同时调用该函数时,最终的结果还是会累加。这时候就不妨直接传参调用该函数,即每次使用都传一个参数用来记录节点的个数

void TreeSize(BTNode* root,int* p){if (root == NULL){return;}(*p)++;TreeSize(root->left, p);TreeSize(root->right, p);}int main(){BTNode* A = (BTNode*)malloc(sizeof(BTNode));A->data = 'A';A->left = NULL;A->right = NULL;BTNode* B = (BTNode*)malloc(sizeof(BTNode));B->data = 'B';B->left = NULL;B->right = NULL;BTNode* C = (BTNode*)malloc(sizeof(BTNode));C->data = 'C';C->left = NULL;C->right = NULL;BTNode* D = (BTNode*)malloc(sizeof(BTNode));D->data = 'D';D->left = NULL;D->right = NULL;BTNode* E = (BTNode*)malloc(sizeof(BTNode));E->data = 'E';E->left = NULL;E->right = NULL;A->left = B;A->right = C;B->left = D;B->right = E;int sizeA = 0;TreeSize(A, &sizeA);printf("TreeSize:%d\n", sizeA);int sizeB = 0;TreeSize(B, &sizeB);printf("TreeSize:%d\n", sizeB);return 0;}


可见,结果正确。
方法二:分治算法:

int TreeSize(BTNode* root){return root == NULL " />0 : TreeSize(root->left) + TreeSize(root->right) + 1;}int main(){BTNode* A = (BTNode*)malloc(sizeof(BTNode));A->data = 'A';A->left = NULL;A->right = NULL;BTNode* B = (BTNode*)malloc(sizeof(BTNode));B->data = 'B';B->left = NULL;B->right = NULL;BTNode* C = (BTNode*)malloc(sizeof(BTNode));C->data = 'C';C->left = NULL;C->right = NULL;BTNode* D = (BTNode*)malloc(sizeof(BTNode));D->data = 'D';D->left = NULL;D->right = NULL;BTNode* E = (BTNode*)malloc(sizeof(BTNode));E->data = 'E';E->left = NULL;E->right = NULL;A->left = B;A->right = C;B->left = D;B->right = E;printf("TreeSizeA:%d\n", TreeSize(A));printf("TreeSizeB:%d\n", TreeSize(B));return 0;}

一棵树的结点数为左子树的结点数+右子树的结点数+根结点。利用分治思想,每次递归,直到不可再分求出一棵树的总结点数。


3.3计算二叉树叶子结点数

计算叶子结点个数和计算结点总数有异曲同工之妙,都是运用递归和分治的思想。这里就不再赘述了。

int TreeLeafSize(BTNode* root){if (root == NULL)return 0;if (root->left == NULL && root->right == NULL)return 1;return TreeLeafSize(root->left) + TreeLeafSize(root->right);}

四、总结

本篇文章介绍了二叉树的一些基本概念和性质,太深入的知识还没有涉及。因为博主现在临近期末,没有太多时间更新博客,所以有关二叉树更多深入的知识就留在暑假更新了~

不过相信大家学习了本篇内容之后,也就可以应对学校里考试的大多数题目了!
博主也只是一个大一学生,也在不断学习,如果文章中有哪些不太严谨的地方,还请私信告诉我,我将不胜感激!大家互相帮助,互相监督,学习的过程也会更加快乐!
一起加油吧~