网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
一个二叉树,如果每一层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果二叉树的层数为k,它的结点总数为2^k-1,那么这是一个满二叉树
如下图所示就是一个满二叉树
看着图我们也能理解这个满二叉树了,而它的层数为k的话,它的第一层是20个结点,第二层是21个结点,第三层是22个结点…第k层是2(k-1)个结点,那么对其求和之后它的总结点数当然就是2^k-1个结点了。同样的我们也可以解出来有N个结点的满二叉树层数为log(N+1),这里的对数是以2为底的。
2.完全二叉树:
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一一对应时称之为完全二叉树。要注意的是满二叉树是一种特殊的完全二叉树
需要特别注意的是,完全二叉树的最后一层如果是不满的话,那么最后一层结点必须是按顺序的,从左到右两个子树中不能出现空树,否则不是完全二叉树。
也就是说假设树的高度是h
1.前h-1层都是满的
2.最后一层不满,但是最后一层从左到右都是连续的
完全二叉树的结点个数我们也很容易的得到是2^k-1-x,x为完全二叉树对应的满二叉树所缺的元素个数。
所以如果知道结点的个数N,我们也可以反推出高度为log(N+1+X),其中该对数是以2为底数的。而且这个高度我们可以近似认为是logN,以2为底数。因为X是小于等于N二大于等于0的。我们只需要近似的算出高度,向上取整即可
3.二叉树的性质
1.若规定根节点的层数为1,则一颗非空二叉树的第i层上最多有2^(i-1)个结点。
2.若规定根节点的层数为1,则深度为h的二叉树最大的结点数是2^h-1
3.对于任何一颗二叉树,如果度为0的叶结点个数为n0,度为2的分支结点个数为n2,则有n0=n2+1
4.若规定根结点的层数为1,具有N个结点的满二叉树的深度为logN
这里是一些二叉树的一些选择题
- 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A 不存在这样的二叉树 B 200 C 198 D 199
对于这道题,其实如果不知道性质3的话是很难想出来的,由性质三的,度为0的结点个数是度为2的结点个数+1直接得到答案是200
也就是说这道题跟这个399没有关系
2.在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n B n+1 C n-1 D n/2
对于这道题,我们可能一开始也是很懵的。但是其实还是考察二叉树的性质
设度为0的结点个数为x0,度为1的结点个数为x1,度为2的结点个数为x2
所以我们就知道了,x0+x1+x2=2n //这是题目给出的第一个方程
然后由性质三得到x0=x2+1 //第二个方程
其实到了这里,我们看似没有条件,但是题目中说了是完全二叉树,所以它的度为1的结点个数要么是0要么是1。所以我们无非就是将两种情况都算一遍就可以了
最终解出来的答案只有A符合
3.一棵完全二叉树的节点数位为531个,那么这棵树的高度为( )
A 11 B 10 C 8 D 12
对于这道题,也是考察二叉树的性质,按照我们的公式就是直接logN,然后最终结果向上取整即可
我们知道1024是2的十次方,512是2的9次方,而它刚好落在这个范围内,所以最终的计算结果应该是9.xxxxx,而它向上取整刚好就是10,所以高度为10
4.二叉树的存储结构
二叉树一般可以使用两种结构存储:一种顺序结构,另外一种链式结构
(1)顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树
(2)链式存储
二叉树的链式存储结构是指,用链表表示一颗二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成。数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在链结点的存储地址。链式结构又分为二叉链,三叉链。我们现在所用的都是二叉链
下面是二叉链与三叉链的声明
三、二叉树链式结构的实现
1.二叉树的前序中序后序(深度优先遍历)
(1)树的分割
我们得先将一颗树的结构给理清楚,一棵树由三部分构成,分别是根节点,左子树,右子树。
比如说下面这棵树
我们是这样看这棵树的
这棵树首先分为根节点A,左子树B,右子树C
然后我们继续具体细分,左子树B中,又可分为根节点B,左子树D,右子树E。而右子树C中,又可分为根节点C,左子树空树,右子树空树
然后继续具体细分,D这棵树可分为根节点D和左子树空,右子树空,E这颗树可分为根节点C,左子树空,右子树空
这就是我们看待一颗树时候的看法
这也是分治算法:分而治之,大问题分为类似的子问题,子问题在分为子问题…直到子问题不可再分割
(2)先序遍历
那么这个树的分割我们直到了,它对我们的先序中序后序遍历树有什么用呢?
我们先看先序遍历,其实先序也称作先根,如下图所示,先根就很通俗易懂了,先访问根,再访问左子树,再访问右子树。
那么我们按照这个思路用先序的方式去访问一下这棵树吧,首先这棵树得先访问根节点A
然后我们开始访问左子树B,访问这颗左子树的时候,我们又先访问左子树的根,也就是B
访问完B的根了,我们就要访问它的左子树D,而它的左子树D,又要先访问它的根,也就是D
访问了D的根节点,那么我们又要访问它的左子树,而它的左子树是空
然后继续访问D的右子树,刚好它也是空
D这棵树访问完了,而D是属于B的左子树,那么我们就应该访问B的右子树E了,而它的访问也一样,先访问根节点,再访问左子树,在访问右子树
然后我们就发现,其实B这颗树也访问完成了,那么这下也就是A的左子树访问完了,该访问A的右子树了,而A的右子树C,它也是先访问根节点C,然后访问左右两颗树,而它的左右两颗树刚好都是空的
这就是我们的先序遍历了。当然有的书上没有NULL这个东西,这个其实也不影响的
(3)中序遍历
有了先序遍历的理解那么其实,中序遍历我们也很容易就能写出来了,因为中序遍历其实就是先左子树,根,最后右子树
那么我们简单的分析一下:
首先这颗树分为根节点A,左子树B,右子树C。那么我们因为是中序遍历,所以得先访问B这颗左子树。而B这颗左子树又分为根节点B,左子树D,右子树E。所以我们还得先访问D这颗树,而D这颗树,又分为根节点D,左子树NULL,右子树NULL。到了这里左子树已经到头了,所以我们该返回去了,所以目前的访问顺序就是NULL D NULL B,这样也就是B的左子树以及它的根已经访问完了,现在该访问B的右子树了,而这个根D是一样的过程,所以目前我们的访问顺序就成了NULL D NULL B NULL E NULL。这样也就意味着B这颗树访问完了,而B又是A这颗树上的左子树,所以现在将A一访问,就该访问右子树C了,而C的右子树又分为左子树NULL根C右子树NULL。
所以最终的访问顺序为NULL D NULL D NULL E NULL A NULL C NULL
(4)后序遍历
这个的访问我们就更加熟悉了,它的访问过程是
NULL NULL D NULL NULL E B NULL NULL C A
我们在看一个例子
这颗树的前序中序后序是
(5)先序中序后序的代码实现
根据了上面的分析,其实我们也不难得知这个是通过递归实现的,代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef char BTDateType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDateType date;
}BTNode;
//先序
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->date);
PrevOrder(root->left);
PrevOrder(root->right);
}
//中序
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%c ", root->date);
InOrder(root->right);
}
//后序
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->date);
}
int main()
{
BTNode* A = (BTNode*)malloc(sizeof(BTNode));
A->date = 'A';
A->left = NULL;
A->right = NULL;
BTNode* B = (BTNode*)malloc(sizeof(BTNode));
B->date = 'B';
B->left = NULL;
B->right = NULL;
BTNode* C = (BTNode*)malloc(sizeof(BTNode));
C->date = 'C';
C->left = NULL;
C->right = NULL;
BTNode* D = (BTNode*)malloc(sizeof(BTNode));
D->date = 'D';
D->left = NULL;
D->right = NULL;
BTNode* E = (BTNode*)malloc(sizeof(BTNode));
E->date = '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);
printf("\n");
}
运行结果为
2.计算二叉树中结点的个数
这个也很简单,只要我们理解了递归,那么这个就很容易了,代码如下
//计算二叉树中结点的个数
int TreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
else
{
return 1 + TreeSize(root->left) + TreeSize(root->right);
}
}
这是运行结果
3.计算二叉树中叶子结点的个数
这个同样很简单,如果树是空树,那么直接返回0即可,如果该结点的左孩子和右孩子都是空指针,那么返回1就可以了,说明他是一个叶子结点,其他情况我们就采用递归的思想去计算他的左孩子和右孩子的结点就可以了,代码如下
//计算叶子结点的个数
int TreeLeftSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
else if (root->left == NULL && root->right == NULL)
{
return 1;
}
else
{
return TreeLeftSize(root->left) + TreeLeftSize(root->right);
}
}
这是运行结果
4.二叉树的层序遍历(广度优先遍历)
我们先说一下广度优先遍历的基本思路,他的基本思路是这样的,需要使用一个队列
如下图所示,我们先将A入队列
然后 A出队列的同时打印他并且将他的左右孩子入队列
然后继续出B,打印他 入他的左右孩子
然后我们继续出C,入他的孩子,打印C
后面的也都是同理,出队列的同时打印他并且入他的孩子,如果没有孩子,那就不用入孩子了
这就是我们的大致思路,而要实现这个首先,我们得导入我们队列,导入之后,我们需要修改的部分就是这两个,前置声明,因为我们的树是在他的里面定义的,所以在队列的头文件里面是不认识树结点的,所以我们得先声明一下,定义就在后面让他去找去。
所以他最终的代码为
//层序遍历
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root != NULL)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%c ", front->date);
if (root->left != NULL)
{
QueuePush(&q, front->left);
}
if (root->right != NULL)
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestory(&q);
}
运行结果为
5.二叉树的销毁
想要销毁这颗二叉树,那么我们就必须得使用后序的方式来进行销毁,如果根节点是NULL,那么什么也不用做,如果不是空,那么就先销毁他的左子树,然后销毁右子树,最后销毁该结点。如此递归下去即可,代码如下
void DestoryTree(BTNode* root)
{
if (root == NULL)
{
return;
}
DestoryTree(root->left);
DestoryTree(root->right);
free(root);
root = NULL;
}
6.二叉树的一些选择题
1.某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH 。该完全二叉树的前序序列为( )
A、 ABDHECFG B、 ABCDEFGH C、 HDBEAFCG D、 HDEBFGCA
对于这道题,最关键的部分是完全二叉树这几个字眼。由此我们直接得出该完全二叉树的图
而他的先序遍历我们很容易就得知,A B D H E C F G
所以选A
2.二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则二叉树根结点为 ()
A、E B、 F C、 G D、 H
对于这道题其实不用中序遍历我们也能做出来,因为我们知道先序遍历第一个肯定是根节点,所以就直接得出答案为E
但是呢我们也要知道先序遍历和中序遍历其实是可以确定一个二叉树的。我们现在画出他的二叉树,首先先序遍历我们可以确定根,所以我们知道首先有一个根E,有了根以后,我们根据这个根在中序遍历中的位置就能确定出左右区间,所以HFI部分为左子树,JKG部分为右子树。而HFI这一部分我们又回到先序遍历中,我们可以看出F是根,我们由这个F是根又回到中序遍历中确定他的左右子树区间,我们不难得出,H就是F的左子树,I就是F的右子树。同理,我们可以得出JKG部分的结构,G为根节点,J和K都是左子树部分,而JK部分中,J又是根节点,而K 是J 的右子树
最终我们得出这颗二叉树为
3.设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为____。
A、 adbce B、 decab C、 debac D、 abcde
这道题和上一道题是一样的,我们可以根据中序遍历和后序写出他的二叉树。这里直接给出他的二叉树
所以他的先序遍历为abcde
7.牛客题目之二叉树遍历
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
g)
3.设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为____。
A、 adbce B、 decab C、 debac D、 abcde
这道题和上一道题是一样的,我们可以根据中序遍历和后序写出他的二叉树。这里直接给出他的二叉树
所以他的先序遍历为abcde
7.牛客题目之二叉树遍历
[外链图片转存中…(img-J4o0Lul4-1715347112300)]
[外链图片转存中…(img-rFPjhu44-1715347112301)]
[外链图片转存中…(img-yWzJiWz4-1715347112301)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新