(学会二叉树) 二叉树基础

目录

〇. 关于树的一些相关术语

一. 二叉树的定义

 1.二叉树的特点

2.特殊二叉树

1.斜树 

2.满二叉树

3.完全二叉树 

二. 二叉树的性质

1.二叉树的性质1

2.二叉树的性质2

3.二叉树的性质3

4.二叉树的性质4

5.二叉树的性质5

 三.二叉树的存储结构

1.二叉树顺序存储结构

 2.二叉链表

四.遍历二叉树

1.二叉树遍历原理

2.二叉树的遍历方法

(1)前序遍历

(2)中序遍历

(3)后序遍历

 (4)层序遍历

3.前序遍历算法

4.中序遍历算法

5.后序遍历算法

6.推导遍历结果

五.部分功能的C语言代码实现 


〇. 关于树的一些相关术语

①节点:包含一个数据元素及若干指向子树分支的信息 。

②节点的度:一个节点拥有子树的数目称为节点的度 。

③叶子节点:也称为终端节点,没有子树的节点或者度为零的节点 。

④分支节点:也称为非终端节点,度不为零的节点称为非终端节点 。

⑤树的度:树中所有节点的度的最大值 。

⑥节点的层次:从根节点开始,假设根节点为第1层,根节点的子节点为第2层,依此类推,如果某一个节点位于第L层,则其子节点位于第L+1层 。

⑦树的深度:也称为树的高度,树中所有节点的层次最大值称为树的深度 。

⑧有序树:如果树中各棵子树的次序是有先后次序,则称该树为有序树 。

⑨无序树:如果树中各棵子树的次序没有先后次序,则称该树为无序树 。

⑩森林:由m(m≥0)棵互不相交的树构成一片森林。如果把一棵非空的树的根节点删除,则该树就变成了一片森林,森林中的树由原来根节点的各棵子树构成 。

(标红即为重要概念)

一. 二叉树的定义

二叉树(Binary Tree)是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。

我们不妨看一个例子:

我在纸上已经写好了一个100以内的正整数数字,请大家想办法猜出我写的是哪一个?注意你们猜的数字不能超过7个,我的回答只会告诉你是“大了”或“小了”。

这个游戏在一些电视节目中,猜测一些商品的定价时常会使用。我看到过有些人是一点一点的数字累加的,比如5、10、15、20 这样猜,这样的猜数策略太低级了显然是没有学过数据结构和算法的人才做得出的事。

其实这是一个很经典的折半查找算法。如果我们用下图(下三层省略)的办法,就一定能在7次以内,猜出结果来。

由于是 100 以内的正整数,所以我们先猜50(100的一半),被告之“大了”,于是再猜 25(50的一半),被告之“小了”,再猜37(25与50的中间数),小了,于是猜 43,大了,40,大了,38,小了,39,完全正确。过程如下列表格所示。

 我们发现,如果用这种方式进行查找,效率高得不是一点点。对于折半查找的详细讲解,我们后面再说。不过对于这种在某个阶段都是两种结果的情形,比如开和关、0和 1、真和假、上和下、对与错,正面与反面等,都适合用树状结构来建模,而这种树是一种很特殊的树状结构,叫做二叉树。

 1.二叉树的特点

二叉树的特点有:

● 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。

● 左子树和右子树是有顺序的,次序不能任意颠倒。就像人是双手、双脚,但显然左手、左脚和右手、右脚是不一样的,右手戴左手套、右脚穿左鞋都会极其别扭和难受。

● 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。下图中,树1和树2是同一棵树,但它们却是不同的二叉树。就好像你一不小心,摔伤了手,伤的是左手还是右手,对你的生活影响度是完全不同的。

二叉树的五种基本形态:

1.空二叉树

2.只有一个根结点

3.根节点只有左子树

4.根节点只有右子树

5.根节点既有左子树又有右子树

应该说这五种形态还是比较好理解的,那我现在问大家,如果是有三个结点的树,有几种形态?如果是有三个结点的二叉树,考虑一下,又有几种形态?

