数据结构:树&&二叉树

不积跬步,无以至千里;不积小流,无以成江海。只有不断持之以恒的学习才能成功,学计算机也是,这期博客内容主要为树的与二叉树的概念,链式二叉树的一些基本操作的实现,主要偏文字叙述比较多因为都是自己的一些理解看法,都是干货,希望大家能够喜欢,下面我们开始本期博客的内容。

(一)树

1. 树的定义

树(tree)是n(n大于等于0)个结点的有限集,它或为空树(n=0),或为非空树。对于非空树T:

(1)有且仅有一个称之为根的结点

(2)除根结点以外的其余结点可分为m(m>0)个互不相交的有限集T1,T2,...,Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)

如下图5.1(b)所示,有同学就会疑惑了,不是说树吗,这哪里像树,你别急,你把图片倒过来看看,是不是有那味了,结点A相当于树根,其他的结点和线看成树枝和树叶,是不是一棵树,这里的A结点就是这棵树的根结点,而B、C、D这三个结点为A的子树,以此类推,这些结点除了A结点外都可以看成子树同样也可以看成根,具体情况具体分析。

树的结构定义是一个递归的定义,即在树的定义中又用到树的定义,它道出了树的固有特性。树还有其他表示形式,如下图所示:

图5.2所示为图5.1(b)中树的各种表示。其中图5.2(a)是以嵌套集合(即一些集合的集体,对于其中的任何两个子集,或者不相交,或者一个包含另一个)的形式表示的;图5.2(b)是以广义表的形式表示的,根作为由子树森林组成的表的名字写在表的左边;图5.2(c)用的是凹入表示法(类似书 的编目)。表示方法的多样化,正说明树结构在日常生活中及计算机程序设计的重要性。一般来说,分等级的分类方案都可以用层次结构来表示,也就是说。都可由树结构来表示。

下面介绍树结构的一些基本术语

2. 树的基本术语 

(1)结点:树中的一个独立单元。包含一个数据元素及若干指向其子树的分支,如图5.1(b)中的A、B、C、D等。(下面术语均以图5.1(b)为例来说明)

(2)结点的度:结点拥有的子树数目称为结点的度。例如,A的度为3,C的度为1,F 的度为0。

(3)树的度:树的度是树内个结点度的最大值。如图5.1(b)树的度为3。

(4)叶子:度为0的结点称为叶子或终端结点。结点K、L、F、G、M、I、J都是树的叶子结点。

(5)非终端结点:度不为0的结点称为非终端结点或分支结点。除根结点和叶子结点之外,非终端结点也称为内部结点。

(6)双亲和孩子:结点的子树的根称为该结点的孩子,相应的,该结点称为孩子的双亲,如图中B的双亲为A,而A的孩子为B、C、D,B的孩子为E、F。

(7)兄弟:同一个双亲的孩子之间叫兄弟,其实跟现实的称呼差不多的感觉,像图中的K、L就是兄弟,共同的父母是E。

(8)祖先:从根到该结点所分支上的所有结点。例如,M的祖先是A、D 和H。

(9)子孙:以某结点为根的子树中的任一结点的称为该结点的子孙,如B的子孙为E、K、L和F。

(10)层次:结点的层次从根的开始定义,根为第一层,根的孩子为第二层。树中任一结点的层次等于其双亲结点的层次加1。

(11)堂兄弟:有兄弟肯定还有堂兄弟,双亲在同一层的结点互为堂兄弟。如:结点G与E、F、H、I、J互为堂兄弟。

(12)树的深度:树中的结点的最大层次称为树的深度或者高度。如图5.1(b)的深度为4。

