数据结构:树和二叉树
引言:
树结构是一类重要的非线性数据结构。树是以分支关系定义的层次结构,在计算机领域广泛应用,尤以二叉树最为常用。在操作系统中,用树来表示文件目录的组织结构,在编译系统中,用树来表示源程序的语法结构,在数据库系统中,树结构也是信息的重要组织形式。本章重点讨论二叉树的存储结构及其各种操作,并研究树和森林与二叉树的转换关系,最后介绍树的应用。
1. 树和二叉树的定义
1.1 树的定义
树是 n 个结点的有限集,它或为空树;或为非空树,对于非空树 T:
(1) 有且仅有一个称之为根的结点;
(2) 除根结点以外的其余结点可分为 m 个互不相交的有限集 T1,T2,…,Tm,其中每一个集合本身又是一棵树,并且称为根的子树。
1.2 树的基本术语
(1) 结点:树中的一个独立单元。包含一个是数据元素及若干指向其子树的分支。
(2) 结点的度:结点拥有的子树数称为结点的度。
(3) 树的度:树的度是树内各结点度的最大值。
(4) 叶子:度为 0 的结点称为叶子或终端结点。
(5) 非终端结点:度不为 0 的结点称为非终端结点或分支结点。
(6) 双亲和孩子:结点的子树的根称为该结点的孩子,相应的,该结点称为孩子的双亲。
(7) 兄弟:同一个双亲的孩子之间互称兄弟。
(8) 祖先:从根到该结点所经分支上的所有结点。
(9) 子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。
(10) 层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。
(11) 堂兄弟:双亲在同一层的结点互为堂兄弟。
(12) 树的深度:树中结点的最大层次称为树的深度或高度。
(13) 有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
(14) 森林:是 m ( m
≥
\geq
≥ 0 ) 棵互不相交的树的集合。
1.3 二叉树的定义
二叉树是 n ( n ≥ \geq ≥ 0 ) 个结点所构成的集合,它或为空树 ( n = 0 );或为非空树,对于非空树 T T T:
(1) 有且仅有一个称之为根的结点;
(2) 除根结点以外的其余结点分为两个互不相交的子集
T
1
T_1
T1 和
T
2
T_2
T2,分别称为
T
T
T 的左子树和右子树,且
T
1
T_1
T1 和
T
2
T_2
T2 本身又都是二叉树。
二叉树与树一样具有递归性质,二叉树与树的区别主要有以下两点:
(1) 二叉树的每个结点至多只有两颗子树(即二叉树中不存在度大于 2 的结点);
(2) 二叉树的子树有左右之分,其次序不能任意颠倒。
2. 树和二叉树的抽象数据类型定义
树的抽象数据类型定义:
ADT Tree {
数据对象 D:D 是具有相同特性的数据元素的集合。
数据关系 R:若 D 为空集,则称为空树;
若 D 仅含一个数据元素,则 R 为空集,否则 R = { H },H 是如下二元关系:
(1) 在 D 中存在唯一的称为根的数据元素 root,它在关系 H 下无前驱;
(2) 若 D - { root }
≠
\neq
=
∅
\emptyset
∅,且存在 D - { root } 的一个划分
D
1
D_1
D1,
D
2
D_2
D2,…,
D
m
D_m
Dm (m > 0),对任意 j
≠
\neq
= k ( 1
≤
\leq
≤ j,k
≤
\leq
≤ m ) 有
D
j
∩
D
k
D_j \cap D_k
Dj∩Dk =
∅
\emptyset
∅,且对任意的 i ( 1
≤
\leq
≤ i
≤
\leq
≤ m ),唯一存在数据元素
x
i
∈
D
i
x_i \in D_i
xi∈Di ,有 < root,
x
i
x_i
xi >
∈
\in
∈ H;
(3) 对应于 D - { root } 的划分,H - { < root,
x
i
x_i
xi >,…,< root,
x
m
x_m
xm > } 有唯一的一个划分
H
1
H_1
H1,
H
2
H_2
H2,…,
H
m
H_m
Hm ( m > 0 ),对任意 j
≠
\neq
= k ( 1
≤
\leq
≤ j,k
≤
\leq
≤ m ) 有
H
j
∩
H
k
H_j \cap H_k
Hj∩Hk =
∅
\emptyset
∅,且对任意 i ( 1
≤
\leq
≤ i
≤
\leq
≤ m ),
H
i
H_i
Hi 是
D
i
D_i
Di 上的二元关系,(
D
i
D_i
Di,{
H
i
H_i
Hi} ) 是一棵符合本定义的树,称为根 root 的子树。
基本操作 P:
InitTree (&T)
操作结果:构造空树 T。
DestoryTree (&T)
初始条件:树 T 存在。
操作结果:销毁树 T。
CreateTree (&T)
初始条件:definition 给出树 T 的定义。
操作结果:按 definition 构造树 T。
ClearTree (&T)
初始条件:树 T 存在。
操作结果:将树 T 清为空树。
TreeEmpty (T)
初始条件:树 T 存在。
操作结果:若 T 为空树,则返回 true,否则 false。
TreeDepth (T)
初始条件:树 T 存在。
操作结果:返回 T 的深度。
Root (T)
初始条件:树 T 存在。cur_e 是 T 中某个结点。
操作结果:返回 T 的根。
Value (T,cur_e)
初始条件:树 T 存在。cur_e 是 T 中某个结点。
操作结果:返回 cur_e 的值。
Assign (T,cur_e,value)
初始条件:树 T 存在。cur_e 是 T 中某个结点。
操作结果:结点 cur_e 赋值为 value。
Parent (T,cur_e)
初始条件:树 T 存在。cur_e 是 T 中某个结点。
操作结果:若 cur_e 是 T 的非根结点,则返回它的双亲,否则函数值为 “ 空 ”。
LeftChild (T,cur_e)
初始条件:树 T 存在。cur_e 是 T 中某个结点。
操作结果:若 cue_e 是 T 的非叶子结点,则返回它的最左孩子,否则返回 " 空 "。
RightChild (T,cur_e)
初始条件:树 T 存在。cur_e 是 T 中某个结点。
操作结果:若 cue_e 是 T 的非叶子结点,则返回它的最左孩子,否则返回 " 空 "。
LeftSibling (T,cur_e)
初始条件:树 T 存在。cur_e 是 T 中某个结点。
操作结果:若 cur_e 有左兄弟,则返回它的左兄弟,否则函数值为 " 空 "。
RightSibling (T,cur_e)
初始条件:树 T 存在。cur_e 是 T 中某个结点。
操作结果:若 cur_e 有右兄弟,则返回它的右兄弟,否则函数值为 " 空 "。
InsertChild (&T,p,i,c)
初始条件:树 T 存在。p 指向 T 中某个结点,1
≤
\leq
≤ i
≤
\leq
≤ p 所指结点的度 +1,非空树 c 与 T 不相交。
操作结果:插入 c 为 T 中 p 指结点的第 i 棵子树。
DeleteChild (&T,p,i)
初始条件:树 T 存在。p 指向 T 中某个结点,1
≤
\leq
≤ i
≤
\leq
≤ p 所指结点的度。
操作结果:删除 T 中 p 所指结点的第 i 棵子树。
TraverseTree (T)
初始条件:树 T 存在。
操作结果:按某种次序对 T 的每个结点访问一次。
} ADT Tree
二叉树的抽象数据类型定义:
ADT BinaryTree {
数据对象 D:D 是具有相同特性的数据元素的集合。
数据关系 R:
若 D 为空集,则称为空二叉树;
若 D 仅含一个数据元素,则 R 为空集,否则 R = { H },H 是如下二元关系:
(1) 在 D 中存在唯一的称为根的数据元素 root,它在关系 H 下无前驱;
(2) 若 D - { root }
≠
\neq
=
∅
\emptyset
∅,则存在 D - { root } = {
D
l
D_l
Dl,
D
r
D_r
Dr },且
D
l
∩
D
r
D_l \cap D_r
Dl∩Dr =
∅
\emptyset
∅;
(3) 若
D
l
D_l
Dl
≠
\neq
=
∅
\emptyset
∅,则 $D_l 中存在唯一的元素
x
l
x_l
xl,< root,
x
l
x_l
xl >
∈
\in
∈ H,且存在
D
l
D_l
Dl 上的关系
H
l
H_l
Hl
⊂
\subset
⊂ H;H = { < root,
x
l
x_l
xl >,< root,
x
r
x_r
xr >,
H
l
H_l
Hl,
H
r
H_r
Hr };
(4) (
D
l
D_l
Dl,{
H
l
H_l
Hl } ) 是一颗符合本定义的二叉树,称为根的左子树,(
D
r
D_r
Dr,{
H
r
H_r
Hr } ) 是一颗符合本定义的二叉树。称为根的右子树。
基本操作 P:
InitBiTree (&T)
操作结果:构造空二叉树 T。
DestoryBiTree (&T)
初始条件:二叉树 T 存在。
操作结果:销毁二叉树 T。
CreateBiTree (&T,definition)
初始条件:definition 给出二叉树 T 的定义。
操作结果:按 definition 构造二叉树 T。
ClearBiTree (&T)
初始条件:二叉树 T 存在。
操作结果:将二叉树 T 清为空树。
BiTreeEmpty (T)
初始条件:二叉树 T 存在。
操作结果:若 T 为空二叉树,则返回 true,否则 false。
BiTreeDepth (T)
初始条件:二叉树 T 存在。
操作结果:返回 T 的深度。
Root (T)
初始条件:二叉树 T 存在。
操作结果:返回 T 的根。
Value (T,e)
初始条件:二叉树 T 存在,e 是 T 中某个结点,
操作结果: 返回 e 的值。
Assign (T,&e,value)
初始条件:二叉树 T 存在,e 是 T 中某个结点,
操作结果:结点 e 赋值为 value。
Parent (T,e)
初始条件:二叉树 T 存在,e 是 T 中某个结点,
操作结果: 若 e 是 T 的非根节点,则返回它的双亲,否则返回 " 空 "。
LeftChild (T,e)
初始条件:二叉树 T 存在,e 是 T 中某个结点,
操作结果: 返回 e 的左孩子。若 e 无左孩子,则返回 " 空 "。
RightChild (T,e)
初始条件:二叉树 T 存在,e 是 T 中某个结点,
操作结果: 返回 e 的右孩子。若 e 无右孩子,则返回 " 空 "。
LeftSibling (T,e)
初始条件:二叉树 T 存在,e 是 T 中某个结点,
操作结果: 返回 e 的左兄弟。若 e 是 T 的左孩子或无左兄弟,则返回 " 空 "。
RightSibling (T,e)
初始条件:二叉树 T 存在,e 是 T 中某个结点,
操作结果: 返回 e 的右兄弟。若 e 是 T 的右孩子或无右兄弟,则返回 " 空 "。
InsertChild (&T,p,LR,c)
初始条件:二叉树 T 存在,p 指向 T 中某个结点,LR 为 0 或 1,非空二叉树 c 与 T 不相交且右子树为空。
操作结果:根据 LR 为 0 或 1,插入 c 为 T 中 p 所指结点的左或右子树。p 所指结点的原有左或右子树则成为 c 的右子树。
DeleteChild (&T,p,LR)
初始条件:二叉树 T 存在,p 指向 T 中某个结点,LR 为 0 或 1.
操作结果:根据 LR 为 0 或 1,删除 T 中 p 所指结点的左或右子树。
PreOrderTraverse (T)
初始条件:二叉树 T 存在。
操作结果:先序遍历 T,对每个结点访问一次。
InOrderTraverse (T)
初始条件:二叉树 T 存在。
操作结果:中序遍历 T,对每个结点访问一次。
PostOrderTraverse (T)
初始条件:二叉树 T 存在。
操作结果:后序遍历 T,对每个结点访问一次。
LevelOrderTraverse (T)
初始条件:二叉树 T 存在。
操作结果:层序遍历 T,对每个结点访问一次。
} ADT BinaryTree
3. 二叉树的性质和存储结构
3.1 二叉树的性质
二叉树具有以下重要特性:
性质 1:在二叉树的第 i i i 层上至多有 2 i − 1 2^{i - 1} 2i−1 个结点 ( i ≥ 1 i \geq 1 i≥1 )。
性质 2:深度为 k k k 的二叉树至多有 2 k − 1 2^{k - 1} 2k−1 个结点 ( k ≥ 1 k \geq 1 k≥1 )。
性质 3:对任何一棵二叉树 T,如果其终端结点数为 n 0 n_0 n0,度为 2 的结点数为 n 2 n_2 n2,则 n 0 n_0 n0 = n 2 n_2 n2 + 1。
现在介绍两种特殊形态的二叉树:
满二叉树:
深度为
k
k
k 且含有
2
k
2^k
2k-1 个结点的二叉树。
满二叉树的特点:
每一层上的结点数都是最大结点数。
完全二叉树:
深度为
k
k
k 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为
k
k
k 的满二叉树中编号从 1 至
n
n
n 的结点一一对应时,称为完全二叉树
完全二叉树的特点:
(1) 叶子结点只可能在层次最大的两层上出现;
(2) 对任一结点,若其右分支下的子孙的最大层次为
l
l
l,则其左分支下的子孙的最大层次必为
l
l
l 或
l
+
1
l+1
l+1。( 从右至左填充结点 )
性质 4:具有 n n n 个结点的完全二叉树的深度为 l o g 2 n + 1 log_2n + 1 log2n+1。
性质 5:如果对一棵有 n 个结点的完全二叉树的结点按层序编号,则对任一结点 i ( 1
≤
\leq
≤ i
≤
\leq
≤ n ),有:
(1) 如果 i = 1,则结点 i 是二叉树的根,无双亲;如果 i > 1 则其双亲是结点 i / 2。
(2) 如果 2i > n,则结点 i 无左孩子;否则其左孩子为结点 2i。
(3) 如果 2i + 1 > n,则结点 i 无右孩子;否则其右孩子为结点 2i + 1。
3.2 二叉树的存储结构
类似线性表,二叉树的存储结构也可采用顺序存储和链式存储两种方式。
二叉树的顺序存储结构:
// 二叉树的顺序存储表示
# define MAXTSIZE 100 // 二叉树的最大结点数
typedef TElemType SqBiTree[MAXTSIZE]; // 0 号单元存储根节点
SqBiTree bt;
为了能够在存储结构中反映出结点之间的逻辑关系,必须将二叉树中的结点按照一定的规律安排在这组单元中。
对于完全二叉树,只要从根起按层序存储即可,依次自上而下、自左至右存储节点元素,如图所示:
对于一般二叉树,则应将其每个结点与完全二叉树上的结点相对照,存储在一位数组的相应分量中。如图所示:
由此可见,这种顺序存储结构仅适用于完全二叉树。因为,在最坏的情况下,一个深度为
k
k
k 且只有
k
k
k 个结点的单支树却需要长度为
2
k
−
1
2^k-1
2k−1 的一维数组,造成了存储空间的极大浪费。因此,对于一般二叉树,更适合采取下面的链式存储结构。
二叉树的链式存储结构:
设计不同的结点结构可构成不同形式的链式存储结构。由二叉树的定义得知,二叉树的结点由一个数据元素和分别指向其左、右子树的两个分支构成,则表示二叉树的链表中的结点至少包含 3 个域:数据域和左、右指针域。利用这两种结点结构所得二叉树的存储结构分别称之为二叉链表和三叉链表。
在不同的存储结构中实现二叉树的操作方法也不痛,如找结点 x 的双亲在三叉链表中很容易实现,而在二叉链表中则需从根指针出发巡查。由此,在具体应用中采用什么存储结构,除根据二叉树的形态之外还应考虑需进行何种操作。下一节的二叉树遍历及其应用算法均采用以下定义的二叉链表形式实现。
typedef struct BiTNode
{
TElemType data; // 结点数据域
struct BiTNode *lchild, *rchild; // 左右孩子指针
}BiTNode, *BiTree;
4. 遍历二叉树和线索二叉树
在二叉树的一些应用中,常常要求在树中查找具有某种特征的结点,或者是对树中的全部结点逐一进行处理,这就提出了一个遍历二叉树的问题。线索二叉树是在第一次遍历时将结点的前驱、后继信息存储下来,便于再次遍历二叉树。
4.1 遍历二叉树
遍历二叉树是指按某条搜索路径巡访书中的每个结点,使得每个结点均被访问一次,而且仅被访问一次。访问的含义很广,包括但不限于输出结点信息,对结点进行运算和修改等。
依照二叉树的递归定义,遍历二叉树即循环遍历根节点、左子树和右子树。依据对根节点的访问次序,将遍历分为先序遍历、中序遍历和后序遍历。而分别由这三种遍历方式得到的结果又称为前缀表示 ( 波兰式 )、中缀表示和后缀表示 ( 逆波兰式 )。下面给出中序遍历二叉树基本操作的递归算法:
算法1:中序遍历的递归算法
【算法描述】
void InOrderTraverse (BiTree T)
{ // 中序遍历二叉树 T 的递归算法
if (T) // 若二叉树非空
{
InOrderTraverse(T->lchild); // 中序遍历左子树
cout << T->data; // 访问根节点
InOrderTraverse(T->rchild); // 中序遍历右子树
}
}
只需改变输出语句的顺序,便可类似的实现先序遍历和后序遍历的递归算法。
我们也可以利用栈将递归算法改写成非递归算法:
算法2:中序遍历的非递归算法
【算法步骤】
(1) 初始化一个空栈 S,指针 p 指向根节点。
(2) 申请一个结点空间 q,用来存放栈顶弹出的元素。
(3) 当 p 非空或者栈 S 非空时,循环执行以下操作:
- 如果 p 非空,则将 p 进栈,p 指向该结点的左孩子;
- 如果 p 为空,则弹出栈顶元素并访问,将 p 指向该结点的右孩子。
【算法描述】
void InOrderTraverse(BiTree T)
{ // 中序遍历二叉树 T 的非递归算法
InitStack(S); p = T;
q = new BiTNode;
while (p || !StackEmpty(S))
{
if (p) // p 非空
{
Push(S, p); // 根指针进栈
p = p->lchild; // 遍历左子树
}
else // p 为空
{
Pop(S, q); // 叶子指针退栈
cout << q->data; // 访问根结点
p = q->rchild; // 遍历右子树
}
} // while
}
时间复杂度:O(n)
算法3:先序遍历的顺序建立二叉链表
【算法步骤】
(1) 扫描字符序列,读入字符 ch。
(2) 如果 ch 是一个 " # " 字符,则表明该二叉树为空树,即 T 为 NULL;否则执行以下操作:
- 申请一个结点空间 T;
- 将 ch 赋给 T->data;
- 递归创建 T 的左子树;
- 递归创建 T 的右子树;
【算法描述】
void CreateBiTree(BiTree &T)
{ // 按先序次序输入二叉树中结点的值 (一个字符),创建二叉链表表示的二叉树 T
cin >> ch;
if (ch == '#') T = NULL; // 递归结束,建空树
else // 递归创建二叉树
{
T = new BiTNode; // 生成根结点
T->data = ch; // 根结点数据域置为 ch
CreateBiTree(T->lchild); // 递归创建左子树
CreateBiTree(T->rchild); // 递归创建右子树
}
}
算法4:复制二叉树
【算法步骤】
如果是空树,递归结束,否则执行以下操作:
- 申请一个新结点空间,复制根结点;
- 递归复制左子树;
- 递归复制右子树。
【算法描述】
void Copy(BiTree T, BiTree &NewT)
{ // 复制一棵和 T 完全相同的二叉树
if (T == NULL) // 如果是空树,递归结束
{
NewT = NULL;
return;
}
else
{
NewT = new BiTNode;
NewT->data = T->data; // 复制根结点
Copy(T->lchild, NewT->lchild); // 递归复制左子树
Copy(T->rchild, NewT->rchild); // 递归复制右子树
}
}
算法5:计算二叉树的深度
【算法步骤】
如果是空树,递归结束,深度为 0,否则执行以下操作:
- 递归计算左子树的深度记为 m;
- 递归计算右子树的深度记为 n;
- 如果 m 大于 n,二叉树的深度为 m + 1,否则为 n + 1。
【算法描述】
int Depth(BiTree T)
{ // 计算二叉树 T 的深度
if (T == NULL) return 0; // 如果是空树,深度为 0,递归结束
else
{
m = Depth(T->lchild); // 递归计算左子树的深度记为 m
n = Depth(T->rchild); // 递归计算右子树的深度记为 n
if (m > n) return (m + 1); // 二叉树的深度为 m 与 n 的较大者加 1
else return (n + 1);
}
}
算法6:统计二叉树中结点的个数
【算法描述】
int NodeCount(BiTree T)
{
if(T = BULL) return 0; // 如果是空树,则结点个数为 0,递归结束
else return NodeCount(T->lchild) + NodeCount(T->rchild) + 1; // 否则结点个数为:左子树结点个数 + 右子树结点个数 + 1
}
4.2 线索二叉树
线索二叉树的基本概念
二叉树中指示结点的前驱与后继的指针称作线索,将这种线索存放在原二叉树中的空链域 ( lchild 或 rchild ) 中。
另在原二叉树中新增两个标志域 RTag 与 LTag 用于判别 rchild 域与 lchild 域是否存放了线索 ( 1 为是,0 为否 )。
上述操作称为线索化,加上线索的二叉树称为线索二叉树。
线索二叉树的类型定义:
typedef struct BiThrNode
{
TElemType data;
structBiThrNode *lchild, *rchild; // 左右孩子指针
int LTag, RTag; // 左右标志
}BiThrNode, *BiThrTree
算法7:以结点 p 为根的子树中序线索化
【算法步骤】
(1) 如果 p 非空,左子树递归线索化。
(2) 如果 p 的左孩子为空,则给 p 加上左线索,将其 LTag 置为 1,让 p 的左孩子指针指向 pre ( 前驱 );否则将 p 的 LTag 置为 0。
(3) 如果 p 的右孩子为空,则给 p 加上右线索,将其 RTag 置为 1,让 pre 的右孩子指针指向 p ( 后继 );否则将 pre 的 RTag 置为 0。
(4) 将 pre 指向刚访问过的结点 p,即 pre = p。
(5) 右子树递归线索化。
【算法描述】
void InThreading(BiThrTree p)
{ // pre 是全局变量,初始化时其右孩子指针为空,便于在树的最左点开始建线索
if (p)
{
InThreading(p->lchild); // 左子树递归线索化
if (!p->lchild) // p 的左孩子为空
{
p->LTag = 1; // 给 p 加上左线索
p->lchild = pre; // p 的左孩子指针指向 pre(前驱)
}
else p->LTag = 0;
if (!p->rchild) // p 的右孩子为空
{
pre->RTag = 1; // 给 p 加上右线索
p->rchild = p; // p 的右孩子指针指向 p(后继)
}
else p->RTag = 0;
pre = p; // 保持 pre 指向 p 的前驱
InThreading(p->rchild); // 右子树递归线索化
}
}
算法8:带头结点的二叉树中序线索化
【算法描述】
void InOrderThreading(BiThrTree &Thrt, BiThrTree T)
{ // 中序遍历二叉树 T,并将其中序线索化,Thrt 指向头结点
Thrt = new BithrNode; // 建头结点
Thrt->LTag = 0; // 头指针有左孩子,若树非空,则其左孩子为树根
Thrt->RTag = 1; // 头结点的右孩子指针为右线索
Thrt->rchild = Thrt; // 初始化时右指针指向自己
if (!T) Thrt->lchild = Thrt; // 若树为空,则左指针也指向自己
else
{
Thrt->lchild = T; pre = Thrt; // 头结点的左孩子指向根,pre 初值指向头结点
InThreading(T); // 对以 T 为根的二叉树进行中序线索化
pre->rchild = Thrt; // 线索化结束后,pre 为最右结点,pre 的右线索指向头结点
pre->RTag = 1;
Thrt->rchild = pre; // 头结点的右线索指向 pre
}
}
算法9:遍历中序线索二叉树
【算法步骤】
(1) 指针 p 指向根结点。
(2) p 为非空树或遍历未结束时,循环执行以下操作:
- 沿左孩子向下,到达最左下结点 *p,它是中序的第一个结点;
- 访问 *p;
- 沿右线索反复查找当前结点 *p 的后继结点并访问后继结点,直至右线索为 0 或者遍历结束;
- 转向 p 的右子树。
【算法描述】
void InOrderTraverse_Thr(BiThrTree T)
{ // T 指向头结点,头结点的左链 lchild 指向根结点
// 中序遍历二叉线索数 T 的非递归算法,对每个数据元素直接输出
p = T->lchild; // p 指向根结点
while (p != T) // 空树或遍历结束时,p == T
{
while (p->LTag == 0) // 沿左孩子向下
p = p->lchild;
cout << p->data; // 访问其左子树为空的结点
while (p->RTag == 1 && p->rchild != T) // 沿右线索访问后继结点
{
p = p->rchild; cout << p->data;
}
p = p->rchild; // 转向 p 的右子树
}
}
时间复杂度为 O(n);
空间复杂度为 O(1)。
5. 树和森林
5.1 树的存储结构
(1) 双亲表示法
这种表示方法以一组连续的存储单元存储树的结点,每个结点除了数据域 data 外,还附设一个 parent 域用以指示其双亲结点的位置。例如,下图为一棵树及其双亲表示的存储结构:
(2) 孩子表示法
由于树中每个结点可能有多棵子树,则可用多重链表,即每个结点有多个指针域。
此时结点就有以下两种结点格式:
若采用第一种结点格式,则多重链表中的结点是同构的。不过这样的话链表中会出现很多空链域,空间较浪费,在一颗有 n 个结点度为 k 的树中必有 n(k-1) + 1 个空链域。
若采用第二种结点格式,则多重链表中的结点是不同构的。这样虽然能节省空间,但操作不方便。
(3) 孩子兄弟法
又称二叉树表示法,以二叉链表做树的存储结构。链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点。以下为结点形式:
typedef struct CSNode
{
ElemType data;
struct CSNode *firstchild, *nextsibling;
}CSNode, *CSTree;