若只从形态上考虑,三个结点的树只有两种情况,那就是下图中有两层的树1和有三层的后四种的任意一种,但对于二叉树来说,由于要区分左右,所以就演变成五种形态,树 2、树3、树4和树5分别代表不同的二叉树。 

2.特殊二叉树

我们再来介绍一些特殊的二叉树。这些树可能暂时你不能理解它有什么用处,但先了解一下,以后会提到它们的实际用途。

1.斜树 

顾名思义,斜树一定要是斜的,但是往哪斜还是有讲究。所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。上图中的树2就是左斜树,树5就是右斜树。斜树有很明显的特点,就是每一层都只有一个结点,结点的个数与二叉树的深度相同。

有人会想,这也能叫树呀,与我们的线性表结构不是一样吗。对的,其实线性表结构就可以理解为是树的一种极其特殊的表现形式。

2.满二叉树

苏东坡曾有词云:“人有悲欢离合,月有阴睛圆缺,此事古难全”。意思就是完美是理想,不完美才是人生。我们通常举的例子也都是左高右低、参差不齐的二叉树。那是否存在完美的二叉树呢?

嗯,有读者已经在空中手指比划起来。对的,完美的二叉树是存在的。 

在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。

下图就是一棵满二叉树,从样子上看就感觉它很完美。

单是每个结点都存在左右子树,不能算是满二叉树,还必须要所有的叶子都在同一层上,这就做到了整棵树的平衡。因此,满二叉树的特点有:

(1)叶子只能出现在最下一层。出现在其他层就不可能达成平衡。

(2)非叶子结点的度一定是 2。否则就是“缺胳膊少腿”了。

