【知识框架】
5.1 树的基本概念
5.1.1 树的定义
树是n(n>=0)个节点的有限集。当n=0时,称为空树
。
在任意一棵非空树中应满足:
- 有且仅有一个特定的称为根(Root)的结点。
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…Tm,其中每个集合本身又是一棵树,并称为根的子树(SubTree)。
显然,树的定义是递归的,即在树的定义中又用到了其自身,树是一种递归的数据结构。
树作为一种逻辑结构,同时也是一种分层结构,有以下两个特点:
- 树的根结点没有前驱,除根结点以外的所有结点有且只有一个前驱(n>0时,根结点是唯一的,不可能存在多个根结点)。
- 树中所有结点可以有零个或多个后继(m>0时,子树的个数没有限制,但它们一定是不相交的)。
树适合用于表示具有层次结构的数据。树中的某个结点(除根结点外)最多只和上一层的一个结点(即其父结点)有直接关系,根结点没有直接上层结点,因此在n个结点的树中有n-1条边。而树中的每个结点与其下一层的零个或多个结点(即其子女结点)有直接关系。
子树T1,T2就是根结点A的子树。
D、G、H、I组成的树又是B为结点的子树,E、J组成的树是C为结点的子树。
5.1.2 基本术语
-
考虑结点 K ∘ K_{\circ} K∘ 根 A A A 到结点 K K K 的唯一路径上的任意结点,称为结点 K K K 的
祖先
。如结点 B B B 是 结点 K K K 的
祖先
,而结点 K K K 是结点 B B B 的子孙
。路径上最接近结点 K K K 的结点 E E E 称为 K K K 的
双亲(Parent)
, 而 K K K 为结点 E E E 的孩子(Child)
。根 A A A 是树中唯一没有双亲的结点。有相同双亲的结点称为
兄弟(Sibling)
,如 结点 K K K 和结点 L L L 有相同的双亲 E , E , E, 即 K K K 和 L L L 为兄弟。 -
树中一个结点的孩子个数称为该结点的
度(Degree)
,树中结点的最大度数称为树的度。如结点 B B B 的 度为 2,结点 D D D 的度为 3,树的度为 3。
-
度大于 0 的结点称为
分支结点(又称非终端结点)
;度为 0 (没有子女结点)的结点称为
叶子结点(又称终端结点)
。在分支结点中,每个结点的分支数就是该结点的度。
-
结点的深度、高度和层次。
结点的层次从树根开始定义,根结点为第 1 层,它的子结点为第 2 层,以此类推。
双亲在同一层的结点互为
堂兄弟
,图 中结点 G \mathrm{G} G 与 E , F , H , I , J \mathrm{E}, \mathrm{F}, \mathrm{H}, \mathrm{I}, \mathrm{J} E,F,H,I,J 互为堂兄弟。结点的深度
是从根结点开始自顶向下逐层累加的。结点的高度
是从叶结点开始自底向上逐层累加的。树的高度(或深度)
是树中结点的最大层数。图中树的高度为 4。 -
有序树和无序树。
树中结点的各子树从左到右是有次序的,不能互换,称该树为
有序树
,否则称为无序树
。假设图为有序树,若将子结点位置互换,则变成一棵不同的树。
-
路径和路径长度。
树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。
注意: 由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下的,
同一双亲的两个孩子之间不存在路径。 -
森林。森杜是 m ( m ≥ 0 ) m(m \geq 0) m(m≥0) 棵互不相交的树的集合。
森林的概念与树的概念十分相近,只要把树的根结点删去就成了森林。
反之,只要给 m m m 棵独立的树加上一个结点,并把 这 m m m 棵树作为该结点的子树,则森林就变成了树。
5.1.3 树的性质
树具有如下最基本的性质:
- 树中的结点数等于所有结点的度数加 1。
- 度为 m m m 的树中第 i i i 层上至多有 m i − 1 m^{i-1} mi−1 个结点 ( i ≥ 1 ) (i \geq 1) (i≥1) 。
- 高度为 h h h 的 m m m 叉树至多有 ( m h − 1 ) / ( m − 1 ) \left(m^{h}-1\right) /(m-1) (mh−1)/(m−1) 个结点。
- 具有 n n n 个结点的 m m m 叉树的最小高度为 ⌈ log m ( n ( m − 1 ) + 1 ) ⌉ \left\lceil\log _{m}(n(m-1)+1)\right\rceil ⌈logm(n(m−1)+1)⌉
(
⌈
⌉
\left\lceil\right\rceil
⌈⌉这种上边带横的,是指不小于括号里式子的最近的整数。(注意是不小于,如果正好是整数的话,就是这个整数本身)
还有一种是下边带横的,指不大于括号里式子的最近整数。(注意是不大于,如果正好是整数的话,就是这个整数本身)
度为m的树中,有n1个度为1的节点……nm个度为m的结点,根据“树中所有结点的度数加 1 等于结点数”的结论,
有
n
=
∑
i
=
0
m
i
n
i
=
n
1
+
2
n
2
+
3
n
3
+
⋯
+
m
n
m
+
1
n=\sum_{i=0}^{m} i n_{i}=n_{1}+2 n_{2}+3 n_{3}+\cdots+m n_{m}+1
n=∑i=0mini=n1+2n2+3n3+⋯+mnm+1 。
又有
n
=
n
0
+
n
1
+
n
2
+
⋯
+
n
m
n=n_{0}+n_{1}+n_{2}+\cdots+n_{m}
n=n0+n1+n2+⋯+nm, 所以叶子结点数n0
n
0
=
(
n
1
+
2
n
2
+
3
n
3
+
⋯
+
m
n
m
+
1
)
−
(
n
1
+
n
2
+
⋯
+
n
m
)
=
n
2
+
2
n
3
+
⋯
+
(
m
−
1
)
n
m
+
1
=
1
+
∑
i
=
2
m
(
i
−
1
)
n
i
\begin{aligned} n_{0} &=\left(n_{1}+2 n_{2}+3 n_{3}+\cdots+m n_{m}+1\right)-\left(n_{1}+n_{2}+\cdots+n_{m}\right) \\ &=n_{2}+2 n_{3}+\cdots+(m-1) n_{m}+1=1+\sum_{i=2}^{m}(i-1) n_{i} \end{aligned}
n0=(n1+2n2+3n3+⋯+mnm+1)−(n1+n2+⋯+nm)=n2+2n3+⋯+(m−1)nm+1=1+i=2∑m(i−1)ni
常用于求解树结点与度之间关系的有:
(1) 总结点数
=
n
0
+
n
1
+
n
2
+
⋯
+
n
m
=n_{0}+n_{1}+n_{2}+\cdots+n_{m}
=n0+n1+n2+⋯+nm 。
(2) 总分支数
=
1
n
1
+
2
n
2
+
⋯
+
m
n
m
(
度为
m
的结点引出
m
条分支
)
。
=1 n_{1}+2 n_{2}+\cdots+m n_{m}(\text { 度为 } m \text { 的结点引出 } m \text { 条分支 })_{\text {。 }}
=1n1+2n2+⋯+mnm( 度为 m 的结点引出 m 条分支 )。
(3) 总结点数
=
=
= 总分支数
+
1
+1
+1 。
5.1.4 树的抽象数据类型
InitTree(*T) | 构造空树T |
---|---|
DestroyTree(*T) | 销毁树T |
CreateTree(*T,definition) | 按definition中给出树的定义来构造树 |
ClearTree(*T) | 若树T存在,则将T清为空树 |
TreeEmpty(T) | 若T为空树,返回True,否则返回False |
TreeDepth(T) | 返回树的深度 |
Root(T) | 返回T的根结点 |
Value(T,cur_e) | cur_e是树T中的一个结点,返回此结点的值 |
Assign(T,cur_e,value) | 给树T的结点cur_e赋值为value |
Parent(T,cur_e) | 若cur_e是树T的非根结点,则返回它的双亲,否则返回空 |
LifeChild(T,cur_e) | 若cur_e是树T的非叶结点,则返回它的最左孩子,否则返回空 |
RightSibling(T,cur_e) | 若cur_e有右兄弟,则返回它的右兄弟,否则返回空 |
InsertChild(*T,*p,i,c) | 其中p指向树T的某个结点,i为所指结点p的度上加1,非空树c与T不相交,操作结果为插入c为树T中p指结点的第i棵子树 |
DeleteChild(*T,*p,i) | 其中p指向树T的某个结点,i为所指结点p的度,操作结果为删除T中p所指结点的第i棵子树 |
5.2 二叉树的概念
5.2.1 二叉树的定义及其主要特性
- 二叉树的定义
二叉树是另一种树形结构,特点是每个结点至多只能有两棵子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,次序不能任意颠倒。
与树相似,二叉树也以递归形式定义。二叉树是n(n>=0)个结点的有限集合:
- 或者为空二叉树,即n=1。
- 或者由一个根结点和两个互不相交的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
二叉树是一棵有序树,若将左、右子树颠倒,则称为一棵不同的二叉树。即使树中结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树的五种基本形态:
二叉树与度为2的有序树的区别:
- 度为2的树至少有三个结点,而二叉树可以为空。
- 度为2的有序树的孩子左右次序是相对另一个孩子而言的,若某个结点只有一个孩子,则这个孩子就无需区分左右次序,而二叉树无论其孩子数是否为2,均需确定其左右次序,即二叉树的结点次序不是相对于另一结点而言,而是确定的。
特殊二叉树:
1)满二叉树:
在一棵树上,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树
。
一棵高度为 h h h, 且含有 2 h − 1 2^{h}-1 2h−1 个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点,如图所示。满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点之外的每个结点度数均为 2 。
可以对满二叉树按层序编号: 约定编号从根结点(根结点编号为 1 ) 起,自上而下,自左 向右。这样,每个结点对应一个编号,对于编号为 i i i 的结点,若有双亲,则其双亲为 ⌊ i / 2 ⌋ \lfloor i/ 2\rfloor ⌊i/2⌋, 若有左孩子,则左孩子为 2 i ; 2 i ; 2i; 若有右孩子,则右孩子为 2 i + 1 2 i+1 2i+1 。
2)完全二叉树
对一棵具有n个结点的二叉树层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树位置中完全相同,则称这棵二叉树为完全二叉树
。
高度为h、有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树
。
完全二叉树特点如下:
(1) 若 i ≤ ⌊ n / 2 ⌋ i \leq\lfloor n / 2\rfloor i≤⌊n/2⌋, 则结点 i i i 为分支结点,否则为叶子结点。
(2) 叶子结点只可能在层次最大的两层(最下两层)上出现。对于最大层次中的叶子结点,都依次排列在该层最左边的位置上。倒数第二层,若有叶子结点,一定都在右部连续位置。
(3) 若有度为 1 的结点,则只可能有一个,且该结点只有左孩子而无右孩子(重要特征)。
(4) 按层序编号后,一旦出现某结点(编号为 i i i )为叶子结点或只有左孩子,则编号大于 i i i 的结点均为叶子结点。
(5) 若 n n n 为奇数,则每个分支结点都有左孩子和右孩子; 若 n n n 为偶数,则编号最大的分支结点(编号为 n / 2 ) n / 2 ) n/2) 只有左孩子,没有右孩子,其余分支结点左、右孩子都有。
完全二叉树就是对应相同高度的满二叉树缺失最右边的一些连续叶子结点。
3)二叉排序树
左子树上所有结点的关键字均小于根结点的关键字(值);右子树的所有结点的关键字(值)均大于根结点的关键字(值);左子树和右子树又各是一棵二叉排序树。
4)平衡二叉树
树上任一结点的左子树和右子树的深度之差不超过1。
- 二叉树的性质
- 非空二叉树上的叶子结点数等于度为 2 的结点数加 1, 即 n 0 = n 2 + 1 n_{0}=n_{2}+1 n0=n2+1 。
证明:
设度为 0,1 和 2 的结点个数分别为
n
0
,
n
1
n_{0}, n_{1}
n0,n1 和
n
2
n_{2}
n2, 结点总数
n
=
n
0
+
n
1
+
n
2
n=n_{0}+n_{1}+n_{2}
n=n0+n1+n2 。 再看二叉树中的分支数,除根结点外,其余结点都有一个分支进入,设
B
B
B 为分支总数, 则
n
=
B
+
1
n=B+1
n=B+1 。由于这些分支是由度为 1 或 2 的结点射出的,所以又有
B
=
n
1
+
2
n
2
B=n_{1}+2 n_{2}
B=n1+2n2 。
于是得
n
0
+
n
1
+
n
2
=
n
1
+
2
n
2
+
1
n_{0}+n_{1}+n_{2}=n_{1}+2 n_{2}+1
n0+n1+n2=n1+2n2+1, 则
n
0
=
n
2
+
1
n_{0}=n_{2}+1
n0=n2+1 。
注意: 该结论经常在选择题中用到。拓展到任意一棵树,若结点数量为 n n n, 则边的数量为 n − 1 n-1 n−1 。
如图结点总数为10,由A、B、C、D度为2的结点,F、G、H、I、J度为1的0的叶子结点和E度为1的结点组成。总和为4+1+5=10。
换个角度,再数一数它的连接线数,由于根结点只有分支出去,没有分支进入,所以分支线总数为结点总数减去 1。图中就是 9 个分支。对于
A
\mathrm{A}
A 、B、C、D 结点来说,它们都有两个分支线出去,而
E
\mathrm{E}
E 结点只有一个分支线出去。所以总分支为
4
×
2
+
1
×
1
=
9
4 \times 2+1 \times 1=9
4×2+1×1=9 。
用代数表达就是分支线总数=n-1=n
1
+
2
n
2
_{1}+2 \mathrm{n}_{2}
1+2n2 。因为刚才我们有等式
n
=
n
0
+
n
1
+
\mathrm{n}=\mathrm{n}_{0}+\mathrm{n}_{1}+
n=n0+n1+
n
2
\mathrm{n}_{2}
n2, 所以可推导出
n
0
+
n
1
+
n
2
−
1
=
n
1
+
2
n
2
\mathrm{n}_{0}+\mathrm{n}_{1}+\mathrm{n}_{2}-1=\mathrm{n}_{1}+2 \mathrm{n}_{2}
n0+n1+n2−1=n1+2n2 。结论就是
n
0
=
n
2
+
1
∘
\mathrm{n}_{0}=\mathrm{n}_{2}+1_{\circ}
n0=n2+1∘
- 非空二叉树上第 k k k 层上至多有 2 k − 1 2^{k-1} 2k−1 个结点 ( k ≥ 1 ) (k \geq 1) (k≥1) 。 第 1 层至多有 2 1 − 1 = 1 2^{1-1}=1 21−1=1 个结点 ( ( ( 根 ) ) ), 第 2 层至多有 2 2 − 1 = 2 2^{2-1}=2 22−1=2 个结点,以此类推,可以证明 其为一个公比为 2 的等比数列 2 k − 1 2^{k-1} 2k−1.
- 高度(深度)为
h
h
h 的二叉树至多有
2
h
−
1
2^{h}-1
2h−1 个结点
(
h
≥
1
)
(h \geq 1)
(h≥1) 。
该结论利用性质 2 求前 h h h 项的和,即等比数列求和的结果。
- 对完全二叉树按从上到下、从左到右的顺序依次编号 1 , 2 , ⋯ , n 1,2, \cdots, n 1,2,⋯,n, 则有以下关系:
(1) 当 i > 1 i>1 i>1 时,结点 i i i 的双亲的编号为 ⌊ i / 2 ⌋ \lfloor i / 2\rfloor ⌊i/2⌋, 即当 i i i 为偶数时,其双亲的编号为 i / 2 i / 2 i/2, 它是 双亲的左孩子; 当 i i i 为奇数时,其双亲的编号为 ( i − 1 ) / 2 (i-1) / 2 (i−1)/2, 它是双亲的右孩子。
(2) 当 2 i ≤ n 2 i \leq n 2i≤n 时,结点 i i i 的左孩子编号为 2 i 2 i 2i, 否则无左孩子。
(3) 当 2 i + 1 ≤ n 2 i+1 \leq n 2i+1≤n 时,结点 i i i 的右孩子编号为 2 i + 1 2 i+1 2i+1 ,否则无右孩子。
(4) 结点 i i i 所在层次(深度)为 ⌊ log 2 i ⌋ + 1 \left\lfloor\log _{2} i\right\rfloor+1 ⌊log2i⌋+1 。
这是一个完全二叉树,度为4,结点总数为10。
对于第一条来说是很显然的,i=1 时就是根结点。i>1 时,比如结点 7,它的双亲
就是
⌊
7
/
2
⌋
=
3
\lfloor 7 / 2\rfloor=3
⌊7/2⌋=3, 结点 9, 它的双亲就是
⌊
9
/
2
⌋
=
4
\lfloor 9 / 2\rfloor=4
⌊9/2⌋=4 。
第二条,比如结点 6,因为
2
×
6
=
12
2 \times 6=12
2×6=12 超过了结点总数 10 , 所以结点 6 无左孩子,
它是叶子结点。同样,而结点 5, 因为
2
×
5
=
10
2 \times 5=10
2×5=10 正好是结点总数 10,所以它的左孩子
是结点 10。
第三条,比如结点 5,因为
2
×
5
+
1
=
11
2 \times 5+1=11
2×5+1=11 ,大于结点总数 10,所以它无右孩子。而
结点 3, 因为
2
×
3
+
1
=
7
2 \times 3+1=7
2×3+1=7 小于 10, 所以它的右孩子是结点 7 。
第四条,结点8所在层次(深度)为 ⌊ log 2 8 ⌋ + 1 \left\lfloor\log _{2} 8\right\rfloor+1 ⌊log28⌋+1=4。
- 具有 n n n 个 ( n > 0 ) (n>0) (n>0) 结点的完全二叉树的高度为 ⌈ log 2 ( n + 1 ) ⌉ \left\lceil\log _{2}(n+1)\right\rceil ⌈log2(n+1)⌉ 或 ⌊ log 2 n ⌋ + 1 \left\lfloor\log _{2} n\right\rfloor+1 ⌊log2n⌋+1 。
设高度为
h
h
h, 根据性质 3 和完全二叉树的定义有
2
h
−
1
−
1
<
n
≤
2
h
−
1
2^{h-1}-1<n \leq 2^{h}-1 \quad
2h−1−1<n≤2h−1 或
2
h
−
1
≤
n
<
2
h
\quad 2^{h-1} \leq n<2^{h}
2h−1≤n<2h
得
2
h
−
1
<
n
+
1
≤
2
h
2^{h-1}<n+1 \leq 2^{h}
2h−1<n+1≤2h, 即
h
−
1
<
log
2
(
n
+
1
)
≤
h
h-1<\log _{2}(n+1) \leq h
h−1<log2(n+1)≤h, 因为
h
h
h 为正整数,所以
h
=
⌈
log
2
(
n
+
1
)
⌉
h=\left\lceil\log _{2}(n+1)\right\rceil
h=⌈log2(n+1)⌉.
或得
h
−
1
≤
log
2
n
<
h
h-1 \leq \log _{2} n<h
h−1≤log2n<h, 所以
h
=
⌊
log
2
n
⌋
+
1
h=\left\lfloor\log _{2} n\right\rfloor+1
h=⌊log2n⌋+1 。
5.2.2 二叉树的存储结构
- 顺序存储结构
二叉树的顺序存储是指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为 i i i 的结点元素存储在一维数组下标为 i − 1 i-1 i−1 的分量中。
将这棵二叉树存入到数组中,相应的下表对应其同样的位置
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映结点之间的逻辑关系,这样既能最大可能地节省存储空间,又能利用数组元索的下标值确定结点在二叉树中的位置,以及结点之间的关系。
但对于一般的二叉树,为了让数组下标能反映二叉树中结点之间的逻辑关系,只能添加一些并不存在的空结点,让其每个结点与完全二叉树上的结点相对照,再存储到一维数组的相应分量中。
然而,在最坏情况下,一个高度为 h h h 且只有 h h h 个结点的单支树却需要占据近 2 h − 1 2^{h}-1 2h−1 个存储单 元。二叉树的顺序存储结构如图所示,其中 0 表示并不存在的空结点。
所以顺序存储结构一般只用于完全二叉树。
这种存储结构建议从数组下标1开始存储树中的结点,若从数组下标0开始存储,则不满足性质4的描述,(比如结点A存储在0下标位置,无法根据性质4来计算出其孩子结点在数组中的位置)
- 链式存储结构(二叉链表)
二叉树每个结点最多有两个孩子,所以设计一个数据域和两个指针域。这样的链表称为二叉链表。
二叉树链式存储结点结构:
data为数据域,lchild为左指针域,rchild为右指针域。分别存放指向左孩子和右孩子的指针。
二叉链表链式存储结构:
typedef struct biTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
在实际不同的应用中,还可以增加某些指针域,如增加指向父结点的指针后,变为三叉链表的存储结构。
重要结论:
含有n个结点的二叉链表中,含有n+1个空链域。
5.3 二叉树的遍历和线索二叉树
5.3.1 二叉树的遍历
二叉树的遍历
是指按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。
由于二叉树是一种非线性结构,每个结点都有可能有两棵子树,因而需要寻找一种规律,以便使二叉树上的结点能排列在一个线性队列上,进而便于遍历。
根据二叉树递归定义可知,遍历一棵二叉树便要决定对根结点N、左子树L和右子树R的访问顺序。按照先遍历左子树再遍历右子树的原则,常见的遍历次序有先序(NLR)
、中序(LNR)
、和后序(LRN)
三种遍历算法,其中‘序’指的是根结点在何时被访问。
- 先序遍历
先序遍历(PreOrder)操作过程如下:
二叉树若为空,则空操作返回,否则先访问根结点,然后先序遍历左子树,再先序遍历右子树。如下图,遍历顺序为ABDGHCEIF。
对应的递归算法如下:
void PreOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根结点
PreOrder(T->lchild); //遍历左子树
PreOrder(T->rchild); //遍历右子树
}
}
- 中序遍历
中序遍历(InOrder)过程如下:
若树为空,空操作返回,否则从根结点开始(不是先访问根结点),中序遍历左子树,然后访问根结点,最后中序遍历右子树。如下图,遍历顺序为:GDHBAEICF。
对应递归算法如下:
void Inorder(BiTree T){
if(T!=NULL){
Inorder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
- 后序遍历
后序遍历(PostOrder)操作过程如下:
若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点,如下图,遍历顺序为:GHDBIEFCA。
对应递归算法如下:
void PostOrder(BiTree T){
if(T!=NULL){
Postorder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
二叉树遍历性质:
- 已知先序遍历和中序遍历序列,可以唯一确定一棵二叉树
- 已知后序遍历和中序遍历序列,可以唯一确定一棵二叉树
- 已知先序遍历和后序遍历,是不能确定一棵二叉树的
- 递归算法和非递归算法的转换
先序遍历非递归算法:
void PreOrder2(BiTree T){
InitStack(S);
BiTree p=T;
while(p||!IsEmpty(S)){
if(p){
visit(T);
Push(S,p);
p=p->lchild;
}
else{
Pop(S,p);
p=p->rchild;
}
}
}
中序遍历非递归算法:
void InOrder2(BiTree T){
InitStack(S);
BiTree p=T;
while(P||IsEmpty(S)){
if(P){
Push(S,p);
p=p->lchild;
}
else{
Pop(S,p);
visit(T);
p=p->rchild;
}
}
}
后序遍历非递归算法:
void PostOrder(BiTree T){
InitStack(S);
p=T;
r=NULL;
while(p||!ISEmpty(S)){
if(p){
Push(p);
p=p->lchild;
}
else{
GetTop(S,p);
if(p->rchild&&p->rchild!=r){
p=p->rchild;
Push(S,p);
p=p->lchild;
}
else{
pop(S,p);
visit(p->data);
r=p;
p=NULL;
}
}
}
}
- 层序遍历
进行层序遍历,要借助一个队列,将二叉树根结点入队,然后出队,访问出队结点,如果有左子树,将左子树根结点入队,若有右子树,将右子树根结点入队,然后出队,如此反复,直至队列为空。
二叉树层序遍历算法:
void LevelOrder(BiTree T){ InitQueue(Q); BiTree p; EnQueue(Q,T); while(!IsEmpty(Q)){ DeQueue(Q,p); visit(p); if(p->lchild!=NULL) EnQueue(Q,p->lchild); if(p->rchild!=NULL) EnQueue(Q,p->rchild); } }}
- 由遍历序列构造二叉树
由二叉树的先序序列和中序序列可以唯一地确定一棵二叉树。 在先序遍历序列中,第一个结点一定是二叉树的根结点; 而在中序遍历中,根结点必然将中序序列分割成两个子序列,前一个子序列是根结点的左子树的中序序列,后一个子序列是根结点 的右子树的中序序列。根据这两个子序列,在先序序列中找到对应的左子序列和右子序列。在先序序列中, 左子序列的第一个结点是左子树的根结点, 右子序列的第一个结点是右子树的根结点。 如此递归地进行下去,便能唯一地确定这棵二叉树。
同理,由二叉树的后序序列和中序序列也可以唯一地确定一棵二叉树。 因为后序序列的最后一个结点就如同先序序列的第一个结点,可以将中序序列分割成两个子序列,然后采用类似的方法递归地进行划分,进而得到一棵二叉树。
//先序输入二叉树中结点值(一个字符)//#表示空树,构造二叉链表表示二叉树Tvoid CreatOrder(BiTree *T){ TElemType ch; scanf("%c",ch); if(ch=='#') *T=NULL; else{ *T=(BiTree)malloc(sizeof(BiTNode)); if(!*T) exit(OVERFLOW); (*T)->data=ch; //生成根结点 CreateBiTree(&(*T)->lchild); CreateBiTree(&(*T)->rchild); }}
5.3.2 线索二叉树
- 线索二叉树的基本概念
遍历二叉树是以一定的规则将二叉树中的结点排列成一个线性序列,从而得到集中遍历序列,使得该序列中的每个结点(第一个和最后一个结点除外)都有一个直接前驱和直接后继。
对于一个有n个结点的二叉链表,每个结点有指向左右两个孩子的指针域,所以一共是2n个指针域。而n个结点的二叉树一共有n-1个分支线数,也就是说,存在2n-(n-1)=n+1个空指针域。所以利用空指针域存放指向其前驱或后继的指针,就可以像遍历单链表一样遍历二叉树。
这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。
如图,把二叉树进行中序遍历后,将所有的空指针域的rchild,改为指向它的后继结点。
再将这棵二叉树的空指针域的lchild,改为指向当前结点的前驱。
可以看出,线索二叉树就是把一棵二叉树转变成了一个双向链表,对二叉树以某种次序遍历使其变为线索二叉树的过程称为线索化。
为了区分某一结点的lchild指向它的左孩子还是指向前驱,rchild指向右孩子还是指向后继,需要添加两个标志域ltag,rtag,ltag和rtag只是存放0或1的布尔型变量。结点结构如下:
规定:
若无左子树,令lchild指向其前驱结点;若无右子树,令rchild指向其后继结点,还需要增加两个标志域表示指针域是指向左(右)孩子还是指向前驱(后继)。
其中:
- ltag为0时指向该结点的左孩子,为1时指向该结点的前驱
- rtag为0时指向该结点的右孩子,为1时指向该结点的后继
上图可修改为如下:
线索二叉树存储结构描述如下:
typedef struct ThreadNode{ ElemType data; struct ThreadNode *lchild,*rchild; int ltag,rtag;}ThreadNode,*ThreadTree;
线索化
的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历该二叉树的时候得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。
以中序线索二叉树的建立为例。附设指针 pre 指向刚刚访问过的结点,指针 p \mathrm{p} p 指向正在访问的结点,即 pre 指向 p \mathrm{p} p 的前驱。
在中序遍历的过程中,检查 p \mathrm{p} p 的左指针是否为空,若为空就将它指向 pre; 检查 pre 的右指针是否为空,若为空就将它指向 p。
中序遍历对二叉树线索化的递归算法如下:
void InThread(ThreadTree &p,ThreadTree &pre){ if(p!=NULL){ InThread(p->lchild,pre); //递归,线索化左子树 if(p->lchild==NULL){ //左子树为空,建立前驱线索 p->lchild=pre; p->ltag=1; } if(pre!=NULL&&pre->rchild==NULL){ pre->rchild=p; //建立前驱结点的后继线索 pre->rtag=1; } pre=p; //标记当前结点成为刚刚访问过的结点 InThread(p->rchild,pre); //递归,线索化右子树 }}
中序遍历建立中序线索二叉树的主过程算法:
void CreateInThread(ThreadTree T){ ThreadTree pre=NULL; if(T!=NULL){ InThread(T,pre); pre->rchild=NULL; pre->rtag=1; }}
为了方便, 可以在二叉树的线索链表上也添加一个头结点, 令其 lchild域的指针指向二叉树的根结点,其 rchild 域的指针指向中序遍历时访问的最后一个结点;
令二叉树中序序列中的第一个结点的 lchild域指针和最后一个结点的 rchild域指针均指向头结点。这好比为二叉树建立了一个双向线索链表,方便从前往后或从后往前对线索二叉树进行遍历。
- 中序线索二叉树的遍历
中序线索二叉树的结点中隐含了线索二叉树的前驱和后继信息。在对其进行遍历时,只要先找到序列中的第一个结点,然后依次找结点的后继,直至其后继为空。在中序线索二叉树中找结点后继的规律是:若其右标志为“ 1 ",则右链为线索,指示其后继,否则遍历右子树中第一个访问的结点(右子树中最左下的结点)为其后继。不含头结点的线索二叉树的遍历算法如下:
- 求中序线索二叉树中中序序列下的第一个结点:
ThreadNode *Firstnode(*ThreadNode *p){ while (p->ltag==0) p=p->lchild; return p;}
- 求中序线索二叉树中结点 p \mathrm{p} p 在中序序列下的后继:
ThreadNode *Nextnode(*ThreadNode *p){ if (p->rtag==0) return FirstNode(p->rchild); else return p->rchild;}
- 不含头结点的中序线索二叉树的中序遍历算法:
void InOrder(ThreadNode *T){ for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p)) visit(p);}
- 先序线索二叉树和后序线索二叉树
求先序线索二叉树的过程:先序序列为 A B C D F A B C D F ABCDF ,然后依次判断每个结点的左右链域,如果为空则将其改造为线索。
结点 A , B A, B A,B 均有左右孩子; 结点 C C C 无 左孩子,将左链域指向前驱 B B B ,无右孩子,将右链域指向后继 D ; D ; D;
结点 D D D 无左孩子,将左链域指向前驱 C C C ,无右孩子,将右链域指向后继 F ; F ; F;
结点 F F F 无左孩子,将左链域指向前驱 D D D, 无右孩子,也无后继故置空,得到的先序线索二叉树如图所示。
求后序线索二叉树的过程: 后序序列 为 C D B F A C D B F A CDBFA ,
结点 C C C 无左孩子,也无前驱故置空,无右孩子,将右链域指向后继 D ; D ; D;
结点 D D D 无左孩子,将左链域指向前驱 C C C ,无右孩子,将右链域指向后继 B ; B ; B;
结点 F F F 无左孩子,将左链域指向前驱 B B B ,无右孩子,将右链域指向后继 A A A, 得到的后序线索二叉树如图所示。
如何在先序线索二叉树中找结点的后继?
如果有左孩子,则左孩子就是其后继; 如果无左孩子但有右孩子,则右孩子就是其后继; 如果为叶结点,则右链域直接指示了结点的后继。
在后序线索二叉树中找结点的后继较为复杂,可分 3 种情况:
(1)若结点 x x x 是二叉树的根,则其后继为空;
(2)若结点 x x x 是其双亲的右孩子,或是其双亲的左孩子且其双亲没有右子树,则其后继即为双亲;
(3)若结点 x x x 是其双亲的左孩子,且其双亲有右子树,则其后继为双亲的右子树上按后序遍历列出的第一个结点。图©中找结点 B B B 的后继无法通过链域找到,可见在后序线索二叉树上找后继时需知道结点双亲,即需采用带标志域的三叉链表作为存储结构。
所用二叉树需要经常遍历或者查找结点时需要某种遍历序列的前驱和后继,采用线索二叉链表的存储结构比较合适。
5.4 树、森林
5.4.1 树的存储结构
- 双亲表示法
这种存储方式采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置。
根结点下标为0,伪指针为-1
双亲表示法存储结构:
# define MAX_TREE_SIZE 100typedef struct{ ElemType data; int parent;}PTNode;typedef struct{ PTNode nodes[MAX_TREE_SIZE]; int n;}PTree;
该存储结构利用了每个结点(根结点除外)只有唯一双亲的性质,可以很快得到每个结点的双亲结点,但求结点的孩子时需要遍历整个结构。
注意: 区别树的顺序存储结构与二叉树的顺序存储结构。
在树的顺序存储结构中,数组下标代表结点的编号,下标中所存的内容指示了结点之间的关系。
而在二叉树的顺序存储结构中,数组下标既代表了结点的编号,又指示了二叉树中各结点之间的关系。
当然,二叉树属于树,因此二叉树都可以用树的存储结构来存储,但树却不都能用二叉树的存储结构来存储。
- 孩子表示法
孩子表示法是将每个结点的孩子结点都用单链表链接起来形成一个线性结构,此时n个结点就有n个孩子链表(叶子结点的孩子链表为空表)
这种存储方式寻找子女的操作很直接,而寻找双亲的操作要遍历n个结点中孩子链表指针域指向的n个孩子链表。
- 孩子兄弟表示法
孩子兄弟表示法又称二叉树表示法,即以二叉链表作为树的存储结构。
孩子兄弟表示法使每个结点包括三部分内容:结点值、指向结点第一个孩子结点的指针,及指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)
孩子兄弟表示法存储结构描述:
typedef struct CSNode{ ElemType data; struct CSNode *firstchild,*nextchild;}CSNode,*CSTree;
5.4.2 树、森林、二叉树的转换
由于二叉树和树都可以用二叉链表作为存储结构,因此以二叉链表作为媒介可以导出树与二叉树的一个对应关系,即给定一棵树,可以找到唯一的一棵二叉树与之对应。从物理结构上看, 它们的二叉链表是相同的,只是解释不同而已。
树转换为二叉树的规则:
每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟,这个规则又称 “左孩子右兄弟”。由于根结点没有兄弟,所以对应的二叉树没有右子树。
树转换成二叉树的画法:
-
加线。在所有兄弟结点之间加一条连线。
-
去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
-
层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。
注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
比如F、G是树B的右孩子,转换之后F是二叉树结点E的右孩子,G是二叉树结点E的右孩子。
森林转化为二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟, 可以按照兄弟的处理办法来操作。步骤如下:
- 把每个树转换为二叉树。
- 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后 就得到了由森林转换来的二叉树。
二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程,也就是反过来做而已。如图 6-11-4
所示。步骤如下:
- 加线。若某结点的左孩子结点存在,将左孩子的 n 个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
- 去线。删除原二叉树中所有结点与其右孩子结点的连线。
- 层次调整。使之结构层次分明。
二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,标准很简单,那就是只要看这二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。那么如果是转换成森林,步骤如下:
- 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在, 则连线删除 ⋯ . . \cdots . . ⋯.., 直到所有右孩子连线都删除为止,得到分离的二叉树。
- 再将每棵分离后的二叉树转换为树即可。
5.4.3 树和森林的遍历
树的遍历是指用某种方式访问树中的每个结点,且仅访问一次。主要有两种方式:
1)先根遍历。
若树非空,先访问根结点,再依次遍历根结点的每棵子树,遍历子树时仍遵循先根后子树的规则。
其遍历序列与这棵树相应二叉树的先序序列相同。
2)后根遍历。
若树非空,先依次遍历根结点的每棵子树,再访问根结点,遍历子树时仍遵循先子树后根的规则。
其遍历序列与这棵树相应二叉树的中序序列相同。
下图树的先根遍历序列为 A B E F C D G A B E F C D G ABEFCDG, 后根遍历序列为 E F B C G D A E F B C G D A EFBCGDA 。
另外,树也有层次遍历,与二叉树的层次遍历思想基本相同,即按层序依次访问各结点。
按照森林和树相互递归的定义,可得到森林的两种遍历方法。
1)先序遍历森林。若森林为非空,则按如下规则进行遍历:
- 访问森林中第一棵树的根结点。
- 先序遍历第一棵树中根结点的子树森林。
- 先序遍历除去第一棵树之后剩余的树构成的森林。
- 中序遍历森林。森林为非空时,按如下规则进行遍历:
- 中序遍历森林中第一棵树的根结点的子树森林。
- 访问第一棵树的根结点。
- 中序遍历除去第一棵树之后剩余的树构成的森林。
该森林先序遍历为ABCDEFGHI,中序遍历为BCDAFEHIG
森林的先序遍历和二叉树的先序遍历结果相同。中序遍历也可以说是后根遍历森林的子树。
当森林转换成二叉树时,其第一棵树的子树森林转换成左子树,剩余树的森林转换成右子树,可知森林的先序和中序遍历即为其对应二叉树的先序和中序遍历。
5.4.4 树的应用----并查集
并查集是一种简单的集合表示,它支持以下 3 种操作:
-
Union (S, Root1, Root2) : 把集合 S \mathrm{S} S 中的子集合 Root2 并入子集合 Root1。要求 Root 1和 Root2 互不相交,否则不执行合并。
-
Find ( S , x \mathrm{S}, \mathrm{x} S,x ) : 查找集合 S \mathrm{S} S 中单元素 x 所在的子集合,并返回该子集合的名字。
-
Initial (S) : 将集合 S \mathrm{S} S 中的每个元素都初始化为只有一个单元素的子集合。
通常用树(森林)的双亲表示作为并查集的存储结构,每个子集合以一棵树表示。
所有表示子集合的树,构成表示全集合的森林,存放在双亲表示数组内。通常用数组元素的下标代表元素名,用根结点的下标代表子集合名,根结点的双亲结点为负数。
例如, 若设有一个全集合为 S = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , S=\{0,1,2,3,4,5,6, S={0,1,2,3,4,5,6,, 7 , 8 , 9 } 7,8,9\} 7,8,9} ,初始化时每个元素自成一个单元素子集合,每个子集合的数组值为-1,如图所示。
经过一段时间的计算,这些子集合合并为 3 个更大的子集合 S 1 = { 0 , 6 , 7 , 8 } , S 2 = { 1 , 4 , 9 } , S 3 = { 2 , 3 , 5 } S_{1}=\{0,6,7,8\}, S_{2}=\{1,4,9\}, S_{3}=\{2,3,5\} S1={0,6,7,8},S2={1,4,9},S3={2,3,5},此时并查集的树形表示和存储结构如图所示。
为了得到两个子集合的并,只需将其中一个子集合根结点的双亲指针指向另一个集合的根结点。因此, S 1 ∪ S 2 S_{1} \cup S_{2} S1∪S2 可以具有如图所示的表示。
在采用树的双亲指针数组表示作为并查集的存储表示时,集合元素的编号从 0 到 size-1。
其中 size 是最大元素的个数。下面是并查集主要运算的实现。
并查集结构定义
# define SIZE 100int UFSets[SIZE]; //集合元素数组(双亲指针数组)
并查集初始化操作
void Initial(int S[]){ for(int i=0;i<size;i++) //每个自成单元集合 S[i]=-1;}
Find操作(函数在并查集S中查找并返回包含元素x的树的根)
int FInd(intS[],int x){ while(S[x]>0) //循环寻找x的根 x=S[x]; return x; //根的S[]小于0}
Union操作(函数求两个不相交子集合的并集)
void Union(int S[],int Root1,int Root2){ //要求Root1合Root2是不同的,且表示子集合的名字 S[Root2]=Root1; //将Root2连接到另一个Root1下面}
5.5 树与二叉树的应用
5.5.1 二叉排序树(BST)
- 二叉排序树定义
二叉排序树
又称二叉查找树,或者是一棵空树,或者是具有以下特性的二叉树:
1)若左子树非空,则左子树上所有结点的值均小于根结点的值。
2)若右子树非空,则右子树上所有结点的值均大于根结点的值。
3)左、右子树也分别是一棵二叉排序树。
根据二叉排序树的定义,左子树结点值 < 根结点值 < 右子树结点值,所以对二叉排序树进行中序遍历,可以得到一个递增的有序序列。
- 二叉排序树的查找
二叉排序树查找是从根结点开始,沿某个分支逐层向下比较的过程。若二叉排序树非空,先将给定值与根结点的关键字比较,若相等,则查找成功;若不等,如果小于根结点的关键字,则在根结点的左子树上查找,否则在根结点的右子树上查找,显然是一个递归的过程。
二叉排序树的非递归查找算法:
BSTNode *BST_Search(BiTree T,Elemtype key){ while(T!=NULL&&key!=T->data){ if(key<T->data) T=T->lchild; else T=T->rchild; } return T;}
二叉排序树查找也可以用递归算法实现,但是执行效率较低。
- 二叉排序树的插入
二叉排序树作为一种动态树,其特点是树的结构通常不是一次生成的,而是查找过程中,当树中不存在关键字值等于给定值的结点时再进行插入的,
插入结点过程如下:
若原二叉排序树为空,则直接插入结点;否则,若关键字k小于根结点值,则插入到左子树,若关键字k大于根结点值,则插入到右子树。插入的结点一定是一个新添加的叶结点,且是查找失败时的查找路径上访问的最后一个结点的左孩子或右孩子。
如下:在一个二叉排序树中插入结点28和58,虚线为查找路径。
二叉排序树插入算法如下:
int BST_Insert(BiTree &T,KeyType k){ if(T==NULL){ T=(BiTree)malloc(sizeof(BSTNode)); T->key=k; T->lchild=T->rchild=NULL; return 1;}else if(k==T->key) //树中存在相同关键字结点,插入失败 return 0;else if(k<T->key) return BST_Imsert(T->lchild,k);else return BST_Insert(T->rchild,k);}
- 二叉排序树的构造
从一棵空树出发,依次输入元素,将它们插入二叉排序树中的合适位置。
设查找的关键字序列为{45,24,53,45,12,24},生成的二叉排序树如图所示:
构造二叉排序树的算法如下:
void Create_BST(BiTree &T,KeyType str[],int n){ T=NULL; int i=0; while(i<n){ BST_Insert(T,str[i]); i++; }}
- 二叉排序树的删除
在二叉排序树中删除一个结点时,不能把以该结点为根的子树上的结点都删除,必须先把被删除结点从存储二叉排序树的链表上摘下,将因删除结点而断开的二叉链表重新链接起来,同时确保二叉排序树的性质不会丢失。
删除操作的实现过程按 3 种情况来处理:
(1) 若被删除结点 z z z 是叶结点,则直接删除,不会破坏二叉排序树的性质。
(2) 若结点 z z z 只有一棵左子树或右子树,则让 z z z 的子树成为 z z z 父结点的子树,替代 z z z 的位置。
(3) 若结点 z z z 有左、右两棵子树,则令 z z z 的直接后继(或直接前驱)替代 z z z ,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
- 二叉排序树的查找效率分析
二叉排序树的查找效率,主要取决于树的高度。若二叉排序树的左、右子树的高度之差的绝对值不超过 1, 则这样的二叉排序树称为平衡二叉树,它的平均查找长度为 O ( log 2 n ) O\left(\log _{2} n\right) O(log2n) 。
若二叉排序树是一个只有右(左)孩子的单支树(类似于有序的单链表 ),则其平均查找长度为
O
(
n
)
O(n)
O(n)。在最坏情况下,即构造二叉排序树的输入序列是有序的,则会形成一个倾斜的单支树,此时二叉排序树的性能显著变坏,树的高度也增加为元素个数
n
n
n ,如图 (b)所示。
在等概率情况下,图 (a)查找成功的平均查找长度为
ASL
a
=
(
1
+
2
×
2
+
3
×
4
+
4
×
3
)
/
10
=
2.9
\operatorname{ASL}_{\mathrm{a}}=(1+2 \times 2+3 \times 4+4 \times 3) / 10=2.9
ASLa=(1+2×2+3×4+4×3)/10=2.9
而图(b)查找成功的平均查找长度为
A
S
L
b
=
(
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
)
/
10
=
5.5
\mathrm{ASL}_{\mathrm{b}}=(1+2+3+4+5+6+7+8+9+10) / 10=5.5
ASLb=(1+2+3+4+5+6+7+8+9+10)/10=5.5
从查找过程看,二叉排序树与二分查找相似。就平均时间性能而言,二叉排序树上的查找和二分查找差不多。但二分查找的判定树唯一,而二叉排序树的查找不唯一,相同的关键字其插入顺序不同可能生成不同的二叉排序树。
就维护表的有序性而言, 二叉排序树无须移动结点, 只需修改指针即可完成插入和删除操作, 平均执行时间为 O ( log 2 n ) O\left(\log _{2} n\right) O(log2n) 。
二分查找的对象是有序顺序表,若有插入和删除结点的操作,所花的代价是 O ( n ) O(n) O(n) 。
当有序表是静态查找表时,宜用顺序表作为其存储结构,而采用二分查找实现其查找操作; 若有序表是动态查找表,则应选择二叉排序树作为其逻辑结构。
5.5.2 平衡二叉树
- 平衡二叉树的定义
为避免树的高度增长过快,降低二叉树排序的性能,规定在插入和删除二叉树结点时,要保证任意结点的左、右子树高度差的绝对值不超过1,这样的二叉树成为平衡二叉树(Balanced Binary Tree)
,简称平衡树。
定义左子树与右子树的高度差为该结点的平衡因子
,平衡二叉树的平衡因子的值之可能是-1、0、1。
因此,平衡二叉树可以定义为或者是一棵空树,或者是具有下列性质的二叉树:
它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度差的绝对值不超过1。
其中结点中的值为该结点的平衡因子。
- 平衡二叉树的插入
二叉排序树保证平衡的基本思想:
每当在二叉排序树中插入(或删除)一个结点时,首先检查其插入路径上的结点是否因为此次操作而导致了不平衡,若导致了不平衡,则先找到插入路径上离插入结点最近的平衡因子的绝对值大于1的结点A,再对以A为根的子树,在保持二叉排序树特性的前提下,调整各结点的位置关系,使之重新达到平衡。
注:每次调整的对象都是最小不平衡子树,即以插入路径上离插入结点最近的平衡因子的绝对值大于1的结点作为根的子树。
平衡二叉树的插入过程的前半部分与二叉排序树相同,但在新结点插入后,若造成查找路径上的某个结点不再平衡,则需要做出相应的调整。
可将调整的规律归纳为下列 4 种情况:
1)LL 平衡旋转(右单旋转)
由于在结点 A A A 的左孩子(L)的左子树(L)上插入了新结点, A A A 的平衡因子由 1 增至 2 , 导致以 A A A 为根的子树失去平衡,需要一次向右的旋转操作。将 A A A 的左孩子 B B B 向右上旋转代替 A A A 成为根结点,将 A A A 结点向右下旋转成为 B B B 的右子树的根结点,而 B B B 的原右子树则作为 A A A 结点的左子树。
结点旁的数值代表结点的平衡因子,而用方块表示相应结点的子树,下方数值代表该子树的高度。
- R R \mathrm{RR} RR 平衡旋转(左单旋转)
由于在结点 A A A 的右孩子(R)的右子树(R)上插入了新结点, A A A 的平衡因子由-1 减至-2,导致以 A A A 为根的子树失去平衡,需要一次向左的旋转操作。将 A A A 的右孩子 B B B 向左上旋转代替 A A A 成为根结点,将 A A A 结点向左下旋转成为 B B B 的左子树的根结点,而 B B B 的原左子树则作为 A A A 结点的右子树。
3)LR 平衡旋转(先左后右双旋转 )
由于在 A A A 的左孩子(L)的右子树(R)上插入新结点, A A A 的平衡因子由 1 增至 2, 导致以 A A A 为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将 A A A 结点的左孩子 B B B 的右子树的根结点 C C C 向左上旋转提升到 B B B 结点的位置,然后再把该 C C C 结点向右上旋转提升到 A A A 结点的位置。
4)RL 平衡旋转(先右后左双旋转 )
由于在 A A A 的右孩子 ® 的左子树(L)上插入新结点, A A A 的平衡因子由-1 减至-2,导致以 A A A 为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将 A A A 结点的右孩子 B B B 的左子树的根结点 C C C 向右上旋转提升到 B B B 结点 的位置,然后再把该 C C C 结点向左上旋转提升到 A A A 结点的位置。
假设关键字序列为 { 15 , 3 , 7 , 10 , 9 , 8 } \{15,3,7,10,9,8\} {15,3,7,10,9,8} ,通过该序列生成平衡二叉树的过程如图所示。
图 (d)插入7后导致不平衡,最小不平衡子树的根为15 ,插入位置为其左孩子的右子树,故执行 L R \mathrm{LR} LR 旋转,先左后右双旋转,调整后的结果如图(e)所示。图(g)插入 9 后导致不平衡,最小不平衡子树的根为 15 ,插入位置为其左孩子的左子树,故执行 LL 旋转,右单旋转,调整后的结果如图(h)所示。图(i)插入 8 后导致不平衡,最小不平衡子树的根为7,插入位置为其右孩子的左子树,故执行 R L \mathrm{RL} RL 旋转,先右后左双旋转,调整后的结果如图(j)所示。
- 平衡二叉树的查找
在平衡二叉树上进行查找的过程与二叉排序树的相同。因此,在查找过程中,与给定值进行比较的关键字个数不超过树的深度。
假设以 n h n_{h} nh 表示深度为 h h h 的平衡树中含有的最少结点数。显然,有 n 0 = 0 , n 1 = 1 , n 2 = 2 n_{0}=0, n_{1}=1, n_{2}=2 n0=0,n1=1,n2=2, 并且有 n h = n h − 1 + n h − 2 + 1 n_{h}=n_{h-1}+n_{h-2}+1 nh=nh−1+nh−2+1 。可以证明,含有 n n n 个结点的平衡二叉树的最大深度为 O ( log 2 n ) O\left(\log _{2} n\right) O(log2n), 因此平衡二叉树的平均查找长度为 O ( log 2 n ) O\left(\log _{2} n\right) O(log2n) 。
该结论可用与求解给定结点数的平衡二叉树的查找所需要的最多比较次数(或树的最大高度)。
5.5.3 哈夫曼树和哈夫曼编码
- 哈夫曼树的定义
在许多应用中,树中结点常常被赋予一个表示某种意义的数值,称为该结点的权
。
从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度
。
树中所有叶结点的带权路径长度之和称为该树的带权路径长度
,记为
W
P
L
=
∑
i
=
1
n
w
i
l
i
\mathrm{WPL}=\sum_{i=1}^{n} w_{i} l_{i}
WPL=i=1∑nwili
式中,
w
i
w_{i}
wi 是第
i
i
i 个叶结点所带的权值,l
i
_{i}
i 是该叶结点到根结点的路径长度。
在含有
n
n
n 个带权叶结点的二叉树中, 其中带权路径长度 (WPL) 最小的二叉树称为哈夫曼树
, 也称最优二叉树
。例如,图中的 3 棵二叉树都有 4 个叶子结点
a
,
b
,
c
,
d
a, b, c, d
a,b,c,d, 分别带权
7
,
5
,
2
,
4
7,5,2,4
7,5,2,4,
它们的带权路径长度分别为
(a) W P L = 7 × 2 + 5 × 2 + 2 × 2 + 4 × 2 = 36 \mathrm{WPL}=7 \times 2+5 \times 2+2 \times 2+4 \times 2=36 WPL=7×2+5×2+2×2+4×2=36 。
(b) W P L = 4 × 2 + 7 × 3 + 5 × 3 + 2 × 1 = 46 \mathrm{WPL}=4 \times 2+7 \times 3+5 \times 3+2 \times 1=46 WPL=4×2+7×3+5×3+2×1=46 。
© WPL = 7 × 1 + 5 × 2 + 2 × 3 + 4 × 3 = 35 =7 \times 1+5 \times 2+2 \times 3+4 \times 3=35 =7×1+5×2+2×3+4×3=35 。
©的WPL最小,它恰好为哈夫曼树。
- 哈夫曼树的构造
给定 n n n 个权值分别为 w 1 , w 2 , ⋯ , w n w_{1}, w_{2}, \cdots, w_{n} w1,w2,⋯,wn 的结点,构造哈夫曼树的算法描述如下:
1) 将这 n n n 个结点分别作为 n n n 棵仅含一个结点的二叉树,构成森林 F 。 F_{\text {。 }} F。 。
2)构造一个新结点,从 F F F 中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
3)从 F F F 中删除刚才选出的两棵树,同时将新得到的树加入 F F F 中。
4)重复步骤 2)和 3),直至 F F F 中只剩下一棵树为止。
从上述构造过程中可以看出哈夫曼树具有如下特点:
1)每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
2)构造过程中共新建了 n − 1 n-1 n−1 个结点(双分支结点),因此哈夫曼树的结点总数为 2 n − 1 2 n-1 2n−1 。
3)每次构造都选择 2 棵树作为新结点的孩子,因此哈夫曼树中不存在度为 1 的结点。
例如,权值
{
7
,
5
,
2
,
4
}
\{7,5,2,4\}
{7,5,2,4} 的哈夫曼树的构造过程如图所示。
- 哈夫曼编码
在数据通信中, 若对每个字符用相等长度的二进制位表示, 称这种编码方式为固定长度编码。
若允许对不同字符用不等长的二进制位表示,则这种编码方式称为可变长度编码。
可变长度编码比固定长度编码要好得多,其特点是对频率高的字符赋以短编码,而对频率较低的字符则赋以较长一些的编码,从而可以使字符的平均编码长度减短,起到压缩数据的效果。
哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码。
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。
举例:设计字符 A , B \mathrm{A}, \mathrm{B} A,B 和 C 对应的编码 0,101 和 100 是前缀编码。对前缀编码的解码很简单,因为没有一个编码是其他编码的前缀。所以识别出第一个编码,将它翻译为原码,再对余下的编码文件重复同样的解码操作。例如,码串 00101100 可被唯一地翻译为 0 , 0 , 101 0,0,101 0,0,101 和 100 。
另举反例:如果再将字符 D \mathrm{D} D 的编设计为 00,此时 0 是 00 的前缀,那么这样的码串的前两位就无法唯一翻译。
由哈夫曼树得到哈夫曼编码是很自然的过程。
首先, 将每个出现的字符当作一个独立的结点, 其权值为它出现的频度(或次数 ),构造出对应的哈夫曼树。显然,所有字符结点都出现在叶结点中。我们可将字符的编码解释为从根至该字符的路径上边标记的序列, 其中边标记为 0 表示“转 向左孩子”, 标记为 1 表示“转向右孩子”。图为一个由哈夫曼树构造哈夫曼编码的示例,矩形方块表示字符及其出现的次数。
这棵哈夫曼树的 WPL 为
W
P
L
=
1
×
45
+
3
×
(
13
+
12
+
16
)
+
4
×
(
5
+
9
)
=
224
\mathrm{WPL}=1 \times 45+3 \times(13+12+16)+4 \times(5+9)=224
WPL=1×45+3×(13+12+16)+4×(5+9)=224
此处的 WPL 可视为最终编码得到二进制编码的长度,共 224 位。若采用 3 位固定长度编码,则得到的二进制编码长度为 300 位,因此哈夫曼编码共压缩了 25%的数据。利用哈夫曼树可以设计出总长度最短的二进制前缀编码。
注意: 0 和 1 究竟是表示左子树还是右子树没有明确规定。左、右孩子结点的顺序是任意的,所以构造出的哈夫曼树并不唯一,但各哈夫曼树的带权路径长度 WPL 相同且为最优。此外,如有若干权值相同的结点,则构造出的哈夫曼树更可能不同,但 WPL 心然相同且是最优的。
归纳总结
遍历是二叉树各种操作的基础,主要为遍历过程中对结点的操作,比如结合递归算法和利用栈或队列的非递归算法。
二叉树遍历算法的递归程序
void Track(BiTree *p){ if(p!=NULL){ //(1) Track(p->lchild); //(2) Track(p->rchild); //(3) }}
访问visit()位于(1)、(2)、(3)的位置,分别对应先序、中序、后序遍历。