目录
树的认识
树的相关概念:
树的表示:
二叉树
特殊的二叉树
二叉树的性质
二叉树的存储
创建二叉树的结点
二叉树的遍历
前序遍历:
中序遍历
后序遍历
在说二叉树的基本概念和操作的同时,我们先说一下树的基本概念.
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成的一个具有层次关系的集合。
把它叫做树,因为它的形状像是一棵倒挂的树,也就是说它的根节点在上,叶结点在下.
类似于这样:
但是注意,树形结构中子结点不能有交集,否则就不是树形结构.
注意:1.子树是不相交的
2.除了根结点外,每个结点只有一个父结点.
上述图片中第一个图片,C子树和D子树相交,所以不是树型结构.
第二个图片,E结点有B和C两个父结点,所以也不是树形结构.
下面来介绍树的相关概念.
节点的度 : 一个结点含有子树的个数称为该结点的度. 例如A有6个子树,分别为:BCDEFG,所有A结点的度为6.
叶节点或终端节点:度为0的点称为叶结点(叶子节点).例如BCHIKLMNPQ都没有子树,即结点都为0,它们都是叶结点
非终端节点或分支节点:度不为0的点.ADEFGJ都是分支节点.
孩子节点或子节点:一个结点含有的 子树的根节点 称为该节点的孩子结点. 例如BCDEFG都是A的子节点.
兄弟节点:具有相同父结点的结点互称为兄弟结点,如BCDEFG的父结点都是A.所以他们互为兄弟结点.
树的度:所有结点中最大的度.如上图,A的度最大,为6,所以树的度为6.
节点的层次:从根开始定义起,根(A)为第1层,根的子节点为第2层(BCD...),以此类推.
树的高度或深度:树中节点的最大层次. 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟(注意不是同一个父亲).如上图:H、I互为兄弟节点,它们父结点分别为D,E.
节点的祖先:从根到该节点所经分支上的所有节点,如上图:A是所有节点的祖先.P的结点的祖先是JEA.
森林:由m(m>0)棵互不相交的树的集合称为森林.
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
这个只作了解即可,需要掌握的是后面的二叉树.
即一个结点含有三个成员:自身数据(data)、第一个孩结点(child)、下一个兄弟结点(brother)
结构体如下:
typedef int DataType;
struct Node
{
struct Node* Child1; // 第一个孩子结点
struct Node* Brother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
查找过程:
这样就能理解了.我们平常电脑打开文件,然后这个文件里面又有好多文件.其实本质就是用树的结构来存储的.
介绍了这么多前置知识,下面开始真正进入主题:
二叉树是指所有节点的度都小于等于2.
一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成,如下图
特殊的二叉树
1.满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2^k -1 ,则它就是满二叉树。
2.完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树,满二叉树是特殊的完全二叉树.
如下图:
千万注意,完全二叉树最后一层节点一定要集中在左边! 下图这样的就不是完全二叉树,因为它们并没有集中在左边.
我们来讲一下二叉树所具有的性质.
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层(最后一层)上最多有2^(i-1) 个结点.
2.若规定根节点的层数为1,则深度为h的二叉树的最大结点数(相当于完全二叉树的总结点)是2^h - 1.
3.对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为n2,则有
n0 = n2 + 1.
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log(n+1). (ps:log是以2
为底,n+1为对数)
我们利用性质来做下面的练习题
1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A 不存在这样的二叉树
B 200
C 198
D 199解析:这个题我们利用性质3,n0=n2+1.得到n0 = 200.所以叶子结点的个数为200个,选择B.
2.在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n
B n+1
C n-1
D n/2解析:我们知道二叉树的度最大不超过2,所以我们设度为0的结点有n0个,度为1的结点有n1个,度为2的结点有n2个.则满足:
n0 + n1 + n2 = 2n(1)
又有性质3得到:n0 = n2 + 1. ==>n2 = n0 - 1.
替换掉(1)式的n2,得到:
n0 + n1 + n0 - 1 = 2n. ==> 2*n0 + n1= 2n+1.
题目中说了,是完全二叉树,即度数为1的的结点只可能有0个或者1个,这个可以参考完全二叉树结构
假设n1 = 0,则2*n0 = 2n+1. ==>n0 = (2n+1)/2. 但n0一定是一个整数,这个结果是一个小数,不符合条件((2n-1)一定是一个奇数,除以2一定是小数)
假设n1 = 1.则2*n0 + 1= 2n+1 ==> n0 = (2n)/2 = n.结果是一个整数.符合结果
所以叶子节点个数为n,选择A
3.一个具有767个节点的完全二叉树,其叶子节点个数为()
A 383
B 384
C 385
D 386解析:和上面方法一样.
n0 + n1 + n2 = 767(1)
n0 = n2 + 1. ==>n2 = n0 - 1.(2)
由以上两个式子得到n0 + n1 + n0 - 1 = 767
2*n0 + n1 = 768
此时n1只有等于0的时候,n0才能为整数.
所以n1= 0时,n0 = 768/2 = 384
选择B.
4.一棵完全二叉树的节点数位为531个,那么这棵树的高度为( ).
A 11
B 10
C 8
D 12解析:设完全二叉树的高度为h,既然它是完全二叉树,则它的前h-1层一定是满的.即它的最少个数为2^(h-1),最大为第h层全部满节点,有2^h-1.
所以这颗树的范围为[2^(h-1),2^h -1].
则2^(h-1) < 531. 解得h-1= 9 进一步得到n=10.
当然把选项带入到选项中,如果题目中的结果在范围中,说明符合条件.
两种方法都可以.
说了二叉树的性质,下面该说二叉树的存储和一些基本操作了.
二叉树一般采用两种方式存储:1.顺序存储 2. 链式存储
1.顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
如下图,说明了为什么会造成空间浪费:
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面我们讲解如红黑树等的时候会用到三叉链。
如下图所示:
其中 ^ 代表为空.
我们后面的操作都是采用链式二叉树进行操作的.
先来看二叉树每个结点的结构体:包括自身的数据、左子节点的地址、右子节点的地址.
typedef char BTDataType;
typedef struct BinaryTreeNode
{
struct BinartTreeNode* left;//左子节点地址
struct BinartTreeNode* right;//右子节点地址
BTDataType data;//自身数据
}BTNode;
先申请创建一个结点,再将目标值赋值给这个结点的data值,然后分别让左右孩子地址分别指为NULL.
代码如下:
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
assert(node);
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
下面来说二叉树的三种遍历:前序遍历,中序遍历,后序遍历.
还有一种遍历是层序遍历(bfs),这个遍历还有一些基本二叉树相关问题我们放到下一节讲解.
先访问每个结点的值,再访问左子树,最后访问右子树.
注意所说的,是左右子树!!!不是左右子结点!
这种遍历用例子来说更加明白.
步骤:
先访问根节点的值,为1,此时前序遍历结果为1.
再访问 1 的左子树,在1的左子树中,先访问根结点,为2,此时前序遍历结果为12
此时再访问 2 的左子树,在2的左子树中,先访问根节点,为3,此时前序遍历结果为123.
接着访问 3 的左子树,此时为空,返回,开始访问3的右子树,也为空,返回.
此时在结点 3 的这个 根节点,左子树和右子树已经全部遍历完毕,返回到2.
2此时只是执行完访问自己的根节点和左子树了,得继续访问右子树,为空,返回
此时结点2 的结点,左子树和右子树也已经全部遍历完毕,返回到1.
此时1自己的根节点和左子树已经遍历完毕,按照顺序,开始访问右子树.
访问1的右子树:在1的右子树中,先访问根节点,为4,此时前序遍历结果为1234
按照前序遍历的顺序,开始访问4的左子树,在4的左子树中,先访问根节点是5,此时前序遍历结果为12345.
再访问5的左右子树,全部为空,返回到5,此时结点5也已经全部遍历完毕,返回到4.
4此时访问了自身和左子树,接下来该访问右子树.
在4的右子树中,先访问根节点,为6,此时前序遍历结果为123456.(其实这里已经可以结束了,但为了完整性,我们继续),再访问6的左右子树,全部为空,返回到6.
结点6 此时左右子树已经全部遍历完毕,返回到4,4此时左右子树也已经遍历完毕,返回到1.
1的左右子树也都已经遍历完毕,程序运行完毕.
代码如下:
代码非常简单,难的是这个理解的过程.
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
printf("%c ", root->data);//访问结点本身的数据
PreOrder(root->left);//访问左子树
PreOrder(root->right);//访问右子树
}
和前序遍历不同的是,这次的遍历顺序是:先访问左子树,再访问根节点,最后访问右子树.
还是这幅图:
理解了中序遍历的话,后序遍历也就能理解了.
再按步骤来一遍:
先访问1的左子树,在1的左子树中,再访问2的左子树,在2的左子树中,再访问3的左子树
3的左子树为空,返回到了,此时左子树访问完毕就该访问这个结点的值了,为3,此时中序遍历的结果为3
再访问3的右子树,为空,返回到3,此时3的左右子树和自己已经访问完成,返回到2.
2的左子树已经访问完,开始访问2结点本身的值,此时中序遍历结果为32.
再访问2的右子树,为空,返回到2,此时结点2已经左右子树全部遍历完毕.
返回到1,此时1的左子树已经遍历完成,开始遍历结点1本身的值,所以此时中序遍历的结果为321.
开始访问1的右子树,在1的右子树中,再访问4的左子树,在4的左子树中,访问5的左子树
5的左子树为空,返回,按照顺序,访问结点5本身的值,此时中序遍历结果为3215.
再访问5的右子树,为空。返回到5,此时结点5的左右子树已经全部遍历完毕,返回到4.
此时4的左子树已经遍历完成,开始访问结点本身的值,所以此时中序遍历的结果为32154.
开始访问4的右子树,在4的右子树中,访问6的左子树,左子树为空,返回,此时访问结点6本身的值,此时中序遍历的结果为321546.(到这其实可以结束了,但是为了完整,依然要说完全流程)
再访问6的右子树,为空,返回到6.
6的左右子树已经全部遍历完毕,返回到4.
4的左右也已经遍历完毕,返回到1
1的左右子树遍历完毕,程序结束.
代码如下:
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
InOrder(root->left);//遍历左子树
printf("%d ", root->data);//遍历自身的值
InOrder(root->right);//遍历右子树
}
后续遍历的顺序是:先访问左子树,再访问右子树,最后访问根节点.
可以参照中序遍历.融会贯通嘛,会非常简单,如果实在不能理解,可以私信博主,单独解答哦.
代码如下:
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
PostOrder(root->left);//遍历左子树
PostOrder(root->right);//遍历右子树
printf("%d ", root->data);//遍历自身的值
}
二叉树的一些基本操作就讲完啦.
我们下一章讲解二叉树的层序遍历,和一些二叉树的基本问题,例如获得二叉树总结点的数目、二叉树的层数等等.学完下一章,相信你会对二叉树的理解会更深一步.
二叉树的基本操作就到这里了,如果有错误或者遗漏的地方,欢迎大家指正或补充哦.