(3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。 

3.完全二叉树 

对一棵具有 n个结点的二叉树按层序编号,如果编号为i(1<i<n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树,如下图所示。

这是一种有些理解难度的特殊二叉树。

首先从字面上要区分,“完全”和“满”的差异,满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满的。

其次, 完全二叉树的所有结点与同样深度的满二叉树, 它们按层序编号相同的结点, 是一一对应的。这里有个关键词是按层序编号,像下图中的树1,因为5结点没有左子树,却有右子树,那就使得按层序编号的第 10个编号空档了。同样道理,下图中的树2,由于3 结点没有子树,所以使得 6和7 编号的位置空档了。下图中的树3 又是因为5编号下没有子树造成第10 和第11 位置空档。只有上图中的树,尽管它不是满二叉树,但是编号是连续的,所以它是完全二叉树。 

从这里我也可以得出一些完全二叉树的特点:

(1)叶子结点只能出现在最下两层。

(2)最下层的叶子一定集中在左部连续位置。倒数二层,若有叶子结点,一定都在右部连续位置

(3)如果结点度为 1,则该结点只有左孩子,即不存在只有右子树的情况。

(4)同样结点数的二叉树,完全二叉树的深度最小。

从上面的例子,也给了我们一个判断某二叉树是否是完全二叉树的办法,那就是看着树的示意图,心中默默给每个结点按照满二叉树的结构逐层顺序编号,如果编号出现空档,就说明不是完全二叉树,否则就是。

二. 二叉树的性质

二叉树有一些需要理解并记住的特性,以便于我们更好地使用它。

1.二叉树的性质1

性质1:在二叉树的第i层上至多有2^{i-1}个结点(i\geqslant 1)。
这个性质很好记忆,观察一下下图


第一层是根结点,只有一个,所以2^{1-1} = 2^{0} = 1

第二层有两个, 2^{2-1} = 2^{1} = 2

第三层有四个,2^{3-1} = 2^{2} = 4

第四层有八个,2^{4-1} = 2^{3} = 8

通过数据归纳法的论证,可以很容易得出在二叉树的第i层上至多有2^{i-1}个结点(i\geqslant 1)的结论。

2.二叉树的性质2

性质2: 深度为k的二叉树至多有2^{k} - 1个结点(k\geqslant 1)

由等比数列计算公式, 可以得出此结论

注意这里一定要看清楚,是 2^{k}后再减去1,而不是 2^{k-1}。以前很多同学不能完全理解,这样去记忆,就容易把性质2与性质1给弄混淆了。

3.二叉树的性质3

性质 3:对任何一棵二叉树 T,如果其终端结点数为n_0,度为 2的结点数为 n_2, 则n_0 = n_2 + 1

终端结点数其实就是叶子结点数, 而一棵二叉树,除了叶子结点外,剩下的就是度为1或2的结点数了,我们设n_1为度是1的结点数。则树T结点总数n = n_0 + n_1 + n_2

比如下图的例子,结点总数为 10,它是由 A、B、C、D 等度为2 结点,F、G、H、I、J等度为0的叶子结点和E这个度为1的结点组成。总和为 4+1+5=10。

我们换个角度,再数一数它的连接线数,由于根结点只有分支出去,没有分支进入,所以分支线总数为结点总数减去1。上图就是9个分支。对于A、B、C、D结点来说,它们都有两个分支线出去,而E结点只有一个分支线出去。所以总分支线为4x2+1x1=9。 

用代数表达就是分支线总数= n - 1 = n_1 + 2n_2。因为刚才我们有等式n_0 = n_2 + 1,所以可推导出n_0 + n_1 + n_2 - 1 = n_1 + 2n_2。结论就是 n_0 = n_2 + 1

4.二叉树的性质4

性质 4:具有n个结点的完全二叉树的深度为[log_2(n)] + 1([x]表示不大于x的最大整数)。

由满二叉树的定义我们可以知道,深度为k 的满二叉树的结点数 n一定是 2^k -1。因为这是最多的结点个数。那么对于n = 2^k - 1推得到满二叉树的度数为k = log_2(n + 1),比如结点数为 15 的满二叉树,度为 4。 

完全二叉树我们前面已经提到,它是一棵具有n个结点的二叉树,若按层序编号后其编号与同样深度的满二叉树中编号结点在二叉树中位置完全相同,那它就是完全二叉树。也就是说,它的叶子结点只会出现在最下面的两层。

它的结点数一定少于等于同样度数的满二叉树的结点数2^k - 1,但一定多于 2^{k-1} - 1。即满足 2^{k-1} - 1 < n \leqslant 2^k - 1。由于结点数n是整数,n\leqslant 2^k - 1意味着n < 2kn > 2^{k-1} - 1,意味着 n \geqslant 2^{k-1},所以 2^{k-1} \leqslant n < 2^k,不等式两边取对数,得到k - 1 \leqslant log_2n < k,而k作为度数也是整数,因此k = log_2(n + 1)

5.二叉树的性质5

性质 5:如果对一棵有n个结点的完全二叉树(其深度为[log_2n] + 1)的结点按层序编号(从第1层到第[log_2n] + 1层,每层从左到右),对任一结点 i (1\leqslant i\leqslant n)有:

1.如果 i=1,则结点 i 是二叉树的根,无双亲; 如果i>1,则其双亲是结点[i/2]。

2.如果 2i>n,则结点i无左孩子 (结点 i 为叶子结点); 否则其左孩子是结点2i。

3.如果 2i+1>n,  则结点 i 无右孩子;否则其右孩子是结点2i+1。

我们以下图为例,来理解这个性质。这是一个完全二叉树,度为4,结点总数是 10。

对于第一条来说是很显然的,i-1时就是根结点。i>1时,比如结点7,它的双亲就是[7/2]=3,结点9,它的双亲就是[9/2]=4。

第二条,比如结点6,因为 2x6=12 超过了结点总数 10,所以结点6无左孩子,它是叶子结点。同样,而结点5,因为2x5=10正好是结点总数10,所以它的左孩子是结点 10。

第三条,比如结点 5,因为2x5+1=11,大于结点总数 10,所以它无右孩子。而结点 3,因为 2x3+1=7 小于 10,所以它的右孩子是结点 7。 

 三.二叉树的存储结构

1.二叉树顺序存储结构

前面我们已经谈到了树的存储结构,并且谈到顺序存储对树这种一对多的关系结构实现起来是比较困难的。但是二叉树是一种特殊的树,由于它的特殊性,使得用顺序存储结构也可以实现。

二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等。

先来看看完全二叉树的顺序存储,一棵完全二叉树如下图所示。

将这棵二叉树存入到数组中,相应的下标对应其同样的位置,如下图所示。 

这下看出完全二叉树的优越性来了吧。由于它定义的严格,所以用顺序结构也可以表现出二叉树的结构来。

当然对于一般的二叉树,尽管层序编号不能反映逻辑关系,但是可以将其按完全二叉树编号,只不过,把不存在的结点设置为“Λ”而已。如下图,注意浅色结点表示不存在。 

 考虑一种极端的情况,一棵深度为k的右斜树,它只有k个结点,却需要分配 2k-1个存储单元空间,这显然是对存储空间的浪费,例如下图所示。所以,顺序存储结构一般只用于完全二叉树。

 2.二叉链表

既然顺序存储适用性不强,我们就要考虑链式存储结构。二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。结点结构图如下表所示。

其中 data 是数据域,kchik 和rchid 都是指针域,分别存放指向左孩子和右孩子的指针

 以下是我们的二叉链表的结点结构定义代码。

结构示意图如下图所示

就如同树的存储结构中讨论的一样,如果有需要,还可以再增加一个指向其双亲的指针域,那样就称之为三叉链表。由于与树的存储结构类似,这里就不详述了。 

四.遍历二叉树

1.二叉树遍历原理

假设,我手头有20张100元的和2000张1元的奖券,同时酒向了空中,大家比赛看谁最终捡的最多。如果是你,你会怎么做?

相信所有同学都会说,一定先捡100元的。道理非常简单,因为捡一张100元等于1元的捡100张,效率好得不是一点点。所以可以得到这样的结论,同样是捡奖券,在有限时间内,要达到最高效率,次序非常重要。对于二叉树的遍历来讲,次序同样显得很重要

二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。

这里有两个关键词: 访问次序

访问其实是要根据实际的需要来确定具体做什么,比如对每个结点进行相关计算,输出打印等,它算作是一个抽象操作。在这里我们可以简单地假定就是输出结点的数据信息。

二叉树的遍历次序不同于线性结构,最多也就是从头至尾、循环、双向等简单的遍历方式。树的结点之间不存在唯一的前驱和后继关系,在访问一个结点后,下一个被访问的结点面临着不同的选择。就像你人生的道路上,高考填志愿要面临哪个城市、哪所大学、具体专业等选择,由于选择方式的不同,遍历的次序就完全不同了。

2.二叉树的遍历方法

二叉树的遍历方式可以很多,如果我们限制了从左到右的习惯方式,那么主要就分为四种:

(1)前序遍历

规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。如图6-8-2所示,遍历的顺序为:ABDGHCEIF。

(2)中序遍历

规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。如下图所示,遍历的顺序为:GDHBAEICF。 

(3)后序遍历

规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。如下图所示,遍历的顺序为:GHDBIEFCA。 

 (4)层序遍历

规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。如下图所示,遍历的顺序为:ABCDEFGHI。

有读者会说,研究这么多遍历的方法干什么呢?

我们用图形的方式来表现树的结构,应该说是非常直观和容易理解,但是对于计算机来说,它只有循环、判断等方式来处理,也就是说,它只会处理线性序列,而我们刚才提到的四种遍历方法,其实都是在把树中的结点变成某种意义的线性序列,这就给程序的实现带来了好处。

另外不同的遍历提供了对结点依次处理的不同方式,可以在遍历过程中对结点进行各种处理。

3.前序遍历算法

二叉树的定义是用递归的方式,所以,实现遍历算法也可以采用递归,而且极其简洁明了。先来看看二叉树的前序遍历算法。代码如下:

//二叉树前序遍历算法
void PreOrderTraverse(BiTree T)
{
    if(T == NULL)
        return;
    printf("%c", T->data);    //显示结点数据, 可以更改为其他对结点的操作
    PreOrderTraverse(T->lchild);    //再遍历左子树
    PreOrderTraverse(T->rchild);    //再遍历右子树
}

假设我们现在有如下图这样一棵二叉树T。这树已经用二叉链表结构存储在内存当中。

 那么当调用 PreOrderTraverse(T)函数时,我们来看看程序是如何运行的。

1.调用 PreOrderTraverse(T),T根结点不为null,所以执行 printf,打印字母A,如下图所示。

2.调用 PreOrderTraverse(T->lchild);访问了A结点的左孩子,不为 null,执行printf 显示字母 B,如下图所示。 

3.此时再次递归调用PreOrderTraverse(T-lkchild);访问了B结点的左孩子,执行 printf 显示字母 D,如下图所示。 

 4.再次递归调用PreOrderTraverse(T->kchild);访问了 D结点的左孩子,执行printf显示字母 H,如下图所示。

5.再次递归调用PreOrderTraverse(T->lchild);访问了H结点的左孩子,此时因为H结点无左孩子,所以T==null,返回此函数,此时递归调用 PreOrderTraverse(T->rchild);访问了H结点的右孩子,printf 显示字母K, 如下图所示

6.再次递归调用PreOrderTraverse(T->lchild);访问了K结点的左孩子,K结点无左孩子,返回,调用Pre0rderTraverse(T->rchild);访问了K结点的右孩子,也是 nul,返回。于是此函数执行完毕,返回到上一级递归的函数(即打印 H结点时的函数),也执行完毕,返回到打印结点D时的函数,调用PreOrderTraverse(T->rchikd);访问了D结点的右孩子,不存在,返回到B结点,调用PreOrderTraverse(T->rchild);找到了结点E,打印字母E,如下图所示

7.由于结点 E没有左右孩子,返回打印结点 B时的递归函数,递归执行完毕,返回到最初的PreOrderTraverse,调用PreOrderTraverse(T->rchild);访问结点A的右孩子,打印字母C,如下图所示。 

8.之后类似前面的递归调用,依次继续打印F、1、G、J,步骤略。
综上,前序遍历这棵二叉树的节点顺序是:ABDHKECFIGJ。 

4.中序遍历算法

那么二叉树的中序遍历算法是如何呢? 别以为很复杂,它和前序遍历算法仅仅只是代码的顺序上的差异。

//二叉树中序遍历算法
void InOrderTraverse(BiTree T)
{
    if(T == NULL)
        return;
    InOrderTraverse(T->lchild);    //先遍历左子树
    printf("%c", T->data);    //显示结点数据, 可以更改为其他对结点的操作
    InOrderTraverse(T->rchild);    //再遍历右子树
}

换句话说,它等于是把调用左孩子的递归函数提前了,就这么简单。读者们可以自行来看看当调用InOrderTraverse(T)函数时,程序是如何运行的。

5.后序遍历算法

那么同样的,后序遍历也就很容易想到应该如何写代码了。

//二叉树后序遍历算法
void PostOrderTraverse(BiTree T)
{
    if(T == NULL)
        return;
    PostOrderTraverse(T->lchild);    //先遍历左子树
    PostOrderTraverse(T->rchild);    //再遍历右子树
    printf("%c", T->data);    //显示结点数据, 可以更改为其他对结点的操作
}

如下图所示,后序遍历是先递归左子树,由根结点A→B→D→H,结点H无左孩子,再查看结点H的右孩子K,因为结点K无左右孩子,所以打印K,返回。

最终,后序遍历的结点的顺序就是:KHDEBIFIGCA。读者们可以自己按照刚才的办法得出这个结果。 

6.推导遍历结果

有一种题目为了考查你对二叉树遍历的掌握程度,是这样出题的。已知一棵二叉树的前序遍历序列为 ABCDEF,中序遍历序列为CBAEDF,请问这棵二叉树的后序遍历结果是多少?

对于这样的题目,如果真的完全理解了前中后序的原理,是不难的。

三种遍历都是从根结点开始,前序遍历是先打印再递归左和右。所以前序遍历序列为 ABCDEF,第一个字母是A 被打印出来,就说明A是根结点的数据。再由中序遍历序列是 CBAEDF,可以知道C和B是A的左子树的结点,E、D、F是A的右子树的结点,如下图所示。

然后我们看前序中的C和B,它的顺序是ABCDEF,是先打印B后打印C,所以B应该是A的左孩子,而C就只能是B的孩子,此时是左还是右孩子还不确定。再看中序序列是 CBAEDF,C是在B的前面打印,这就说明C是B的左孩子,否则就是右孩子了,如下图所示。

再看前序中的E、D、F,它的顺序是ABCDEF,那就意味着D是A结点的孩子,E和F是D的子孙,注意,它们中有一个不一定是孩子,还有可能是孙子的。再来看中序序列是 CBAEDF,由于E在D的左侧,而F在右侧,所以可以确定E是D的左孩子,F是D的右孩子。因此最终得到的二叉树是下图所示。 

为了避免推导中的失误,你最好在心中递归遍历,检查一下这棵树的前序和中序遍历序列是否与题目中的相同。

已经复原了二叉树,要获得它的后序遍历结果就是易如反掌,结果是 CBEFDA。

但其实,如果读者们足够熟练,不用画这棵二叉树,也可以得到后序的结果,因为刚才判断了 A 结点是根结点,那么它在后序序列中,一定是最后一个。刚才推导出C是B的左孩子,而B是A的左孩子,那就意味着后序序列的前两位一定是CB。同样的办法也可以得到 EFD 这样的后序顺序,最终就自然的得到CBEFDA 这样的序列,不用在草稿上画树状图了。

反过来,如果我们的题目是这样:二叉树的中序序列是ABCDEFG,后序序列是BDCAFGE,求前序序列。

这次简单点,由后序的BDCAFGE,得到E是根结点,因此前序首字母是E。

于是根据中序序列分为两棵树ABCD和FG,由后序序列的BDCAFGE,知道A是E的左孩子,前序序列目前分析为 EA。

再由中序序列的ABCDEFG,知道BCD是A结点的右子孙,再由后序序列的BDCAFGE 知道C结点是A结点的右孩子,前序序列目前分析得到EAC。

中序序列ABCDEFG,得到B是C的左孩子,D是的右孩子,所以前序序列目前分析结果为 EACBD

由后序序列BDCAFGE,得到G是E的右孩子,于是F就是G的孩子。如果你是在考试时做这道题目,时间就是分数、名次、学历,那么你根本不需关心F是G的左还是右孩子,前序遍历序列的最终结果就是EACBDGF

不过细细分析,根据中序序列ABCDEFG,是可以得出F是G的左孩子。

从这里我们也得到两个二叉树遍历的性质

●已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。

●已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。

但要注意了,已知前序和后序遍历,是不能确定一棵二叉树的,原因也很简单比如前序序列是 ABC,后序序列是 CBA。我们可以确定A一定是根结点,但接下来我们无法知道,哪个结点是左子树,哪个是右子树。这棵树可能有如下所示的四种可能。

五.部分功能的C语言代码实现 

本节我们讲到了二叉树的遍历, 以后将继续向大家分享二叉树的进阶部分, 以下是C语言实现二叉树部分功能代码

BineryTree.h

#ifndef BINERYTREEHOMEWORK_BINERYTREE_H
#define BINERYTREEHOMEWORK_BINERYTREE_H
#include <stdio.h>
#include <stdlib.h>

typedef char BTDataType;

typedef struct BinaryTreeNode
{
    BTDataType _data;
    struct BinaryTreeNode* _left;
    struct BinaryTreeNode* _right;
}BTNode;

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode* root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root);

#endif //BINERYTREEHOMEWORK_BINERYTREE_H

BineryTree.c

#include "Queue.h"
#include "BineryTree.h"

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
    if(a[*pi] == '#')
    {
        ++(*pi);
        return NULL;
    }
    BTNode* root = (BTNode*)malloc(sizeof(BTNode));
    root->_data = a[(*pi)++];
    if(*pi == n)
    {
        root->_left = root->_right = NULL;
    }
    root->_left = BinaryTreeCreate(a, n, pi);
    root->_right = BinaryTreeCreate(a, n, pi);
}

