在计算机科学, 图遍历(Tree Traversal,也称图搜索)是一系列图搜索的算法, 是单次访问树结构类型数据(tree data structure)中每个节点以便检查或更新的一系列机制。图遍历算法可以按照节点访问顺序进行分类,根据访问目的或使用场景的不同,算法大致可分为28种:
序号 | 算法名称 | 应用场景 |
---|---|---|
1 | 修剪算法 | 减少树搜索过程中minimax算法评估的节点数量,属于对抗性搜索算法 |
2 | 算法 | 用于寻路和图遍历,在多个节点之间寻找路径,多用于点对点 |
3 | 算法 | 一种最佳优先图搜索算法,用于查找给定初始节点到任何目标节点的最低成本路径 |
4 | 回溯算法(backtracking) | 通用算法,用于查找某些计算问题的所有可行解,特别是约束满足问题,逐步构建候选解,并在确定候选解不可行时进行回溯 |
5 | 波束搜索(beam) 算法 | 启发式搜索算法,通过扩展有限集中最可能的节点来探索图形 |
6 | Bellman-Ford 算法 | 计算加权有向中单个源节点到其他节点的最短路径,比Dijkstra算法慢,但更通用 |
7 | Best-first 算法 | 根据指定规则选择最可能的点来进行图搜索 |
8 | 双向(Bidirectional)搜索 | 从有向图中找出给定初始点到目标点的最短路径,进行两次搜索:一次从初始点开始向前搜索,一次从目标点向后搜索,相遇时停止 |
9 | Boruvka算法 | 贪婪算法,从图中找到所有边缘权重不同的最小生成树或最小生成林 |
10 | 分支定界(Branch & Bound) 算法 | 通过状态空间搜索可行解 |
11 | 广度优先搜索BFS (Breadth-first search) | 遍历或搜索树或图数据结构,从图任意节点开始,并在移动到下一节点之前探索当前深度水平的所有相邻节点 |
12 | 算法 | 增量搜索算法 |
13 | 深度优先搜索DFS(Depth-first search) | 遍历或搜索树或图数据结构的算法,选择任意节点作为根节点,在回溯之前尽可能地沿着每个分支进行搜索 |
14 | Dijkstra算法(SPF 算法, shortest path faster) | 搜索节点之间的最短路径。单源最短路径搜索:给定初始点,搜寻初始点到其他点的最短路径,生成最短路径树 |
15 | Edmonds算法 | 最小生成树的定向模拟 |
16 | Floyd-Warshall算法 | 在具有正或负边缘权重的加权图中寻找最短路径 |
17 | Fringe搜索 | 寻找从给定初始节点到目标节点之间的最低成本路径 |
18 | 爬山(Hill Climbing) 算法 | 从图的任意节点开始,然后尝试通过对节点进行增量更改来找到更好的节点 |
19 | IDA (Iterative deepening )算法 | 在加权图中找到从给定初始节点到一组目标节点中任意节点之间的最短路径 |
20 | 迭代深化搜索算法(Iterative deepening) | 具有深度限制的深度优先搜索 |
21 | Johnson算法 | 在稀疏边缘加权有向图中找到节点之间最短路径 |
22 | 跳跃点搜索JPS (Jump point)算法 | 算法优化版本 |
23 | Kruskal 算法 | 最小生成树算法,找到联通树中任意两棵树的最小权重边缘 |
24 | 词典宽度有限搜索Lex-BFS算法 | 对图节点进行排序的线形时间搜索算法 |
25 | LPA算法 | 基于算法的增量启发式搜索算法 |
26 | Prim 算法 | 一种贪婪算法,用于从加权无向图中找到最小生成树 |
27 | SMA算法 | 使用有界内存的算法,继承了算法的特性 |
28 | 最短路径快速SPFA算法 | Bellman-Ford算法的改进,在加权有向图中计算单源最短路径 |
图遍历即以特定方式访问图中所有节点,给定节点下有多种可能的搜索路径。假定以顺序方式进行(非并行),还未访问的节点就需通过堆栈(LIFO)或队列(FIFO)规则来确定访问先后。由于树结构是一种递归的数据结构,在清晰的定义下,未访问节点可存储在调用堆栈中。本文介绍了图遍历领域最流行的广度优先搜索算法BFS和深度优先搜索算法DFS,对其原理、应用及实现进行了阐述。通常意义上而言,深度优先搜索(DFS)通过递归调用堆栈比较容易实现,广义优先搜索通过队列实现。
一、深度优先搜索(Depth-first search)
深度优先搜索(DFS)是用于遍历或搜索图数据结构的算法,该算法从根节点开始(图搜索时可选择任意节点作为根节点)沿着每个分支进行搜索,分支搜索结束后在进行回溯。在进入下一节点之前,树的搜索尽可能的加深。
DFS的搜索算法如下(以二叉树为例):假定根节点(图的任意节点可作为根节点)标记为,
(L) : 递归遍历左子树,并在节点结束。
(R): 递归遍历右子树,并在节点结束。
(N): 访问节点。
这些步骤可以以任意次序排列。如果(L)在(R)之前,则该过程称为从左到右的遍历;反之,则称为从右到左的遍历。根据访问次序的不同,深度优先搜索可分为 pre-order、in-order、out-order以及post-order遍历方式。
1. Pre-order(NLR)遍历
(a)检查当前节点是否为空;
(b)展示根节点或当前节点数据;
(c)递归调用pre-order函数遍历左子树;
(d)递归调用pre-order函数遍历右子树。
pre-order遍历属于拓扑排序后的遍历,父节点总是在任何子节点之前被访问。该遍历方式的图示如下:
遍历次序依次为:F -B -A-D- C-E-G- I-H.
2. In-order (LNR)遍历
(a)检查当前节点是否为空;
(b)递归调用in-order函数遍历左子树;
(c)展示根节点或当前节点数据;
(d)递归调用in-order函数遍历右子树。
在二叉树搜索中,in-order遍历以排序顺序访问节点数据。该遍历方式的图示如下:
遍历次序依次为:A -B - C - D - E - F - G -H-I
3. Out-order (RNL)遍历
(a)检查当前节点是否为空;
(b)递归调用out-order函数遍历右子树;
(c)展示根节点或当前节点数据;
(d)递归调用out-order函数遍历左子树。
该遍历方式与LNR类似,但先遍历右子树后遍历左子树。仍然以图2为例,遍历次序依次为:H- I-G- F- B- E- D- C- A.
4. post-order (LRN)遍历
(a)检查当前节点是否为空;
(b)递归调用post-order函数遍历左子树;
(c)递归调用post-order函数遍历右子树;
(d)展示根节点或当前节点数据。
post-order遍历图示如下:
遍历次序依次为:A-C-E-D-B-H-I-G-F.
pre-order遍历方式使用场景:用于创建树或图的副本;
in-order遍历使用场景:二叉树遍历;
post-order遍历使用场景:删除树
遍历追踪也称树的序列化,是所访问根节点列表。无论是pre-order,in-order或是post-order都无法完整的描述树特性。给定含有不同元素的树结构,pre-order或post-order与in-order遍历方式结合起来使用才可以描述树的独特性。
二、广度优先搜索(Breath-first search)
树或图形的访问也可以按照节点所处的级别进行遍历。在每次访问下一层级节点之前,遍历所在高层级的所有节点。BFS从根节点(图的任意节点可作为根节点)出发,在移动到下一节点之前访问所有相同深度水平的相邻节点。
1945年,Konrad Zuse 在他被拒的博士论文中首次提到BFS在联通子图方面的应用。1972年,该博士论文被出版。1959年,Edward F. Moore对BFS进行重新设计,并用于寻找迷宫中的最短路径。 1961年,C.Y.Lee对算法进行开发作为电路路由算法。
BFS的遍历方法图示如下:
遍历次序依次为: F-B-G-A-D-I-C-E-H.
三、DFS与BFS的伪代码实现
1. DFS: pre-order 遍历
preorder(node)
if (node = null)
return
visit(node)
preorder(node.left)
preorder(node.right)
iterativePreorder(node)
if (node = null)
return
s ← empty stack
s.push(node)
while (not s.isEmpty())
node ← s.pop()
visit(node)
//right child is pushed first so that left is processed first
if (node.right ≠ null)
s.push(node.right)
if (node.left ≠ null)
s.push(node.left)
2. DFS:in-order遍历
inorder(node)
if (node = null)
return
inorder(node.left)
visit(node)
inorder(node.right)
iterativeInorder(node)
s ← empty stack
while (not s.isEmpty() or node ≠ null)
if (node ≠ null)
s.push(node)
node ← node.left
else
node ← s.pop()
visit(node)
node ← node.right
3. DFS: post-order遍历
postorder(node)
if (node = null)
return
postorder(node.left)
postorder(node.right)
visit(node)
iterativePostorder(node)
s ← empty stack
lastNodeVisited ← null
while (not s.isEmpty() or node ≠ null)
if (node ≠ null)
s.push(node)
node ← node.left
else
peekNode ← s.peek()
// if right child exists and traversing node
// from left child, then move right
if (peekNode.right ≠ null and lastNodeVisited ≠ peekNode.right)
node ← peekNode.right
else
visit(peekNode)
lastNodeVisited ← s.pop()
4. BFS
levelorder(root)
q ← empty queue
q.enqueue(root)
while (not q.isEmpty())
node ← q.dequeue()
visit(node)
if (node.left ≠ null)
q.enqueue(node.left)
if (node.right ≠ null)
q.enqueue(node.right)
四、DFS与BFS的R及Python代码实现
1. R语言实现DFS与BFS
图算法相关的R包为igraph,主要包括图的生成、图计算等一系列算法的实现。
1.1 R语言实现DFS:函数dfs
使用方法:
dfs(graph, root, neimode = c("out", "in", "all", "total"),
unreachable = TRUE, order = TRUE, order.out = FALSE,
father = FALSE, dist = FALSE, in.callback = NULL,
out.callback = NULL, extra = NULL, rho = parent.frame())
参数说明:
参数 | 含义 |
---|---|
graph | 输入图 |
root | 根节点,搜索开始的单个节点 |
neimode | 对有向图,指定访问边的类型。'out' 表示从节点出发的边,'in'表示进节点的边,'all' 完全忽略边的方向,'total' 与'all'相同。对于无向图,该参数可忽略 |
unreachable | 逻辑值,取值'true'或'false', 表示搜索是否需要访问从根节点无法到达的节点集合 |
order | 逻辑值,是否返回DFS排序后的节点序列 |
order.out | 逻辑值,是否返回离开子图的节点排序 |
father | 逻辑值,是否返回节点的父节点 |
dist | 逻辑值,是否返回从根节点出发搜索的距离 |
in.callback | 若不为空,则存在callback函数 |
out.callback | 若不为空,则存在callback函数 |
extra | callback函数的其他参数 |
rho | callback函数被评估的环境 |
示例:
require(igraph)
## generate graph
graph <- make_tree(10) %du% make_tree(10)
# plot graph
plot(graph)
# 搜索从根节点出发的节点排序
dfs_res<-dfs(graph, root=1, "out",TRUE, TRUE, TRUE, TRUE)
结果展示:
DFS R输出节点排序:
1-2-4-8-9-5-10-3-6-7-11-12-14-18-19-15-20-13-16-17
1.2 R语言实现BFS:函数bfs
使用方法:
bfs(graph, root, neimode = c("out", "in", "all", "total"),
unreachable = TRUE, restricted = NULL, order = TRUE,
rank = FALSE, father = FALSE, pred = FALSE, succ = FALSE,
dist = FALSE, callback = NULL, extra = NULL,
rho = parent.frame())
参数含义同dfs
示例:
require(igraph)
## generate graph
graph <- make_ring(10) %du% make_ring(10)
# plot graph
plot(graph)
# 搜索从根节点出发的节点排序
bfs_res<-bfs(graph, root=1, "out",order=TRUE, rank=TRUE, father=TRUE, pred=TRUE,
succ=TRUE, dist=TRUE)
结果展示:
BFS R输出节点排序:
1-2-10-3-9-4-8-5-7-6-11-12-20-13-19-14-18-15-17-16
2. Python 实现BFS 及DFS
以寻找两点之间的路径为例,分别展示BFS及DFS的实现。图示例如下:
2.1 Python实现BFS
示例:
graph = {'A': set(['B', 'C']),
'B': set(['A', 'D', 'E']),
'C': set(['A', 'F']),
'D': set(['B']),
'E': set(['B', 'F']),
'F': set(['C', 'E'])}
def bfs_paths(graph, start, goal):
queue = [(start, [start])]
while queue:
(vertex, path) = queue.pop(0)
for next in graph[vertex] - set(path):
if next == goal:
yield path + [next]
else:
queue.append((next, path + [next]))
list(bfs_paths(graph, 'A', 'F'))
输出结果:
[['A', 'C', 'F'], ['A', 'B', 'E', 'F']]
2.2 Python实现DFS
示例:
graph = {'A': set(['B', 'C']),
'B': set(['A', 'D', 'E']),
'C': set(['A', 'F']),
'D': set(['B']),
'E': set(['B', 'F']),
'F': set(['C', 'E'])}
def dfs_paths(graph, start, goal):
stack = [(start, [start])]
while stack:
(vertex, path) = stack.pop()
for next in graph[vertex] - set(path):
if next == goal:
yield path + [next]
else:
stack.append((next, path + [next]))
list(dfs_paths(graph, 'A', 'F'))
输出结果:
[['A', 'C', 'F'], ['A', 'B', 'E', 'F']]
参考资料:
[1] 维基百科:https://en.wikipedia.org/wiki/Tree_traversal
[2] GeeksforGeeks: https://www.geeksforgeeks.org/tree-traversals-inorder-preorder-and-postorder/
[3] http://webdocs.cs.ualberta.ca/~holte/T26/tree-traversal.html
[4]Martin Broadhurst, Graph Algorithm: http://www.martinbroadhurst.com/Graph-algorithms.html#section_1_1
[5]igraph: https://igraph.org/r/doc/dfs.html
[6]igraph: https://igraph.org/r/doc/bfs.html
[7] Depth-First Search and Breadth-First Search in Python: https://eddmann.com/posts/depth-first-search-and-breadth-first-search-in-python/