前置知识:树的基本概念与性质
二叉树的定义
二叉树是一种特殊的树形结构,其特点是每个结点至多只有两棵子树(即二叉树中不存在度大于
2
2
2的结点),并且二叉树是有序树,左右子树不能互换。
与树类似,二叉树也以递归形式定义。二叉树是
n
n
n(
n
≥
0
n≥0
n≥0)个结点的有限集合。当
n
=
0
n=0
n=0时,称为空二叉树。
当
n
≠
0
n≠0
n=0时,二叉树由根结点和两个互不相交的子树组成,左子树和右子树又分别是一棵二叉树。即使树中结点只有一棵子树,因为二叉树有序,也要区分是左子树还是右子树。
二叉树与度为
2
2
2的有序树的区别:
- 度为 2 2 2的有序树至少有 3 3 3个结点,而二叉树可以为空。
- 度为 2 2 2的有序树的孩子的左右次序是相对于另一个孩子而言的,若某一结点只有一个孩子结点,则无需区分其左右。而对二叉树上的结点来说,无论其孩子数是否为 2 2 2,均需确定其左右次序。即二叉树的结点次序不是相对于另一个结点而言的,而是确定的。
几种特殊的二叉树
Man! 满二叉树
一棵高度为
h
h
h,且有
2
h
−
1
2^h-1
2h−1个结点的二叉树被称为满二叉树。
因为对一个满二叉树来说,第
i
i
i层有
2
i
−
1
2^{i-1}
2i−1个结点,一共有
h
h
h层,则求和结果就是
2
h
−
1
2^h-1
2h−1。
满二叉树的特点:
- 只有最后一层有叶子结点。
- 不存在度为 1 1 1的结点。
- 若层序从 1 1 1开始编号,则对第 i i i个结点,其左孩子必为第 2 i 2i 2i个结点,右孩子必为第 2 i + 1 2i+1 2i+1个结点。如果结点 i i i存在双亲结点,则其双亲必为第 ⌊ i / 2 ⌋ \left\lfloor i/2 \right\rfloor ⌊i/2⌋个结点(即对 i / 2 i/2 i/2向下取整)。
(下图来自王道考研408数据结构课程视频 - 二叉树的定义和基本术语)
完全二叉树
一棵高度为
h
h
h、有
n
n
n个结点的二叉树,当且仅当其每个结点都与高度为
h
h
h的满二叉树中编号为
1
−
n
1-n
1−n的结点一一对应时,称为完全二叉树。
完全二叉树的特点:
- 只有最后两层(层数最大的两层)可能有叶子结点。
- 最多只有一个度为 1 1 1的结点,且该结点只有左孩子而无右孩子。
- 层序从 1 1 1开始编号,则对第 i i i个结点,其左孩子必为第 2 i 2i 2i个结点,右孩子必为第 2 i + 1 2i+1 2i+1个结点。如果结点 i i i存在双亲结点,则其双亲必为第 ⌊ i / 2 ⌋ \left\lfloor i/2 \right\rfloor ⌊i/2⌋个结点。(同满二叉树)
- 对结点 i i i,当 i ≤ ⌊ n / 2 ⌋ i≤ \left\lfloor n/2 \right\rfloor i≤⌊n/2⌋时,该结点为分支结点;当 i > ⌊ n / 2 ⌋ i> \left\lfloor n/2 \right\rfloor i>⌊n/2⌋时,该结点为叶结点。
- 若 n n n为奇数,则每个分支结点都有左孩子和右孩子;若 n n n为偶数,则编号最大的分支结点(编号为 n / 2 n/2 n/2)只有左孩子,没有右孩子,其余分支结点均有左、右孩子。
二叉排序树⭐
左子树上所有结点的关键字均小于根结点的关键字,右子树上所有结点的关键字均大于根结点的关键字,左右子树又各是一棵二叉排序树,这样的二叉树被称为二叉排序树。
平衡二叉树⭐
树中任意一个结点的左子树和右子树的高度之差的绝对值不超过
1
1
1,这样的二叉树称为平衡二叉树。
如果一棵二叉排序树是平衡的,那么它的搜索效率会高很多。
关于二叉排序树和平衡二叉树,后续章节会有详细介绍,现在暂时了解其概念即可。
正则二叉树
树中每个分支结点都有 2 2 2个孩子,即树中只有度为 0 0 0或 2 2 2的结点。
二叉树的性质相关
常见考点1:非空二叉树上的叶结点数等于度为 2 2 2的结点数加 1 1 1,即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1。
假设树中结点总数为
n
n
n,度为
0
0
0的结点数为
n
0
n_0
n0,度为
1
1
1的结点数为
n
1
n_1
n1,度为
2
2
2的结点数为
n
2
n_2
n2。则有:
①
n
=
n
0
+
n
1
+
n
2
n=n_0+n_1+n_2
n=n0+n1+n2
②
n
=
n
1
+
2
n
2
+
1
n=n_1+2n_2+1
n=n1+2n2+1(树的结点数=总度数+1)
联立两式,得
n
0
=
n
2
+
1
n_0=n_2+1
n0=n2+1。
这一个考点常在选择题中涉及,需要牢记并灵活应用。
常见考点2:非空二叉树的第 i i i层至多有 2 i − 1 2^{i-1} 2i−1个结点( i ≥ 1 i≥1 i≥1)
由树的基本概念与性质可知, m m m叉树的第 i i i层至多有 m i − 1 m^{i-1} mi−1个结点( i ≥ 1 i≥1 i≥1),令 m = 2 m=2 m=2即可。
常见考点3:高度为 h h h的二叉树至多有 2 h − 1 2^h-1 2h−1个结点( h ≥ 1 h≥1 h≥1)
由树的基本概念与性质可知,高度为 h h h的 m m m叉树至多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m−1mh−1个结点( i ≥ 1 i≥1 i≥1),令 m = 2 m=2 m=2,得有 2 h − 1 2^h-1 2h−1个(满二叉树)。
完全二叉树的性质相关
常见考点1:具有 n n n个结点( n > 0 n>0 n>0)的完全二叉树的高度 h h h为 ⌈ log 2 ( n + 1 ) ⌉ \left\lceil {{\log }_{2}}\left( n+1 \right) \right\rceil ⌈log2(n+1)⌉或 ⌊ log 2 n ⌋ + 1 \left\lfloor {{\log }_{2}}n \right\rfloor +1 ⌊log2n⌋+1。
由完全二叉树的已知性质,假设完全二叉树的高度为 h h h,则有
2 h − 1 − 1 < n ≤ 2 h − 1 {{2}^{h-1}}-1<n\le {{2}^{h}}-1 2h−1−1<n≤2h−1 或 2 h − 1 ≤ n < 2 h {{2}^{h-1}}\le n<{{2}^{h}} 2h−1≤n<2h
取对数后可解得
h − 1 < log 2 ( n + 1 ) ≤ h h-1<{{\log }_{2}}\left( n+1 \right)\le h h−1<log2(n+1)≤h 或 h − 1 ≤ log 2 n < h h-1\le {{\log }_{2}}n<h h−1≤log2n<h
即
h = ⌈ log 2 ( n + 1 ) ⌉ h=\left\lceil {{\log }_{2}}\left( n+1 \right) \right\rceil h=⌈log2(n+1)⌉ 或 h = ⌊ log 2 n ⌋ + 1 h=\left\lfloor {{\log }_{2}}n \right\rfloor +1 h=⌊log2n⌋+1
由此可知,第 i i i个结点所在层次为 ⌈ log 2 ( i + 1 ) ⌉ \left\lceil {{\log }_{2}}\left( i+1 \right) \right\rceil ⌈log2(i+1)⌉或 ⌊ log 2 i ⌋ + 1 \left\lfloor {{\log }_{2}}i \right\rfloor +1 ⌊log2i⌋+1。
常见考点2:对于完全二叉树,可以由结点数 n n n推出度为 0 0 0、 1 1 1和 2 2 2的结点个数 n 0 n_0 n0、 n 1 n_1 n1和 n 2 n_2 n2。
由之前的结论可知,完全二叉树最多只有一个度为
n
n
n的结点,即
n
1
=
0
n_1=0
n1=0或
1
1
1。
且
n
0
=
n
2
+
1
n_0=n_2+1
n0=n2+1,则
n
0
+
n
2
=
2
n
2
+
1
n_0+n_2=2n_2+1
n0+n2=2n2+1必为奇数。
若完全二叉树有
2
k
2k
2k(偶数)个结点,则必有
n
1
=
1
n_1=1
n1=1,
n
0
=
k
n_0=k
n0=k,
n
2
=
k
−
1
n_2=k-1
n2=k−1
若完全二叉树有
2
k
−
1
2k-1
2k−1(奇数)个结点,则必有
n
1
=
0
n_1=0
n1=0,
n
0
=
k
n_0=k
n0=k,
n
2
=
k
−
1
n_2=k-1
n2=k−1
二叉树的存储结构
1.顺序存储结构
二叉树的顺序存储是指用一组连续的存储单元依次自上而下、自左而右存储完全二叉树上的结点元素,即将完全二叉树上编号为
i
i
i的结点元素存储在一维数组下标为
i
−
1
i-1
i−1的分量中。
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映结点之间的逻辑关系。这样既能最大可能地节省空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
#define MaxSize 100
typedef struct TreeNode {
int value;//结点中的数据元素
bool isEmpty;//结点是否为空
}TreeNode;
TreeNode t[MaxSize];//静态顺序存储二叉树
int n;//记录结点总数
void TreeInit() {//初始化
for (int i = 0; i < MaxSize; ++i)
t[i].isEmpty = true;//将每个结点状态设为空
return;
}
用这种方式存储的完全二叉树,对第
i
i
i个结点,其左孩子为
2
i
2i
2i,右孩子为
2
i
+
1
2i+1
2i+1,父节点为
⌊
i
/
2
⌋
\left\lfloor i/2 \right\rfloor
⌊i/2⌋,所在层次为
⌈
log
2
(
i
+
1
)
⌉
\left\lceil {{\log }_{2}}\left( i+1 \right) \right\rceil
⌈log2(i+1)⌉或
⌊
log
2
i
⌋
+
1
\left\lfloor {{\log }_{2}}i \right\rfloor +1
⌊log2i⌋+1。
若该完全二叉树共有
n
n
n个结点,则对第
i
i
i个结点,判断其是否有左右孩子,以及是否为分支/叶子结点,可用以下逻辑实现:
bool Have_LeftChild(int i) {//判断结点i是否有左孩子
return (2 * i <= n && !t[2 * i].isEmpty) ? true : false;
}
bool Have_RightChild(int i) {//判断结点i是否有右孩子
return (2 * i + 1 <= n && !t[2 * i + 1].isEmpty) ? true : false;
}
bool Is_Leaf(int i) {//判断结点i是否为叶子结点
return i > (n / 2) ? true : false;
}
如果不是完全二叉树,依旧按照顺序存储的方式存储二叉树,则数组下标将不再能够表示各结点之间的逻辑关系。为解决这个问题,可以将二叉树中的结点编号与完全二叉树一一对应,从而进行顺序存储。但是这样又会产生新的问题:假如一棵二叉树中除叶结点外的所有结点都只有右孩子,那么,若这棵二叉树深度为 h h h,则它需要使用 2 h − 1 2^h-1 2h−1大小的数组对所有结点进行存放,空间利用率非常非常低。因此,除了完全二叉树可以采用顺序存储结构,一般的二叉树通常采用的都是链式存储结构。
2.链式存储结构
由于顺序存储的空间利用率较低,因此二叉树一般都采用链式存储结构,用链表结点来存储二叉树中的每个结点。在二叉树中,结点结构通常包括若干数据域和若干指针域。二叉链表至少包含
3
3
3个域:数据域
d
a
t
a
data
data,左指针域
l
c
h
i
l
d
lchild
lchild,右指针域
r
c
h
i
l
d
rchild
rchild。实际运用中,还可以增加数据域或指针域比如增加一个指向父节点的指针,变成三叉链表的存储结构。
二叉树的链式存储结构描述如下:
typedef struct Elemtype {//复合数据域
int value;//只有一个int类型
}Elemtype;
typedef struct BiTNode {//二叉链表结点
Elemtype data;//数据域
struct BiTNode* lchild, * rchild;//左、右孩子指针
// struct BiTNode* parent;//父节点指针(根据实际需要决定是否添加)
}BiTNode, * BiTree;
由二叉链表的性质,
n
n
n个结点的二叉链表共有
n
+
1
n+1
n+1个空指针域,这一部分可用于构造线索二叉树。
(计算方法:度为
2
2
2的结点有
0
0
0个空指针域,度为
1
1
1的结点有
1
1
1个空指针域,度为
0
0
0的结点有
2
2
2个空指针域。由此前的性质可知,当
n
1
=
1
n_1=1
n1=1时,
n
0
=
n
/
2
n_0=n/2
n0=n/2,空指针域数量为
1
+
2
∗
n
/
2
=
n
+
1
1+2*n/2=n+1
1+2∗n/2=n+1;当
n
1
=
0
n_1=0
n1=0时,
n
0
=
(
n
+
1
)
/
2
n_0=(n+1)/2
n0=(n+1)/2,空指针域数量为
0
+
2
∗
(
n
+
1
)
/
2
=
n
+
1
0+2*(n+1)/2=n+1
0+2∗(n+1)/2=n+1。故无论
n
n
n的值为奇数还是偶数,
n
n
n个结点的二叉链表的空指针域都为
n
+
1
n+1
n+1个)
对根结点的初始化操作如下:
BiTree root = NULL;//定义一棵空树
void InitBiTree() {//初始化根结点
root = (BiTree)malloc(sizeof(BiTNode));
root->data = { 1 };
// root->parent = NULL;
root->lchild = NULL;
root->rchild = NULL;
return;
}
插入根结点的左孩子:可依照此逻辑完成对二叉树的构建。
void InsertRLChild() {//插入新节点(根结点的左孩子)
BiTNode* p = (BiTNode*)malloc(sizeof(BiTNode));
p->data = { 2 };
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;
// p->parent = root;
return;
}
完整代码可以看我的Github:传送门
若要对二叉树进行遍历,只能从根结点开始,依次向下。在之后的章节里还会继续讨论这个问题。
408考研初试中,通常都是考察不带父指针的情况,即考察使用二叉链表存储二叉树。
若从
0
0
0开始对结点进行编号,则对应的操作逻辑都需要进行更改。
以上。