// 二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
    //前序销毁不太好
//    if(*root == NULL)
//        return ;
//    BTNode* leftTree = (*root)->_left;
//    BTNode* rightTree = (*root)->_right;
//    free(*root);
//    *root = NULL;
//    BinaryTreeDestory(&leftTree);
//    BinaryTreeDestory(&rightTree);

    if(root == NULL)
        return ;
    //采用后续销毁
    BinaryTreeDestory(root->_left);
    BinaryTreeDestory(root->_right);
    free(root);
}

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
    if(root == NULL)
        return NULL;
    if(root->_data == x)
        return root;
    BTNode* leftnode = BinaryTreeFind(root->_left, x);
    return leftnode == NULL ? BinaryTreeFind(root->_right, x): leftnode;
}

int BinaryTreeLevelKSize(BTNode* root, int k)
{
    if(root == NULL)
        return 0;
    if(k == 1)
        return 1;
    return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
}

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
    return root == NULL ? 0 :
        BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;
}

int BinaryTreeLeafSize(BTNode* root)
{
    if(root == NULL)
        return 0;
    if(root->_left == NULL && root->_right == NULL)
        return 1;
    return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}

// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
    if(root == NULL)
        return ;
    printf("%d ", root->_data);
    BinaryTreePrevOrder(root->_left);
    BinaryTreePrevOrder(root->_right);
}

// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
    if(root == NULL)
        return ;
    BinaryTreeInOrder(root->_left);
    printf("%d ", root->_data);
    BinaryTreeInOrder(root->_right);
}

// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
    if (root == NULL)
        return;
    BinaryTreePostOrder(root->_left);
    BinaryTreePostOrder(root->_right);
    printf("%d ", root->_data);
}

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
    Queue q;
    QueueInit(&q);
    if(root)
        QueuePush(&q, root);
    while(!QueueEmpty(&q))
    {
        BTNode* front = QueueFront(&q);
        QueuePop(&q);
        printf("%d ", front->_data);

        if(root->_left)
            QueuePush(&q, root->_left);

        if(root->_right)
            QueuePush(&q, root->_right);
    }
    QueueDestroy(&q);
}

// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
    Queue q;
    QueueInit(&q);
    if(root)
        QueuePush(&q, root);
    while(!QueueEmpty(&q))
    {
        BTNode* front = QueueFront(&q);
        QueuePop(&q);
        if(front == NULL)
            break;

        QueuePush(&q, root->_left);
        QueuePush(&q, root->_right);
    }
    while(!QueueEmpty(&q))
    {
        BTNode* front = QueueFront(&q);
        QueuePop(&q);
        if(front)
        {
            QueueDestroy(&q);
            return false;
        }
    }
    QueueDestroy(&q);
    return true;
}

