笔记中图片均来源于课堂PPT
正在学习中,如有错误欢迎指正!
一、树的简介
树是以分支关系定义的层次结构,是族谱、社会组织机构一类实体的逻辑抽象。与前面介绍的数据结构不同的是,树是非线性结构。前面介绍的所有数据结构,无论是线性表、链表还是栈,它们的每个元素的前趋和后继都分别对应着一个且唯一一个元素,所以叫做线性结构,而树,顾名思义,开枝散叶就意味着一个前趋节点可能对应不止一个子元素,所以它是非线性的,之后我们会详细介绍。
二、树的术语
1.结点
每个数据元素对应着一个结点。结点可以分类为:分支节点、根节点等,除去第一层根节点外,其余各结点有且仅有一个前驱,可以有多个后继。当然,也存在只有前趋,没有后继的叶结点。
2.度与深度
某个结点的度意味着它最大的结点度,也就是说该结点出子树的最大数量。
树的深度为整棵树的高度,也就是说从第一层到最后一层的层数。
3.结点间的关系
现在观察这棵树,我们来说明结点之间的关系。
从A点出发,分叉而开的两个结点,我们叫做A的子节点。
箭头指向A点的,我们叫做A点的父节点。
第一层的根节点,跨越两层到达A点,我们把它叫做A的爷结点。
以此类推,可以根据亲缘关系,表达出各个结点的关系。
4.森林
森林指不相交的树的集合,也就是说两棵树是完全独立的。
三、二叉树引入
1.二叉树的简介
在各种形态的树中,我们主要研究二叉树这种特殊形态的树,它能很好的体现树的性质,而且是很典型的树的代表。二叉树是结点的有限集,或为空,或由根的左、右子树组成,左右子树又分别是符合定义的二叉树。各个形态的二叉树如下:
简单而形象的来说,二叉树就是每个结点分叉都是两条,子树可以为空的树。
下面介绍几种特殊的二叉树:
①满二叉树:所有的结点要么有两个孩子,要么一个也没有。所有的叶结点都位于同一层。
②完全二叉树:特殊的半满二叉树,最后一层节点从左至右依次排列,没有间断。
2.二叉树的性质
①在二叉树的第i层上最多有2^(i - 1)个结点—————— → 每个结点的子树都非空的完全二叉树
②深度为k的二叉树最多有(2^k - 1)个结点———— →除了根节点外,每个子树都对应两个子节点
③叶结点数比具有两个孩子的结点数多1个—————— →设变量证明:
④深度为K的满二叉树,结点个数为2k-1—————— →满二叉树的性质,画图可知
⑤具有n个节点的完全二叉树,深度为 [log2n]+1 —————— →完全二叉树的性质,证明略
四、二叉树的基本操作
二叉树的基本操作有:创建、遍历、插入、删除,其中遍历是考试的重点,插入和删除不考代码,理解理论。
注意:在二叉树的各种操作中,递归是核心思想。递归的一个形象例子是:从前有座山 山里有座庙 庙里有个老和尚讲故事 故事是 从前有座山……也就是说,问题的答案被自身调用就叫递归。
1.二叉树的定义
拥有了前面的知识储备,定义二叉树并不难。对于二叉树的某个结点来说,它的结构也应该分为元素域和指针域,元素域指向该点储存的元素,指针域分别指向它的左孩子和右孩子。和前面的链表不同的是,它的指针域不仅仅含有一个指针,分别指向两个下层元素的指针,而且它们是相互独立互不干扰的。对于该结点来说,它的左孩子和右孩子仍然为树,所以它们的结构也要定义为树的结构。
typedef struct bintree
{
int data;
struct bintree* lchild, * rchild;
}bintree;//二叉树的定义
2.二叉树的创建
二叉树其实有很多种创建方法,大体上也是根据递归思想一层层创建树,现在给出其中一种创建方法(源于网上),若我掌握了新的方法我也会更新此处的代码。
此处代码用到了遍历的顺序,可以先学习遍历的的三种方法再来看这部分代码。
bintree* creatBiTree()
{
char k;
scanf("%c", &k);
if (k == '#')//#代表空子树
return NULL;
bintree* T = (bintree*)malloc(sizeof(bintree));//分配下一个内存空间
T->data = k;
T->lchild = creatBiTree();
T->rchild = creatBiTree();//根左右,先根法
return T;
}
3.二叉树的遍历
二叉树广为学习的遍历方式有三种,它们分别是:中根遍历、先根遍历和后根遍历。
①中根遍历
方法如其名,中根遍历就是指对于某个结点来说,先遍历它的左子树,再遍历它的根节点,最后遍历它的右子树。也就是说,对于所有结点,它的顺序遵循着左根右的原则。
拆解分析一下中根遍历的顺序:首先从A出发→A的左子树B(此时还没有遍历B这个结点,而是要先把B的左子树遍历过再遍历B)→B的左子树C→C的左子树F→F没有左子树,所以遍历F结点→根据左根右的遍历顺序,遍历F的右子树H→回到C结点,C结点的左子树遍历结束,所以遍历C结点→C的右结点E(没有左子树所以直接遍历根节点)→H→B的左子树已经遍历完,所以遍历根节点B→遍历B的右结点D,因为D没有左子树,所以直接遍历D→然后遍历D的右子树I→I有左子树,所以首先遍历它的左子树J→遍历I→至此,A的左子树已经遍历完毕,现在遍历A结点→进入K结点→K有左子树,所以进入K的左子树L结点→L没有左子树,所以遍历L结点→进入L结点的右子树M结点→M有左子树,先遍历左子树N结点→N没有左子树,所以遍历N结点→N遍历结束,所以遍历N的右子树O结点→O有左子树P,所以遍历P→然后遍历根节点O→遍历右子树Q→回到M并遍历M→回到K并遍历K
刚开始理解可能有点困难,多分析几遍就好了,以下几种遍历方式同理。
②先根遍历
和中根遍历相似,只是顺序变化了。先根遍历是先遍历根结点,然后遍历它的左子树,最后遍历它的右子树。对于所有结点,遵循根左右的原则。
③后根遍历
和中根遍历相似,只是顺序变化了。后根遍历是先遍历该结点的左子树,然后遍历它的右子树,最后遍历根节点。对于所有结点,遵循左右根的原则。
这三种遍历都很重要,后面还会应用到二叉树的重构上,所以务必掌握!
下面我们来写这三种遍历的代码,其实,只要掌握了递归的思想,写出它们的代码是十分容易的。
①中根遍历
void zhonggen(bintree* T)
{
if (T)//对于每个非空结点
{
zhonggen(T->lchild);//递归先遍历左子树
printf("%2c", T->data);//然后遍历根节点
zhonggen(T->rchild);//最后遍历右子树
}
}
②先根遍历
void xiangen(bintree* T)
{
if (T)//T不为空
{
printf("%2c", T->data);//先遍历根节点
xianxu(T->lchild);//然后遍历左子树
xianxu(T->rchild);//最后遍历右子树
}
}
③后根遍历
void hougen(bintree* T)
{
if (T)
{
hougen(T->lchild);//先遍历它的左子树
hougen(T->rchild);//然后遍历它的右子树
printf("%2c", T->data);//最后遍历根节点
}
}
4.二叉树的插入
要在当前的树中插入新元素,要考虑它在哪个结点、这个结点的左还是右孩子插入新元素,通常上,我们选择和遍历同样的方法进行插入操作。
举个例子:要在根节点和右子树之间插入一个新元素,现在需要确定的是原来的右结点会成为新插入结点的左子树还是右子树,从图中可以看出,现在使用的顺序是中根遍历,所以插入元素之后,再读取所有元素时也要用中根遍历的顺序,并且插入元素和原来元素的相对位置不能改变。可能说的有点抽象,不如直接看例子。
对于该插入有两种插入方式 ,一种是把右子树插入到新元素结点的左孩子中:
另一种是把右子树插入到新元素结点的右孩子中:
那么究竟选哪种插入方式呢?刚才我们提到插入时,选择遍历顺序也一致的方法。这句话的意思就是:我们把新元素插入到根节点和右子树之间,原来遍历的顺序为:左 根 右,所以插入元素后遍历的顺序也要是左、根、new、右,在这个原则下,我们选择第二种插入方式。
5.二叉树的删除
和插入类似,当我们删除某个结点之后,要考虑的是删除结点的子结点应该怎么挂在爷结点身上。同理,剩余结点要符合原来的遍历顺序,举个例子:
没有删除结点之前的遍历顺序为:爷 左 del 右,对于删除之后的结点挂法,有两种可能:
第一种方法,把删除结点的左结点挂在爷结点身上,右结点作为左结点的子树,遍历顺序为 爷左右,符合删除结点后应该具有的顺序,成立。
第二种方法,将右结点挂在爷结点身上,左结点作为右结点的子树,这种删除方式过后,得到的遍历顺序是:爷右左,不符合未删除结点之前的顺序,故不采用。
树的基础知识就这么多啦,还有树的重构,哈夫曼树等等没有写,等考试结束后再把拓展的东西贴上来。