相应的练习代码:https://github.com/liuxuan320/Algorithm_Exercises
0. 写在前面
这一章本来应该叫做基本检索与周游的。但是考虑到其实讲的更多的是树,所以也就直接写树了。
1. 二叉树的三种常用检索方式
我们常见的树,80%的例子都是二叉树,因为二叉树结构简单,易于处理,如果是多叉树,可能牵扯到的就是图论里的相关知识,这个在最后讲。现在我们讲解二叉树的三种常见的检索方式:先序遍历、中序遍历、后序遍历。
所谓的先序遍历,其中的先表示的是父节点是先访问,也就是说其顺序应该是:访问父节点→访问左叶子节点→访问右叶子节点。这么说完,可能你中序遍历和后序遍历应该都会了,中序遍历:访问左叶子节点→访问父节点→访问右叶子节点,后序遍历:访问左叶子节点→访问右叶子节点→访问父节点。下面给出他们的算法:
//先序遍历
procedure PREORDER(T)
//T是一棵二元树,T中每个节点有3个信息段,LCHILD,DATA,RCHILD
if(T≠0) then call VISIT(T)
call PREORDER(LCHILD(T))
call PREORDER(RCHILD(T))
endif
end PREORDER
//中序遍历
procedure INORDER(T)
if(T≠0) then call INORDER(LCHILD(T))
call VISIT(T)
call INORDER(RCHILD(T))
endif
end INORDER
//后序遍历
procedure POSTORDER(T)
//T是一棵二元树,T中每个节点有3个信息段,LCHILD,DATA,RCHILD
if(T≠0) then call POSTORDER(LCHILD(T))
call POSTORDER(RCHILD(T))
call VISIT(T)
endif
end POSTORDER
这里给出的是三种遍历的递归算法,非递归的大家可以自己重写。
2. 树的常见几种应用
二叉树常考的不适这三种遍历,而是基于这三种遍历的应用算法。我们这里讲3种,分别是镜像树、D-search检索、还有红黑树。
1. 镜像树
镜像树,顾名思义,就是和原来的树是镜像,原来的左孩子是现在的右孩子,原来的右孩子是现在的左孩子。我一同学面试时,也遇到过这题,因此我在这提到它。
它的简单原理就是利用递归在每个结点先进行左右互换,然后再不断递归,直到叶子节点。具体代码如下:
procedure swapTree(T)
if(T!=NULL) then
Node root ←createNode()
root.data←T.data
root.right←swapTree(T.left)
root.left←swapTree(T.right)
return root
else
return NULL
endif
end swapTree
2.深度优先与广度优先
深度优先和广度优先可以用在树中,也可以用在图中。我们这里讲解一下这两种优先遍历算法。对于广度优先来讲,主要是去维护一个未检测结点表的队列。
//深度优先检索
procedure DFS(V)
VISITED(V)←1
for 邻接于V的每个结点w do
if VISITED(w)=0 then
call DFS(w)
endif
repeat
end DFS
//宽度优先
procedure BFS(V)
VISITED(V)←1;u←v
将Q初始化为空
loop
for 邻接于u的所有结点w do
if VISITED(w)=0 then
call ADDQ(w,Q)
VISITED(w)←1
endif
repeat
if Q为空 then
return
endif
call DELETEQ(u,Q)
repeat
end BFS
3. D-search检索
D-search检索是一种不太常见但非常有用的检索,它与BFS(广度优先检索)不同之处在于,下一个要检测的结点时最新加到未检测结点表的那个算法。因此这个表应做成一个栈而不是一个队列。具体算法如下:
procedure D-search(V)
VISITED(V)←1;u←V
将STACK初始化为空
loop
for 邻接于u的所有结点 w do
if VISIT(w)=0 then
pull(w,STACK)
VISITED(W)←1
endif
repeat
if STACK=null then
return
endif
push(u,STACK)
repeat
end D-search
3. 红黑树
我们在这里不深入的讲解红黑树,对于红黑树,一定是有必要单开一篇来讲解的。这个作为是否真正算得上熟悉数据结构的必要条件之一,是十分重要的。但是我们这里只是简要介绍一下,具体的红黑树,会在以后的专题中介绍。
红黑树,是一种自平衡二叉查找树。它把结点分为红黑两种结点,才被称为红黑树。它具有5种性质:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
但是其实这么说,我们可能没有直观的认识,在yang_yulei的博客里写了一个非常简单的红黑树的说法,那就是使用红链接的2-3树。而红黑树的具体实现,大家可以参考July的博客。
由于我们的重点不在于红黑树,所以这一章中我们只能简要介绍并给大家指明方向。希望能够共同学习。
3. 树的其他高级应用
1. 森林的三中检索方式
森林和树的不同在于,森林里不只有一个根节点,也就是说,森林里不止一棵树。那么那三种周游先根、中根、后根周游都有相应的变化。
- 树先根次序周游(F)
1)若F为空,则返回;
2)访问F的第一棵树的根
3)按树先根次序周游F的第一棵树的子树;
4)按树先跟次序周游F其余的树。 - 树中根次序周游(F)
1)若F为空,则返回;
2)按树中根次序周游F的第一棵树的子树;
3)访问F的第一棵树的根;
4)按树中跟次序周游F其余的树。 - 树后根次序周游(F)
1)若F为空,则返回;
2)按树后根次序周游F的第一棵树的子树;
3)按树后跟次序周游F其余的树;
4)访问F的第一棵树的根;
这样,三种遍历就已经改造完成了。
2. 双连通分图
双连通分图是很久之前说的了,现在只有双连通图与双连通分量。顾名思义,对于一个图来讲,如果一个图的两个部分,不存在唯一一个结点(学术上称为关节点)使得这两个分图相交,那么这个图就称为双连通分图。
对于双连通分图有这么几个问题,第一个问题就是生成双连通分图,第二个问题则是如何把一个不是双连通分图的图改成双连通分图。
首先,对于第一个问题来讲,如何生成双连通分图,在这之前,又需要解决两个部分,但是我们先不说,我们先看算法:
算法 生成双连通分图
procedure ART(u,v)
global DFN(n),L(n),num,n,S
置存放边的栈S为空
DFN(u)←num;L(u)←num;num←num+1
for 每个邻接于u的结点 w do
if v≠w and DFN(w)<DFN(u) then
将(u,w)加到S的顶部
endif
if DFN(w)=0 then call ART(w,u)
if L(w)≥DFN(u) then
print('new biconnected component')
loop
从栈S的顶部删去一条边
设这条边是(x,y)
print('(',x,',',y,')')
until((x,y)=(u,w) or (x,y)=(w,u)) repeat
endif
L(u)←min(L(u),L(w))
else if w≠v then
L(u)←min(L(u),DFN(w))
endif
endif
repeat
end ART
看到这,肯定第一个疑问就是DFN和L到底是什么东西。DFN是深度优先的次数,L是后跟遍历,这其实就是著名的Tarjan算法。具体求这两个数的思想如下:
1. 先深度优先遍历找出DFN(i)
2. 后根遍历找到L(i)
首先考虑结点i是不是叶子节点,
如果是叶子节点,
若有逆边就用逆边的DFN
否则,用自己的结点的DFN
如果不是叶子节点,
就用孩子结点中最小的L(u)来作为自己的L。
所谓的逆边,则是指除了深度优先检索以外还剩下的边,称为逆边。那么,使用以上的算法就可以解决了第一个问题。
对于第二个问题,如何把非双连通分图改造成双连通分图。那么还需要额外一个双连通分图列表,另外还有一个双连通分图改造函数。具体算法如下,代码请见我的github地址:
procedure ART2(u,v)
global DFN(n),L(n),num,n,S,LL
置存放边的栈S为空
DFN(u)←num;L(u)←num;num←num+1
for 每个邻接于u的结点 w do
if v≠w and DFN(w)<DFN(u) then
将(u,w)加到S的顶部
endif
if DFN(w)=0 then call ART(w,u)
if L(w)≥DFN(u) then
print('new biconnected component')
创建一个双连通分图结点L//新添加的语句
loop
从栈S的顶部删去一条边
设这条边是(x,y)
把(x,y)加入到L中//新添加的语句
print('(',x,',',y,')')
until((x,y)=(u,w) or (x,y)=(w,u)) repeat
把L加入到双连通分图结点列表LL中//新添加的语句
endif
L(u)←min(L(u),L(w))
else if w≠v then
L(u)←min(L(u),DFN(w))
endif
endif
repeat
end ART2
porcedure changeBG(LL)
global c(N:2) //c为已标记两双连通分图连接列表
遍历LL中两个双连通分图结点(u,w),u≠w
遍历c
if(u,w)还没有连接过 then
遍历u中的结点i
遍历w中的结点j
if i=j then
标记(u,w)为连接
添加(u,w)到c中
查找u中任意一结点m,m≠i
查找w中任意一结点n,n≠j
print(" 要连接(",m,",",n")")
endif
endif
endchangeBG
4. 小结
这一节中,我们主要讲了关于树的若干算法,主要是三种遍历方法,三种检索方法,以及其红黑树、双连通分图这两个应用。树论经常和图论一起讨论,我们这里只讨论一些常见的,基础的问题。在接下来的更高级的面试算法中,我们会详细介绍一些具体的算法。