实现层序遍历需要用到的队列代码:

Queue.h

#ifndef PROJECT10_QUEUE_H
#define PROJECT10_QUEUE_H

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
#include<stdbool.h>

typedef struct BinaryTreeNode* QDataType;       //存指针

typedef struct QueueNode
{
    struct QueueNode* next;
    QDataType val;
}QNode;

typedef struct Queue
{
    QNode* phead;
    QNode* ptail;
    int size;
}Queue;

void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);

// 队尾插入
void QueuePush(Queue* pq, QDataType x);
// 队头删除
void QueuePop(Queue* pq);

// 取队头和队尾的数据
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);

int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);

 队尾插入
//void QueuePush(QNode** pphead, QNode** pptail, QDataType x);
 队头删除
//void QueuePop(QNode** pphead, QNode** pptail);

#endif //PROJECT10_QUEUE_H

Queue.c

#include"Queue.h"

void QueueInit(Queue* pq)
{
    assert(pq);
    pq->phead = NULL;
    pq->ptail = NULL;
    pq->size = 0;
}

void QueueDestroy(Queue* pq)
{
    assert(pq);

    QNode* cur = pq->phead;
    while (cur)
    {
        QNode* next = cur->next;
        free(cur);

        cur = next;
    }

    pq->phead = pq->ptail = NULL;
    pq->size = 0;
}

