第7章 二叉树
第7章 二叉树
一、二叉树的基本概念
- 二叉树:一个根结点及两颗互不相交的分别称作这个根结点的左子树和右子树的二叉树组成
- 二叉树与普通树的区别:二叉树的子树一定有序;普通树子树可以有序也可以无序
- 满二叉树:所有终端结点都位于同一层次,且其他非终端结点的度都为 \(2\)
- 完全二叉树:一颗二叉树扣除其最大层次后为一颗满二叉树,且层次最大那层的所有结点都向左靠齐
- 注:满二叉树一定是完全二叉树;完全二叉树不一定是满二叉树
- 二叉树的性质:
- 一颗非空二叉树的第 \(i\) 层上至多有 \(2^{i-1}\) 个结点 (\(i>=1\))
- 深度为 \(h\) 的二叉树至多有 \(2^h-1\) 个结点(\(h>=1\))
- 对于任何一颗二叉树 \(T\),如果其终端结点(叶子结点)数为 \(n_0\),度为 \(1\) 的结点树为 \(n_1\),度为 \(2\) 的结点树为 \(n_2\),则 \(n_0=n_2+1\)
- 注:\(n_0+n_1+n_2 = n_1+2*n_2+1\)
- 对于具有 \(n\) 个结点的完全二叉树,如果按照从
上到下、同一层次上的结点按从左到右的顺序对二叉树中的所有结点从$ 1$ 开始顺序编号,则对于序号为 \(i\) 的
结点,有:- 如果 \(i>1\),则序号为 \(i\) 的双亲结点的序号为 \([i/2](取整函数)\)
- 如果 \(2*i>n\),则结点 \(i\) 无左子女(此时结点 \(i\) 为终端结点);否则其左子女为结点 \(2*i\)
- 如果 \(2*i+1>n\),则结点 \(i\) 无右子女;否则其右子女为节点 \(2*i+1\)
二、二叉树的基本运算
- 略
三、二叉树的存储结构
3.1 顺序存储结构
- 略
3.2 链式存储结构
3.2.1 二叉树链式存储结构
typedef char datatype; // 结点属性值的类型
// 二叉树结点的类型
typedef struct node {
datatype data;
struct node *lchild, *rchild;
// struct node *parent; // 指向双亲的指针 (可有可无)
} bintnode;
typedef bintnode *bintree;
bintree root; // 指向二叉树根结点的指针
四、二叉树的遍历
4.1 二叉树遍历的定义
-
前序遍历:首先访问根结点;
然后按照前序遍历的顺序访问根结点的左子树;
再按照前序遍历的顺序访问根结点的右子树 -
中序遍历:首先按照中序遍历的顺序访问根结点的左子树;
然后访问根结点;最后按照中序遍历的顺序访问根结点的右子树 -
后序遍历:首先按照后序遍历的顺序访问根结点的左子树;
然后按照后序遍历的顺序访问根结点的右子树;最后访问根结点 -
图二叉树的遍历:
4.2 二叉树遍历的递归实现
-
二叉树遍历的递归实现 :按照遍历规定的次序,访问根结点时就输出根结点的值
-
常用操作:
- 中序遍历的二叉树的递归算法
- 后序遍历的二叉树的递归算法
4.2.1 前序遍历的二叉树的递归算法(算法)
typedef char datatype; // 结点属性值的类型
// 二叉树结点的类型
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
void preorder(bintree t) {
if (t) {
printf("%c", t->data);
preorder(t->lchild);
preorder(t->rchild);
}
}
4.2.2 前序遍历时二叉树的创建算法(算法)
typedef char datatype; // 结点属性值的类型
// 二叉树结点的类型
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
bintree createbintree() {
char ch;
bintree t;
if ((ch = getchar()) == '#') t = NULL;
else {
t = (bintnode *) malloc(sizeof(bintnode)); // 生成二叉树的根结点
t->data = ch;
t->lchild = createbintree(); // 递归实现左子树的建立
t->rchild = createbintree(); // 递归实现右子树的建立
}
return t;
}
- 图创建二叉树:
4.3 二叉树遍历的非递归实现
- 常用操作:
- 中序遍历的二叉树的非递归算法
- 后序遍历的二叉树的非递归算法
4.3.1 前序遍历的二叉树的非递归算法(算法)
- 算法步骤:
- 对于一颗树(子树)\(t\)
- 访问完 \(t\) 的根结点值后,进入 \(t\) 的左子树,但是此时需要将 \(t\) 保存起来
- 在 \(t\) 处设置一个回溯点
- 访问完左子树后,通过回溯点 \(t\) 进入 \(t\) 的右子树访问
- 注:栈顶元素即将出栈时,意味着根结点和左子树访问完成,出栈后需要进入其右子树访问
typedef char datatype; // 结点属性值的类型
// 二叉树结点的类型
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
void preorder1(bintree t) {
seqstack s;
s.top = 0;
// 当前处理的子树不为空或栈不为空则循环
while ((t) || (s.top != 0)) {
if (t) {
printf("%c ", t->data);
push(&s, t);
t = t->lchild;
} else {
t = pop(&s);
t = t->rchlid;
}
}
}
五、二叉树其他运算的实现
5.1 二叉树的查找(算法)
- 算法步骤:
- 首先判断树是否为空
- 如果树(子树)结点值为 \(x\),则返回
- 否则前往左子树查找,找到返回值
- 否则前往右子树查找,找到返回值
- 否则前往左子树查找,找到返回值
typedef char datatype; // 结点属性值的类型
// 二叉树结点的类型
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
bintree locate(bintree t, dataype x) {
bintree p;
if (t == NULL) return NULL;
else {
if (t->data == x) return t;
else {
p = locate(t->lrchild);
if (p) return p;
else return locate(t->rchild)
}
}
}
5.2 统计二叉树中的结点的个数(算法)
- 算法步骤:
- 判断树(子树)是否为空
- 不为空递归查找左子树和右子树,并且返回左子树结点总数+右子树结点总数+根结点
typedef char datatype; // 结点属性值的类型
// 二叉树结点的类型
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
int numofnode(bintree t) {
if (t == NULL) return 0;
else return (numofnode(t->lchild) + numofnode(t->rchild) + 1);
}
5.3 判断二叉树是否等价(算法)
- 算法步骤:
- 判断两个二叉树是否都为空,都为空则等价
- 如果两个二叉树不都为空
- 首先判断根结点是否相同
- 其次递归判断左子树是否相同
- 最后递归判断右子树是否相同,是否等价的标准取决于右子树是否也相同
typedef char datatype; // 结点属性值的类型
// 二叉树结点的类型
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
int isequal(bintree t1, bintree t2) {
int t;
t = 0;
if (t1 == NULL && t2 == NULL) t = 1; // t1 和 t2 均为空,则二者等价
else {
// 处理 t1 和 t2 均不为空的情况
if (t1 != NUll && t2 != NULL)
if (t1->data == t2->data) // 如果根结点的值相等
if (isequeal(t1->lchild, t2->lchild)) // 如果 t1 和 t2 的左子树等价
t = isequeal(t1->rchild, t2->rchild); // 返回值取决于 t1 和 t2 的右子树是否等价
}
return (t);
}
5.4 求二叉树的高度(算法)
- 算法步骤:
- 首先处理空二叉树的情况
- 其次递归得出左子树的高度
- 最后递归得出右子树的高度
- 递归期间,如果左子树高度大于右子树高度,左子树高度加 \(1\),否则右子树高度加 \(1\)
- 注:递归出口应该用第三个变量 \(h\) 来返回子树高度
typedef char datatype; // 结点属性值的类型
// 二叉树结点的类型
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} bintnode;
typedef bintnode *bintree;
int depth(bintree t) {
int h, lh, rh;
if (t == NULL) h = 0; // 处理空二叉树的情况
else {
lh = depth(t->lchild); // 求左子树的高度
rh = depth(t->rchild); // 求右子树的高度
if (lh >= rh) h = lh + 1; // 求二叉树t的高度
else h = rh + 1;
}
return h;
}
六、穿线二叉树
6.1 穿线二叉树的定义
-
穿线二叉树的指针:结点的左、右指针指向其左、右子女
-
中序穿线二叉树的线索:结点的左、右指针指向其中序遍历的前驱、后继结点
-
为了区别结点的左右指针是指针还是线索,一般加上 \(ltag\) 和 \(rtag\) 两个标志位
- \(ltag=0\) 表示结点的左指针指向其左子女
- \(ltag=1\) 表示结点的左指针指向其中序遍历的前驱
- \(rtag=0\) 表示结点的右指针指向其右子女
- \(rtag=1\) 表示结点的右指针指向其中序遍历的后继
-
图中序穿线二叉树:
6.2 中序穿线二叉树的基本运算
- 略
6.3 中序穿线二叉树的存储结构及其实现
- 常用操作:
- 创建一颗中序二叉树
6.3.1 中序穿线二叉树的存储结构
typedef char datatype;
typedef struct node {
datatype data;
int ltag, rtag; // 左右标志位
struct node *lchild, *rchild;
} binthrnode;
typedef binthrnode *binthrtree;
6.3.2 中序遍历中序穿线二叉树(真题)(算法)
- 算法步骤:
- 找到中序遍历下的第一个结点(从根结点出发,沿着左指针不断往左走,直至左指针为空)
- 从中序遍历的第一个结点开始,不断地寻找当前结点在中序遍历下的后继结点并输出
- 在中序穿线二叉树中找后继结点步骤:
- 若右标志为 \(1\),则表明右指针正好指向其中序遍历下的后继结点
- 若右标志为 \(0\),则说明他有右子树,因此其中序遍历下的后继结点应该是该右子树中序遍历下的第一个结点(右子树的最左下的结点,与第一步步骤完全相同)
- 在中序穿线二叉树中找后继结点步骤:
typedef char datatype;
typedef struct node {
datatype data;
int ltag, rtag; // 左右标志位
struct node *lchild, *rchild;
} binthrnode;
typedef binthrnode *binthrtree;
// 寻找结点 p 在中序遍历下的后继结点
binthrtree insuccnode(binthrtree p) {
binthrtree q;
if (p->rtag == 1) // p 的右指针为线索,恰巧指向p的后继结点
return p->rchild;
else {
q = p->rchild; // 寻找 p 的右子树中最左下的结点
while (q->ltag == 0) q = q->lchild; // 求该右子树下中序遍历下的第一个结点
return q;
}
}
// 中序遍历中序穿线二叉树
void inthrtree(binthrtree p) {
if (p) {
while (p->ltag == 0) p = p->lchild; // 求 p 中序遍历下的第一个结点
do {
printf("%c ", p->data);
p = insuccnode(p); // 求 p 中序遍历下的后继结点
} while (p);
}
}
七、树、森林和二叉树的转换
- 注:任意一颗树(森林)都唯一地对应一颗二叉树;相反,任何一颗二叉树都唯一地对应一颗树(森林)
7.1 树、森林到二叉树的转换
-
树、森林到二叉树的转换步骤
- 在所有兄弟结点之间添加一条连线,如果是森林,则在其所有树的树根之间同样也添加一条连线
- 对于树、森林中的每个结点,除保留其到第一个子女的连线外,撤消其到其它子女的连线;
- 将以上得到的树按照顺时针方向旋转45度
-
图树到二叉树的转换:
-
图森林到二叉树的转换:
7.2 二叉树到树、森林的转换
-
首先将二叉树按照逆时针方向旋转45度
-
若某结点是其双亲的左子女,则把该结点的右子女,右子女的右子女,……都与该结点的双亲用线连起来
-
最后去掉原二叉树中所有双亲到其右子女的连线
-
注:最后连接子结点,只能连接右子女,而不能连接左子女
-
图二叉树到森林的转换:
八、通过前中后序遍历确定二叉树(补充)
8.1 前序+中序遍历
- 已知一棵二叉树的前序和中序序列,构造该二叉树的过程如下:
-
根据前序序列的第一个元素建立根结点;
-
在中序序列中找到该元素,确定根结点的左右子树的中序序列;
-
在前序序列中确定左右子树的前序序列;
-
由左子树的前序序列和中序序列建立左子树;
-
由右子树的前序序列和中序序列建立右子树。
-
如:已知一棵二叉树的先序遍历序列和中序遍历序列分别是 abdgcefh、dgbaechf,求二叉树及后序遍历序列。
先序:abdgcefh—>a bdg cefh
中序:dgbaechf—->dgb a echf
得出结论:a 是树根,a 有左子树和右子树,左子树有 bdg 结点,右子树有 cefh 结点
先序:bdg—>b dg
中序:dgb —>dg b
得出结论:b 是左子树的根结点,b 无右子树,有左子树
先序:dg—->d g
中序:dg—–>dg
得出结论:d 是 b 左子树的根节点,d 无左子树,g 是 d 的右子树
然后对于 a 的右子树类似可以推出
然后还原
8.2 中序+后序遍历
- 已知一棵二叉树的中序和后序序列,构造该二叉树的过程如下:
- 根据后序序列的最后一个元素建立根结点
- 在中序序列中找到该元素,确定根结点的左右子树的中序序列
- 在后序序列中确定左右子树的后序序列
- 由左子树的后序序列和中序序列建立左子树
- 由右子树的后序序列和中序序列建立右子树
如:已知一棵二叉树的后序遍历序列和中序遍历序列分别是gdbehfca、dgbaechf,求二叉树
后序:gdbehfca—->gdb ehfc a
中序:dgbaechf—–>dgb a echf
得出结论:a是树根,a有左子树和右子树,左子树有 bdg 结点,右子树有 cefh 结点
后序:gdb—->gd b
中序:dgb—–>dg b
得出结论:b 是 a 左子树的根节点,无右子树,有左子树 dg
后序:gd—->g d
中序:dg—–>d g
得出结论:d 是 b 的左子树根节点,g 是 d 的右子树
然后对于 a 的右子树类似可以推出
然后还原
8.3 前序+后序遍历
- 注:前序和后序在本质上都是将父节点与子结点进行分离,但并没有指明左子树和右子树的能力,因此得到这两个序列只能明确父子关系,而不能唯一地确定一棵二叉树。
九、算法设计题
9.1 求一颗给定二叉树中叶子结点的个数(递归和非递归)(真题)(算法)
分别采用递归和非递归方式编写两个函数,求一棵给定二叉树中叶子结点的个数
- 算法步骤(递归):
- 判断二叉树是否为空,为空返回 \(0\)
- 判断二叉树的子树(子树的子树)的两个左右孩子是否为空,为空返回 \(1\)
- 左右递归搜索两棵子树
- 算法步骤(非递归):
- 二叉树不为空或栈不为空则开启循环
- 如果二叉树存在,并且左右子树为空,计数加 \(1\)
- 否则将该子树放入栈中,并且搜索该子树的左子树
- 如果二叉树不存在,证明左子树搜索完毕,从栈中取出子树搜索其右子树
- 如果二叉树存在,并且左右子树为空,计数加 \(1\)
- 二叉树不为空或栈不为空则开启循环
typedef char datatype;
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} binthrnode;
typedef binthrnode *bintree;
// 递归方法求二叉树叶子结点的个数
int leaf1(bintree t) {
if (t == NULL) return 0;
else if (!t->lchild && !t->rchild)
return 1;
else
return leaf1(t->lchild) + leaf1(t->rchild);
}
// 非递归方法求二叉树叶子结点的个数
int leaf2(bintree t) {
seqstack s; // 顺序栈
int count = 0; // 叶子结点计数变量
init(&s); // 初始化空栈
while (t || !empty(&s)) {
if (t) {
if (!t->lchild && !t->rchild) count++;
push(&s, t);
t = t->lchild;
} else {
t = pop(&s);
t = t->rchild;
}
}
return count;
}
9.2 返回一颗给定二叉树在中序遍历下的最后一个结点(真题)(算法)
试编写一个函数,返回一颗给定二叉树在中序遍历下的最后一个结点
- 算法 步骤:
- 不断地查找右子树的右子结点
- 返回最后一个右子结点
typedef char datatype;
typedef struct node {
datatype data;
struct node *lchild, *rchild;
} binthrnode;
typedef binthrnode *bintree;
// 递归实现
bintree midlast(bintree t) {
if (t && t->rchild) t = midlast(t->rchild);
return t;
}
// 非递归实现
bintree midlast(bintree t) {
bintree p = t;
while (p && p->rchild) p = p->rchild;
return p;
}
十、错题集
-
前序(根左右)和中序(左根右)遍历结果相同的二叉树(去掉左都为“根右”)为(所有结点只有右子树的二叉树);前序(根左右)和后序(左右根)遍历结果相同的二叉树(哪一个子树都不可以去掉)为(只有根结点的二叉树)
-
有 \(n\) 个结点的二叉树,已知叶结点个数为 \(n_0\),则该树中度为 \(1\) 的结点的个数为(\(n-2*n_0+1\));若此树是深度为 \(k\) 的完全二叉树,则 \(n\) 的最小值为 (\(2^{k-1}\))
- 注:完全二叉树的最后一层,一定有一个结点,因此 \(n\) 的最小值为 \(k-1\) 层总结点数加 \(1\) 为 \(2^{k-1}-1+1\)
-
(真题)对于一颗具有 \(n\) 个结点的二叉树,该二叉树中所有结点的读书之和为(\(n-1\))
-
(真题)试分别画出具有 \(3\) 个结点的树和具有 \(3\) 个结点的二叉树的所有不同形态。(此题注意树具有无序性)
- 图例题7-9答案