树的定义
树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:①有且仅有一个特定的称为根(Root)的结点;②当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、…Tm,,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree),如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ADOifX01-1684047713009)(E:\笔记\笔记内图片\QQ截图20230511171139.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hhRQ8tRu-1684047713009)(E:\笔记\笔记内图片\QQ截图20230511175624.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q7qovanx-1684047713010)(E:\笔记\笔记内图片\QQ截图20230511175714.png)]
注意点:
1.根结点唯一,不可能存在多个根节点
2.子树数量无限制,但是他们一定不相交
结点
结点的分类
结点拥有的子树数称为结点的度,度为0的结点称为叶结点(Leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值。如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQYWaOsY-1684047713010)(E:\笔记\笔记内图片\QQ截图20230511172420.png)]
A为根结点,B,C,D,F为内部结点,G,H,I,J,F为叶结点,D的度为3,则该树的度为3
结点之间的关系
结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)。同一个双亲的孩子之间互称兄弟(Sibling)。结点的祖先是从根到该结点所经分支上的所有结点。所以对于H来说,D、B、A都是它的祖先。反之,以某结点为根的子树中的任一结点都称为该结点的子孙。B的子孙有D、G、H、I。
结点的层次
结点的层根开始定义起,根为第一层,根的孩子为第二层。若某结点在第i层,则其子树就在第i+1层,其双亲在同一层的结点互为堂兄弟上图的D、E、F是堂兄弟,树中结点的最大层次称为树的深度(Depth)或高度,上图树的深度为4、
有序树与无序树
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
森林
森林(Forest)是m(m≥0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。对于开头的图中的树而言,后面的图中的两棵子树T₁和T₂其实就可以理解为森林。
树的储存结构
双亲表示法
每个结点都一定有双亲
我们可以设计在结点储存结构时,在每个结点中,附一个指针指向其双亲在数组中的位置
typedef struct node//注意这里是结点的储存结构
{
int data;//数据域,这里可以是各种数据类型
int parent;//指向其双亲的指针
}node;
树结构
typedef struct tree
{
node a[100];//结点数组
int r;//根的位置
int n;//结点数
}tree;
因为根结点没有双亲,所以将根的位置域设置为-1;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4skMxv2k-1684047713010)(E:\笔记\笔记内图片\QQ截图20230511211948.png)]
利用这样的数据结构可以很快的找到每个结点的双亲结点,但是如果你想找到该结点的孩子就只能遍历全部元素了,为了解决这个问题我们可以在设计结点储存结构时再设计一个指针指向他的右边第一个孩子。若有更多的要求,如要找其他的孩子,我们甚至可以设置一个数组记录其孩子的位置,但是这样真的就一定方便吗,不一定。设计储存结构时我们不一定要根据某个模板设计,设计储存结构是十分灵活的过程,复杂的结构意味着更多时间与空间的开销,简单的设计对应着快速的查找与增删,我们确实要根据实际情况来做出取舍。
孩子表示法
上面是从孩子到双亲的表示法,那么是否可以沿着这种思路设计一种由双亲到孩子的储存结构,下面我们将介绍一种由双亲到孩子的表示法。由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。不过,树的每个结点的度,也就是它的孩子个数是不同的。所以可以设计两种方案来解决。
方案一
将指针域的个数直接设置为树的度,这样虽然可以完成储存的目标,但是对于部分树,结点相差特别大的时候,对空间的浪费就会特别严重,为了减少空间的浪费,我们是否可以将,每一个结点的指针个数设置为不同的,按需求分配.
方案二
在设计储存结构时我们可以单独设置一个位置来储存该节点的孩子个数,在后续根据该位置设置指针个数
这种方法虽然减少了空间的浪费但是在运行时的时间损耗又比较大,下面开始介绍孩子表示法
具体办法是,把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,如右图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XDh4cJ7M-1684047713010)(E:\笔记\笔记内图片\微信图片_20230513113130.jpg)]
为此,设计两种结点结构。
一个是孩子链表的孩子结点,设置child和next
其中,child是数据域,用来存储某个结点在表头数组中的下标:nex是指针域,用来存储指向某结点的下一个孩子结点的指针。
一个是表头数组的表头结点,data 和firstchild
其中,data是数据域,存储某结点的数据信息;fistchld是头指针域,存储该结点的孩子链表的头指针。
以下是我们的孩子表示法的结构定义代码。
typedef struct node1//孩子结点
{
int child;//储存孩子对应下标位置
struct node1*next;//指向另一个孩子
}node1*;
typedef struct node//表头结点
{
int data;//该节点中的数据
node1* firstchild;//第一个孩子的位置
}node;
typedef struct tree//表示树结构
{
node a[100];//节点数组
int r;//根的位置
int n;//结点数
}tree;
二叉树的定义
定义:
二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
如果某个结点有俩个以上子树则他不是二叉树
例如上图中D有三个子树则其不是二叉树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-deXK0J5Q-1684047713011)(E:\笔记\笔记内图片\QQ截图20230513115804.png)]
这个就是一个二叉树
二叉树的特点
- 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树具有以下五种基本形态:
(1)空二叉树。
(2)只有一个根结点。
(3)根结点只有左子树。
(4)根结点只有右子树。
(5)根结点既有左子树又有右子树。
特殊二叉树
斜树
质名思义,斜树一定要是斜的,但是往哪斜还是有讲究的。所有的结点都只有左子树的二叉树叫左斜树。所有结点都只有右子树的二叉树叫右斜树。这两者统称为斜树。斜树有很明显的特点,就是每一层都只有、个结点,结点的个数与二叉树的深度相同。
满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
例[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jLHm8MF8-1684047713011)(E:\笔记\笔记内图片\QQ截图20230513121408.png)]
满二叉树的特点
(1)叶子只能出现在最下一层。出现在其他层就不可能达到平衡。
(2)非叶子结点的度一定是2。否则就是“缺胳膊少腿”了。
(3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
完全二叉树
对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同深度的满二叉树中编号为的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树,判断时可以按层排序,观察是否有不是最底层但是有结点空挡的情况,如果是则不是满二叉树。
从这里我们也可以得出一些完全二叉树的特点:
(1)叶子结点只能出现在最下两层。
(2)最下层的叶子一定集中在左部连续位置。
(3)倒数两层,若有叶子结点,一定都在右部连续位置。
(4)如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。
(5)同样结点数的二叉树,完全二叉树的深度最小。
二叉树的性质
性质一:
在二叉树的第i层最多有2^(i-1)个结点
二叉树因为定义的关系所以在每一层到下一层增长最快就是增长二倍,又第一层一定只有一个根节点所以最多有1*2^(i-1)个
性质二:
深度为k的二叉树至多有(2^k)-1个结点(k>=1)。
性质三:
性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fFkHT4oY-1684047713011)(E:\笔记\笔记内图片\QQ截图20230513193349.png)]
从连线数方面来解释一下,除了A是根结点无连线接入外,其他每个结点都只有一条连线接入,所以连线数N=n(结点数)-1;在分析每个结点连出去的连线,N=n1(度为1的结点)+2*n2(度为2的结点),在结点方面分析,可以得到 n=n0+n1+n2;
已经得到了三个式子
N=n+1
N=n1+2*n2
n=n0+n1+n2;
联立得到:n1+2*n2=n0+n1+n2+1
化简得到 n2=n0+1
性质四:
具有n个结点的完全二叉树的深度[log~2 n]+1([x]表示不大于x的最大整数)
由满二叉树可以得到n(结点数)=(2^k)-1,可以很容易得到k=log~2(n+1)
完全二叉树我们前面已经提到,它是一棵具有n个结点的二叉树,若按层序编号后,其编号与同样深度的满二叉树中编号结点在二叉树中位置完全相同,那它就是完全二叉树。也就是说,它的叶子结点只会出现在最下面的两层。
它的结点数一定小于等于同样深度的满二叉树的结点数2k-1,但一定多于2(k-1)-1。即满足2(k-1)-1<n<2k-1。由于结点数n是整数,n<2k-1意味着n<2k,n>2k-1,,2(k-1)-1<n意味着n>=2k-1,所以2(k-1)<=n<2^k不等式两边取对数,得到k-1<=log~2 n<k,而k作为深度也是整数,因此k=[log~2 n]+1。
性质五:
如果对一棵有n个结点的完全二叉树(其深度为[log~2 n]+1)的结点按层序编号(从第1层到第[long~2 n]+1层,每层从左到右),对任一结点i(1<=i<=n)有:
(1)如果i=1,则结点是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]。
(2)如果2i>n,则结点i无左孩子(结点/为叶子结点);否则其左孩子是结点2i。
(3)如果2j+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qz3mZxP8-1684047713011)(E:\笔记\笔记内图片\QQ截图20230513203644.png)]
二叉树的储存结构
二叉树的顺序储存结构
前面在讨论树的储存结构的时候是没有顺序储存结构的,因为一对多的特性导致在实现顺序结构的时候是特别困难的。但是二叉树因为其的特殊性(每个节点最多有俩个子树),所以二叉树的顺序储存的实现相对比较简单
完全二叉树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0JOlWsm0-1684047713012)(E:\笔记\笔记内图片\QQ截图20230513205053.png)]
因为完全二叉树的特殊性他可以很容易的存入数组并且可以根据下标判断其双亲和孩子之间的联系(详细看上面二叉树的性质五)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6BsZ0DcB-1684047713012)(E:\笔记\笔记内图片\微信图片_20230513205923.jpg)]
非二叉树
根据完全二叉树的储存思路我们可以将其他的二叉树近似的看成完全二叉树进行储存,具体看下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fpzaLMd5-1684047713012)(E:\笔记\笔记内图片\QQ截图20230513210407.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8XXWWuYp-1684047713012)(E:\笔记\笔记内图片\微信图片_20230513210617.jpg)]
这样有一个问题,在储存斜树的时候对空间的浪费是特别严重的,所有在使用顺序储存的时候一般为完全二叉树
二叉链表
因为顺序储存对部分二叉树不是特别方便,所以链式储存就又出现了;
思路和前面双亲表示法有一点像,不过在指针域将指向双亲的指针换为指向左右孩子的指针
typedef struct node
{
int data;
struct node* zuonode;
struct node* younode;
}node;
遍历二叉树
遍历原理
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。
次序:二叉树的遍历次序不同于线性结构,最多也就是从头至尾、循环、双向等简单的遍历方式。树的结点之间不存在唯一的前驱和后继关系,在访问一个结点后,下一个被访问的结点面临着不同的选择。
遍历方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ar5rgxZd-1684047713012)(E:\笔记\笔记内图片\QQ截图20230513221943.png)]
前序遍历:规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,
再前序遍历右子树。遍历的顺序为ABDGHCEIF。
中序遍历:规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。遍历的顺序为GDHBAEICF。
后序遍历:规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。遍历的顺序为GHDBIEFCA。
层序遍历:规则是若因为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。追历的顺序为ABCDEFGHI。
前序遍历法
这里利用递归实现对二叉树的遍历
typedef struct node
{
int data;
struct node* zuonode;
struct node* younode;
}node;//就是上面二叉链表节点
void qianxu(node t)
{
if(t==null)
{
return ;
}
printf("%d",t->data);
qianxu(t->zuonode);
qianxu(t->younode);
}
核心在于每到一个点,就完成对该点的访问(不一定是输出),然后先进入左子树进行下一步访问,当其一直向左向下遇到一个点没有左节点,开始访问他的右节点,然后依次向上,再次判断有无右节点,到达根节点时开始访问右边的结点。
中序遍历法
void zongxu(node t)
{
if(t==null)
{
return ;
}
qianxu(t->zuonode);
printf("%d",t->data);
qianxu(t->younode);
}
中序和前序在代码是没有本质区别,不过是将访问和向左树的递归进行了互换,代码的互换导致了在运行时代码是从根节点开始但是不是从根节点开始访问,中序遍历他会一直向左向下,直到遇到一个节点没有左子树他会开始访问该节点,然后向该点的右树进行递归。中序遍会一直向左向下递归在某个点没有左树,他会开始访问该节点,再递归右树,在右树进行相同的步骤,在该点右树处理结束时他会开始返回上一层继续步骤,直到全部访问结束。
后序遍历法
void zongxu(node t)
{
if(t==null)
{
return ;
}
qianxu(t->zuonode);
qianxu(t->younode);
printf("%d",t->data);
}
与上面类似,他会一直向左向下走,在某点没有左树开始走右树,到某点没有左树没有右树,访问该点,退回上一层,走下一步。
该文章有很多部分借鉴了《大话数据结构》,若没有看过建议买一本,讲的挺好的