// 队尾插入
void QueuePush(Queue* pq, QDataType x)
{
    assert(pq);

    QNode* newnode = (QNode*)malloc(sizeof(QNode));
    if (newnode == NULL)
    {
        perror("malloc fail");
        return;
    }

    newnode->next = NULL;
    newnode->val = x;

    if (pq->ptail == NULL)
    {
        pq->phead = pq->ptail = newnode;
    }
    else
    {
        pq->ptail->next = newnode;
        pq->ptail = newnode;
    }

    pq->size++;
}

// 队头删除
void QueuePop(Queue* pq)
{
    assert(pq);
    assert(pq->size != 0);

    /*QNode* next = pq->phead->next;
    free(pq->phead);
    pq->phead = next;

    if (pq->phead == NULL)
        pq->ptail = NULL;*/

    // 一个节点
    if (pq->phead->next == NULL)
    {
        free(pq->phead);
        pq->phead = pq->ptail = NULL;
    }
    else // 多个节点
    {
        QNode* next = pq->phead->next;
        free(pq->phead);
        pq->phead = next;
    }

    pq->size--;
}

QDataType QueueFront(Queue* pq)
{
    assert(pq);
    assert(pq->phead);

    return pq->phead->val;
}

QDataType QueueBack(Queue* pq)
{
    assert(pq);
    assert(pq->ptail);

    return pq->ptail->val;
}


