【程序人生】数据结构杂记(四)
说在前面
个人读书笔记
二叉树
树属于半线性结构——附加某种约束(比如遍历),也可以在树中的元素之间确定某种线性次序
树是一种分层结构
树 T T T中所有节点深度的最大值称作该树的高度 ( h e i g h t ) (height) (height),记作 h e i g h t ( T ) height(T) height(T)。
不难理解,树的高度总是由其中某一叶节点的深度确定的。特别地,仅含单个节点的树高度为
0
0
0,空树高度为
−
1
-1
−1。
推而广之,任一节点
v
v
v所对应子树
s
u
b
t
r
e
e
(
v
)
subtree(v)
subtree(v)的高度,亦称作该节点的高度,记作
h
e
i
g
h
t
(
v
)
height(v)
height(v)。特别地,全树的高度亦即其根节点
r
r
r的高度,
h
e
i
g
h
t
(
T
)
=
h
e
i
g
h
t
(
r
)
height(T) = height(r)
height(T)=height(r)。
二叉树的实现
作为图的特殊形式,二叉树的基本组成单元是节点与边;作为数据结构,其基本的组成实体是二叉树节点(binary tree node),而边则对应于节点之间的相互引用。
遍历
二叉树本身并不具有天然的全局次序,故为实现遍历,需通过在各节点与其孩子之间约定某种局部次序,间接地定义某种全局次序。
按惯例左兄弟优先于右兄弟,故若将节点及其孩子分别记作V、L和R,则如下图所示,局部访问的次序可有VLR、LVR和LRV三种选择。根据节点V在其中的访问次序,三种策略也相应地分别称作先序遍历、中序遍历和后序遍历
先序遍历
为遍历(子)树
x
x
x,首先核对
x
x
x是否为空。若
x
x
x为空,则直接退出——其效果相当于递归基。
反之,若
x
x
x非空,则按照先序遍历关于局部次序的定义,优先访问其根节点
x
x
x;然后,依次深入左子树和右子树,递归地进行遍历。
迭代实现先序遍历
在二叉树
T
T
T中,从根节点出发沿着左分支一直下行的那条通路(以粗线示意),称作最左侧通路(leftmost path)。若将沿途节点分别记作
L
k
L_k
Lk,
k
=
0
,
1
,
2
,
.
.
.
,
d
k = 0, 1, 2, ..., d
k=0,1,2,...,d,则最左侧通路终止于没有左孩子末端节点
L
d
L_d
Ld。若这些节点的右孩子和右子树分别记作
R
k
R_k
Rk和
T
k
T_k
Tk,
k
=
0
,
1
,
2
,
.
.
.
,
d
k = 0, 1, 2, ..., d
k=0,1,2,...,d,则该二叉树的先序遍历序列可表示为:
也就是说,先序遍历序列可分解为两段:
沿最左侧通路自顶而下访问的各节点,以及自底而上遍历的对应右子树。
基于对先序遍历序列的这一理解,可以导出以下迭代式先序遍历算法。
在全树以及其中每一棵子树的根节点处,该算法都首先调用函数VisitAlongLeftBranch(),自顶而下访问最左侧通路沿途的各个节点。这里也使用了一个辅助栈,逆序记录最左侧通路上的节点,以便确定其对应右子树自底而上的遍历次序。
中序遍历
各节点在中序遍历序列中的局部次序,与按照有序树定义所确定的全局左、右次序完全吻合。
迭代版中序遍历
参照迭代式先序遍历的思路,再次考查二叉树
T
T
T的最左侧通路,并对其中的节点和子树标记命名。于是,
T
T
T的中序遍历序列可表示为:
在全树及其中每一棵子树的根节点处,该算法首先调用函数goAlongLeftBranch(),沿最左侧通路自顶而下抵达末端节点
L
d
L_d
Ld 。在此过程中,利用辅助栈逆序地记录和保存沿途经过的各个节点,以便确定自底而上各段遍历子序列最终在宏观上的拼接次序。
中序遍历序列直接后继及其定位
这里,共分两大类情况。
若当前节点有右孩子,则其直接后继必然存在,且属于其右子树。此时只需转入右子树,再沿该子树的最左侧通路朝左下方深入,直到抵达子树中最靠左(最小)的节点。
反之,若当前节点没有右子树,则若其直接后继存在,必为该节点的某一祖先,且是将当前节点纳入其左子树的最低祖先。于是首先沿右侧通路朝左上方上升,当不能继续前进时,再朝右上方移动一步即可。
作为后一情况的特例,出口时s可能为NULL。这意味着此前沿着右侧通路向上的回溯,抵达了树根。也就是说,当前节点全树右侧通路的终点——它也是中序遍历的终点,没有后继。
迭代版中序遍历(无需辅助栈)
可见,这里相当于将原辅助栈替换为一个标志位backtrack。每当抵达一个节点,借助该标志即可判断此前是否刚做过一次自下而上的回溯。若不是,则按照中序遍历的策略优先遍历左子树。反之,若刚发生过一次回溯,则意味着当前节点的左子树已经遍历完毕(或等效地,左子树为空),于是便可访问当前节点,然后再深入其右子树继续遍历。
每个节点被访问之后,都应转向其在遍历序列中的直接后继。按照以上的分析,通过检查右子树是否为空,即可在两种情况间做出判断:
该后继要么在当前节点的右子树(若该子树非空)中,要么(当右子树为空时)是其某一祖先。后一情况即所谓的回溯。请注意,由succ()返回的直接后继可能是NULL,此时意味着已经遍历至中序遍历意义下的末节点,于是遍历即告完成。
后序遍历
先序遍历序列与后序遍历序列并非简单的逆序关系。
层次遍历
在所谓广度优先遍历或层次遍历(level-order traversal)中,确定节点访问次序的原则可概括为“先上后下、先左后右”——先访问树根,再依次是左孩子、右孩子、左孩子的左孩子、左孩子的右孩子、右孩子的左孩子、右孩子的右孩子、…,依此类推。
当然,有根性和有序性是层次遍历序列得以明确定义的基础。正因为确定了树根,各节点方可拥有深度这一指标,并进而依此排序;有序性则保证孩子有左、右之别,并依此确定同深度节点之间的次序。
此前介绍的迭代式遍历,无论先序、中序还是后序遍历,大多使用了辅助栈,而迭代式层次遍历则需要使用与栈对称的队列结构
示例:
完全二叉树
叶节点只能出现在最底部的两层,且最底层叶节点均处于次底层叶节点的左侧
满二叉树
所有叶节点同处于最底层
结语
如果您有修改意见或问题,欢迎留言或者通过邮箱和我联系。
手打很辛苦,如果我的文章对您有帮助,转载请注明出处。