(13)有序和无序树:如果将树中结点的各子树看成从左到右是有次序的(不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子最右边的称为最后一个孩子。

(14)森林:m(m>0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此,也可以用森林和树相互递归的定义来描述树。

3.树的表示

上述树的理论部分已经说完了接下来就是代码实现了,对于树的结构通过上述的表述无非就是双亲与孩子之间的关系,那我们可以定义一个结构体存放每个结点的孩子情况。

typedef int DataType;
typedef struct TreeNode
{//确定结点与孩子结点的关系
	DataType data;//结点所存的数据
	//所有的孩子结点
	struct TreeNode* child1;
	struct TreeNode* child2;
	struct TreeNode* child3;
	struct TreeNode* child4;
	struct TreeNode* child5;
	struct TreeNode* child6;
	//......
};

但显然这样的表示是不合理的,因为假如我们不知道一个结点的度那就不知道这个结点的还在有多少,那就有问题了。

或者我们定义一个指针数组或一个顺序表来存储结点的孩子结点。

#define N 10
typedef int DataType;
struct TreeNode
{
	//数据
	DataType data;
	//指针数组
	struct TreeNode* children[N];
};
typedef int DataType;
typedef struct TreeNode* SLDataType
struct TreeNode
{
	//数据
	DataType data;
	//指针顺序表
	Seqlist children;
};

但显然这太过于麻烦,那有什么比较好的方法呢,这里就引入了孩子兄弟表示法 。即定义一个结构体表示结点,结构体有一个数据域和两个指针,一个是指向该结点的第一个孩子结点的指针,另一个是指向该结点的兄弟结点的指针。

typedef int DataType;
struct TreeNode
{
	//数据
	DataType data;
	struct TreeNode* child;//指向第一个孩子节点
	struct TreeNode* brother;//指向其兄弟节点
};

具体的连法可以看下图: 

 

(二)二叉树

1. 二叉树的定义

二叉树顾名思义就是每个点最多只有两个叉的树,也就是树的度为为2,下图就是一个二叉树。 

 二叉树与树一样具有递归性质,二叉树与树的区别主要有以下两点:

(1)二叉树每个结点至多只有两棵子树(二叉树中不存在度大于2的结点);

(2)二叉树的子树有左右之分,其次序不能任意颠倒,所以二叉树是有序树。

拓:上述树的术语都适用于二叉树。

 

2. 二叉树的性质 

性质1:在二叉树的第i层上至多有2^i-1(i>=1)个结点。

性质2:深度为k的二叉树至多有2^k-1(k>=1)个结点。 

性质3:对任何一棵二叉树T,如果其叶子结点数为n0,度为2的结点数为n2,则n0=n2+1。

性质4:具有n个结点的完全二叉树的深度为[log2n]+1。

性质5:若对一棵有n个节点的完全二叉树进行顺序编号(1≤i≤n),那么,对于编号为i(i≥1)的节点: 

当i=1时,该节点为根,它无双亲节点 。

当i>1时,该节点的双亲节点的编号为i/2 。

若2i≤n,则有编号为2i的左节点,否则没有左节点 。

若2i+1≤n,则有编号为2i+1的右节点,否则没有右节点 。

3. 特殊的二叉树 

满二叉树:深度为k且含有2^k-1个结点的二叉树,其特点是每一层上的结点是数都是最大结点数,即每一层i的结点数都具有最大值2^i-1。

下图5.6 (a)所示的是一棵深度为4的满二叉树。

此处可以对满二叉树的结点进行连续编号,约定编号从根结点起,自上而下,自左向右。由此可引出完全二叉树的定义。

完全二叉树 :深度为k、有n个结点的二叉树,当且仅当其每个结点都与深度的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。

详见上图5.6(b),其表示一棵深度为4的完全二叉树。

完全二叉树相较于满二叉树,最后一层结点不一定要全满但一定要连续就是所对应的编号不能断,不然就不是完全二叉树,而满二叉树是完全二叉树的一种特例,只要是满二叉树则必为完全二叉树。

4. 二叉树的存储结构 

类似线性表,二叉树的存储结构也可采用顺序存储和链式存储两种方式。

顺序存储

 顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆(一种完全二叉树)才会使用数组来存储,关于堆在下面会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面的高阶数据结构如红黑树等会用到三叉链。

5. 二叉树的遍历 

二叉树的拆解

首先在讲遍历前先讲二叉树的拆解,就拿下面这个二叉树为例吧。

第一次拆解,可拆解为 ,

其中根为:

左子树为 :

右子树为: 

第二次拆解,将拿到的左子树进行拆解,可拆成

其中根为: 

左子树为:

右子树为: 

第三次拆解,我们这时发现第二次拆解的左子树还可以拆解。

拆解后根为 :

左子树:为NULL,空树不能再进行拆解。

右子树:为NULL,空树不能再进行拆解。

第四次拆解,因为总的左子树已经拆解完毕,那我们就往回退,此时发现B结点的右子树咱还没拆,所以我们拆B的右子树。

其中根为:

左子树:为NULL,空树不能再进行拆解。

右子树:为NULL,空树不能再进行拆解。

第四次拆解,此时B结点左右子树全都拆解完毕,就相当于B结点拆解完成,咱再往回走会发现我们拆完了A结点的左子树,但右子树没拆,那我们就拆A结点对应的右子树,也就是

拆解后根为: 

左节点:为NULL,空树不能再继续拆解了。

右节点:

 第五次拆解,拆C结点的右子树。

根为:

左子树:为NULL,空树不能再继续拆解了。

右子树:为NULL,空树不能再继续拆解了。

这样就全部拆解完毕了,相当于每个结点分区自制一样,只不过结点A是老大头头,其他是各个区的区长。

二叉树的前序遍历

还是这个图,我们对它进行前序遍历,前序遍历的原则根左右 ,这里的根把它理解为数据比较方便一点,为什么这么说呢,遍历过后咱就知道了,上述图首先进入肯定是从A这个结点开始对吧,因为是前序遍历,所以先不管三七二十一,咱先输出该结点对应的数据A,而这里输出数据就代表根了,根之后咱要怎么遍历,根左右,根左右对吧,往左,那么往左就是朝着A结点对应的左子树遍历,那么首先映入眼帘的是谁,B结点对不对,那我们先在到了B结点这个位置,那么熟悉的话右来了,根左右,根左右,现在这里的B是不是根,根我们要干嘛,输出数据对不对,那就输出B结点对应的数据B,好,现在已经输出了AB了,那么现在根弄完了,下一个是什么,根左右,那肯定找B结点的左子树啦,好,那我们现在指到B结点的左子树D,那么还是那句话,根左右,我们此时指向D结点这个位置,它此时是不是一个根按照上面的拆解,是根那咱要干什么,输出数据对不对,好,咱输出C,此时我们已经输出了ABD了对吧,好根左右,根左右,我们是不是要找此时这个D根结点的左子树了对吧,哦豁,D结点的左子树没了是个空树,那别急,还是那句话,根左右,既然左不行我不是还有右子树吗对吧,但是这里右子树也没了,不要慌那说明什么,说明B结点的左子树咱遍历完了到头了呀你说是不是,那么根据根左右,我们下一步该干什么,根B结点搞过,B结点的左子树遍历完了,现在是不是还有个右子树咱没弄,那就找右子树,B结点的右子树是不是E,好我们现在跳到E结点这个位置,根左右根左右,此时E结点不就是根结点吗,那我们就输出E结点对应的数据E,好,此时我们以及输出了ABDE,此时根弄完了,是不是左了,但这里的E结点对应的左子树为空,那就到右呗,哦?右子树也为空,那就相当于E结点遍历完了,再回到B结点,此时的B结点的左右子树都遍历完了,那就意味着B结点已经遍历完成了,好那我们就往回走,就相当于A的左子树搞完了根据根左右,现在是不是到右子树了,A结点的右子树是什么,是C对吧好,此时C结点变成结点了吧,根左右只要是根咱就无脑输出,输出C结点所对应的数据,好现在已经输出了ABDEC了,然后发现它没有左子树,没有没关系我们找它的右子树,右子树是谁,是不是T结点,又是变成根,那我们就无脑输出,现在是不是输出变成了ABDECT了,好再往下看T结点的左子树,空,好找右子树好,那T结点就遍历完了,往上相当于C结点遍历完了,再往上,此时A结点所对应的左子树和右子树是不是我们都遍历完了,这就是前序遍历。

链式二叉树的结点的创建:

typedef char Datatype;//定义树结点所存数据的数据类型,此处的数据;类型不仅仅局限于char类型,这里知识举例子
typedef struct BinaryTreeNode
{//确定二叉树的结点结构
	Datatype data;//二叉树结点中所存的数据
	struct BinaryTreeNode* childl;//结点对应的左孩子的位置
	struct BinaryTreeNode* childr;//结点对应的右孩子的位置
}BTNode;

代码展示,这里我采用的是递归的算法:

void PrevOder(BTNode* root)
{//二叉树的前序遍历
	if (root == NULL)
	{//当传入的树为空时直接返回结束
		printf("NULL");
		return;
	}
		printf("%d\n", root->data);//还记得我们把根比作数据,这里的输出数据就相当于遍历了根结点
		PrevOder(root->childl);//这里通过递归遍历左子树
		PrevOder(root->childr);//这里通过递归遍历右子树
}
二叉树的中序遍历

中序遍历和前序遍历的区别在于它是左根右,就是先遍历左子树再遍历根结点(这里遍历根结点按照上面的意思理解成输出结点所对应的数据更好,下面的根结点的遍历均为数据的输出,就不在一一叙述了),还是这个二叉树,这次我们用中序遍历来输出这个二叉树的值:

 首先我们从A结点进入对不对,然后记住中序遍历的法则是左根右,既然是左根右,那么我们肯定先要找当前A结点的左子树,我们可以看到左子树是B结点,所以我们就跳到B结点这个位置,然后还是那句话左根右,好那我们就要找此时B结点的左子树,好我们看到,它对应的左子树是D结点,左子树说的还是有点不对,这里一个说左孩子要好一点,我们找到B结点的左孩子现在我们要干什么,左根右,继续找D的左孩子,但我们看到D没有左孩子,所以D结点的左边就到头,那么根据左根右,现在是什么,是根,根不用再说需要干什么了吧,输出D结点所对应的数据也就是D,然后在看D结点的右孩子哦豁,没有,没有代表什么,代表D结点的右子树遍历完毕了,好现在D结点相当于遍历完了, 对于B结点相当于左子树遍历完了,左根右,左边完了下一个是什么,是根了对吧,那就输出B,好现在已经输出了DB了,现在按照左根右根搞完了,下一个就是右,那就找B结点的右孩子,B结点的右孩子是E结点对不对,好我们到了一个新结点第一件事是什么,左根右左根右,找当前结点的左孩子,但可以知道E结点的左边为空,就意味着E结点的左子树为空,为空到头咱就换根嘛,换成根不就输出嘛,好输出E,现在我们已经输出了DBE了,好现在就相当于B结点的左子树和右子树都遍历完了,对于A结点来说意味着什么,意味着A结点的左子树遍历完了,好左既然完了对于A结点是不是就到根了,那我们就输出A,根之后是右对吧,那就找A结点的右孩子,是C,好找C的左孩子,左根右嘛,没有对吧,输出C然后找右孩子,右孩子是T对吧,好,我们再找T结点的左孩子,没有,输出T,再找右孩子,没有好。A结点的右子树遍历完成,整个树遍历完成。输出的序列是DBEACT。

具体代码实现:

void InOder(BTNode* root)
{//二叉树的中序遍历
	if (root == NULL)
	{//当传入的树为空时直接返回结束
		printf("NULL");
		return;
	}
	InOder(root->childl);//这里通过递归遍历左子树
	printf("%d\n", root->data);//还记得我们把根比作数据,这里的输出数据就相当于遍历了根结点
	InOder(root->childr);//这里通过递归遍历右子树
}

可以看出来,跟前序遍历的代码一模一样除了函数名不一样,还有一个不一样的地方是后面的三条语句的顺序,你看,前序的是根左右,输出就在前,左子树遍历就在第二个,右子树的遍历就在第三个,中序是左根右,输出就在中间,左子树的遍历就在第一个,右子树的遍历就在最后,是不是很神奇,既然知道了这样的规律,那后续遍历不就直接出来了吗。

二叉树的后续遍历 

后续遍历是左右根,既然知道左右跟一个就差不多能根据上面的两个遍历照葫芦画瓢做了吧,当然还是这个图:

 既然是左右根那首先我们的起点依然是A结点,左右根嘛,那我们就找A结点的左孩子,左孩子是谁,B结点对吧,好我们就跳到B结点,还是左右根,先找B结点的左孩子,有,谁,D结点对吧,好我们就跳到D,然后我们再找D结点的左孩子,嗯没有对吧,好左右根,找D结点的右孩子,嗯?也没有,没关系,到根,根是干什么滴,输出输出,好我们这里就输出D,好D搞完意味着什么,意味着B结点的左子树遍历完了,现在根据左右根下一步该干什么了,找B结点的右孩子,有木有呢,有,是E结点,好我们直接跳到E,然后左右根,找E结点的左孩子,有没有,没有,好,那就找它的右孩子,有木有,也没有好,找根,根是什么,输出对吧,好,咱输出E,好目前为止我们输出了DE,现在B的右子树也遍历完了,左右都有剩下一个谁,根对吧,好那就输出B,现在我们已经输出了DEB了,接下来就不一一细讲了,最后按照以上的步骤往复,得到的输出序列是DEBTCA。

代码实现:

void PostOrder(BTNode* root)
{//二叉树的后序遍历
	if (root == NULL)
	{//当传入的树为空时直接返回结束
		printf("NULL");
		return;
	}
	PostOrder(root->childl);//这里通过递归遍历左子树
	PostOrder(root->childr);//这里通过递归遍历右子树
	printf("%d\n", root->data);//还记得我们把根比作数据,这里的输出数据就相当于遍历了根结点
}

除了函数名,唯一不一样的就是位置,这个规律如果理解了的话就很方便记。

6. 二叉树的结点个数的求取 

在求二叉树节点个数时,我们不能单一定义一个局部变量来实现,也不能定义一个静态变量来统计节点个数(静态变量除第一次再被函数调用时初始值是不一样的),在这里我们使用全局变量来实现:

int size = 0;//定义一个全局变量size
void CountTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	size++;
	CountTreeSize(root->childl);
	CountTreeSize(root->childr);//用递归遍历结点,每遍历一次就让size值加一
}
void TreeSize(BTNode* root)
{
	size = 0;//防止多次调用时初始值发生改变
	CountTreeSize(root);
}

当然这里还有一个比较牛的方法

int TreeSize2(BTNode* root)
{
	return root == NULL ? 0 : TreeSize2(root->Left) + TreeSize2(root->Right) + 1;
}

拓:这里的?: 我说明一下这是一个三目运算符,可以将它看成一个选择分支结构,正常的选择分支是由if(判断条件),else(否则),而对于这个三目运算符,?的前面是判断条件,相当于if()括号里面的表达式,如果前面的表达式满足则执行后面的那个语句否则执行后面的表达式。

好解释完后我们来分析这个算法是什么个原理,我们来画图分析一下吧,依然是这个图,不要问为什么,因为好用:

 

这个图不知道大家能不能看得明白,我是按照我的理解画的,具体是什么个意思呢,刚开始我们是不是传入根结点A,根据判断,不为空,那么我们就进行“:”后面的语句, 也就是这个TreeSize2(root->Left) + TreeSize2(root->Right) + 1,但执行前我们得算出TreeSize2(root->Left)这个,这个是什么,这个相当于吧B结点传入这个函数,然后执行判断,非空,再执行这个语句TreeSize2(root->Left) + TreeSize2(root->Right) + 1,但在执行前我们要求TreeSize2(root->Left)这个时候传入的就是B结点的左孩子结点也就是D,好再执行“:”后的语句,我们可以求得D的左孩子为空,那么根据判断我们要返回一个0,再判断右孩子,同样也是空,那么返回一个0,那么此时D结点对应的值是0+0+1=1,按照上述的分析方法,求得E结点的值也为0+0+1=1,T结点的值为1,B结点的值就是左孩子结点的值加右孩子结点的值+1,TreeSize2(root->Left) + TreeSize2(root->Right) + 1就是这个式子,所以B结点的值为1+1+1=3,C结点的值为0+1+1=2,好A结点的值就是2+3+1=6,这里的6就是结点的个数,我们可以把这种过程理解为,一个老大,要知道他们一个帮子有多少人,这里的A就是老大,那老大就要问帮子里的组长,这里的组长就是结点B、C,而他们要问人数是不是得问下面的人,也就是组员,就是D、E、T,问完收集人数信息后加一是因为要算上自己所以要加一,最后知道所有的名单都收集完毕,跟老大讲,老大统计好后加上自己就是加一就是总人数。

7. 求二叉树的高度

 

int TreeHeight(BTNode* root)
{//判断二叉树的深度
	if (root == NULL)
		return 0;
	int LeftHeight = TreeHeight(root->childl);//记录左子树深度
	int RightHeight = TreeHeight(root->childr);//记录右子树深度
	return LeftHeight > RightHeight ? LeftHeight + 1 : RightHeight + 1;//返回较大子树的深度
}

这里也是运用递归的思想,分析要画图才能更好的理解:

 

主打的一个递归算法的一个运用,再一个就是理解。 

本次博客主要介绍的是二叉树的一些相关的用法,包括一些递归思想的理解,下期的博客给大家带来二叉树剩下的操作及代码,以及八大排序的堆排和希尔排序等排序的运用,感谢各位看官的收看,欢迎大家的批评指正,我们下次再见。

 

 

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值