int QueueSize(Queue* pq)
{
    assert(pq);

    return pq->size;
}

bool QueueEmpty(Queue* pq)
{
    assert(pq);

    return pq->size == 0;
}

  • 50
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
二叉搜索树是一种特殊的二叉树,它的左子树上所有节点的值都小于根节点的值,右子树上所有节点的值都大于根节点的值,这样的树就称为二叉搜索树。 在 Python 中实现二叉搜索树,你可以先定义一个节点类,来表示树的节点。每个节点需要包含一个值、左子树和右子树。 ```python class Node: def __init__(self, value): self.value = value self.left = None self.right = None ``` 接下来,你需要实现一个二叉搜索树类,来操作树。其中包含插入节点、查找节点和删除节点等方法。 ```python class BinarySearchTree: def __init__(self): self.root = None def insert(self, value): if self.root is None: self.root = Node(value) else: self._insert(value, self.root) def _insert(self, value, node): if value < node.value: if node.left is None: node.left = Node(value) else: self._insert(value, node.left) elif value > node.value: if node.right is None: node.right = Node(value) else: self._insert(value, node.right) def search(self, value): if self.root is None: return False else: return self._search(value, self.root) def _search(self, value, node): if node is None: return False elif node.value == value: return True elif value < node.value: return self._search(value, node.left) else: return self._search(value, node.right) def delete(self, value): if self.root is None: return False else: self.root = self._delete(value, self.root) def _delete(self, value, node): if node is None: return node elif value < node.value: node.left = self._delete(value, node.left) return node elif value > node.value: node.right = self._delete(value, node.right) return node else: if node.left is None and node.right is None: node = None return node elif node.left is None: node = node.right return node elif node.right is None: node = node.left return node else: temp = self._find_min_node(node.right) node.value = temp.value node.right = self._delete(temp.value, node.right) return node def _find_min_node(self, node): while node.left is not None: node = node.left return node ``` 这里的 `_insert`、`_search` 和 `_delete` 方法都是私有方法,是为了防止用户直接调用修改根节点。用户只需调用 `insert`、`search` 和 `delete` 方法即可操作树。 以上就是一个简单的 Python 实现二叉搜索树的代码。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RedefineLim.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值