图遍历算法之DFS/BFS

在计算机科学, 图遍历(Tree Traversal,也称图搜索)是一系列图搜索的算法, 是单次访问树结构类型数据(tree data structure)中每个节点以便检查或更新的一系列机制。图遍历算法可以按照节点访问顺序进行分类,根据访问目的或使用场景的不同,算法大致可分为28种:

序号 算法名称 应用场景
1 \alpha-\beta 修剪算法 减少树搜索过程中minimax算法评估的节点数量,属于对抗性搜索算法
2 A^{\star}算法 用于寻路和图遍历,在多个节点之间寻找路径,多用于点对点
3 B^{\star} 算法 一种最佳优先图搜索算法,用于查找给定初始节点到任何目标节点的最低成本路径
4 回溯算法(backtracking) 通用算法,用于查找某些计算问题的所有可行解,特别是约束满足问题,逐步构建候选解,并在确定候选解不可行时进行回溯
5 波束搜索(beam) 算法 启发式搜索算法,通过扩展有限集中最可能的节点来探索图形
6 Bellman-Ford 算法 计算加权有向中单个源节点到其他节点的最短路径,比Dijkstra算法慢,但更通用
7 Best-first 算法 根据指定规则选择最可能的点来进行图搜索
8 双向(Bidirectional)搜索 从有向图中找出给定初始点到目标点的最短路径,进行两次搜索:一次从初始点开始向前搜索,一次从目标点向后搜索,相遇时停止
9 Boruvka算法 贪婪算法,从图中找到所有边缘权重不同的最小生成树或最小生成林
10 分支定界(Branch & Bound) 算法 通过状态空间搜索可行解
11 广度优先搜索BFS (Breadth-first search) 遍历或搜索树或图数据结构,从图任意节点开始,并在移动到下一节点之前探索当前深度水平的所有相邻节点
12 D^{\star}算法 增量搜索算法
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 A^{\star})算法 在加权图中找到从给定初始节点到一组目标节点中任意节点之间的最短路径
20 迭代深化搜索算法(Iterative deepening) 具有深度限制的深度优先搜索
21 Johnson算法 在稀疏边缘加权有向图中找到节点之间最短路径
22 跳跃点搜索JPS (Jump point)算法 A^{\star}算法优化版本
23 Kruskal 算法 最小生成树算法,找到联通树中任意两棵树的最小权重边缘
24 词典宽度有限搜索Lex-BFS算法 对图节点进行排序的线形时间搜索算法
25 LPA^{\star}算法 基于A^{\star}算法的增量启发式搜索算法
26 Prim 算法 一种贪婪算法,用于从加权无向图中找到最小生成树
27 SMA^{\star}算法 使用有界内存的A^{\star}算法,继承了A^{\star}算法的特性
28 最短路径快速SPFA算法 Bellman-Ford算法的改进,在加权有向图中计算单源最短路径

图遍历即以特定方式访问图中所有节点,给定节点下有多种可能的搜索路径。假定以顺序方式进行(非并行),还未访问的节点就需通过堆栈(LIFO)或队列(FIFO)规则来确定访问先后。由于树结构是一种递归的数据结构,在清晰的定义下,未访问节点可存储在调用堆栈中。本文介绍了图遍历领域最流行的广度优先搜索算法BFS和深度优先搜索算法DFS,对其原理、应用及实现进行了阐述。通常意义上而言,深度优先搜索(DFS)通过递归调用堆栈比较容易实现,广义优先搜索通过队列实现。

一、深度优先搜索(Depth-first search)

深度优先搜索(DFS)是用于遍历或搜索图数据结构的算法,该算法从根节点开始(图搜索时可选择任意节点作为根节点)沿着每个分支进行搜索,分支搜索结束后在进行回溯。在进入下一节点之前,树的搜索尽可能的加深。
DFS的搜索算法如下(以二叉树为例):假定根节点(图的任意节点可作为根节点)标记为N,
(L) : 递归遍历左子树,并在节点N结束。
(R): 递归遍历右子树,并在节点N结束。
(N): 访问节点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遍历属于拓扑排序后的遍历,父节点总是在任何子节点之前被访问。该遍历方式的图示如下:


图1. 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遍历以排序顺序访问节点数据。该遍历方式的图示如下:


图2. 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遍历图示如下:


图3. 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的遍历方法图示如下:


图4. 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)

结果展示:


图5. 无向图示例

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)

结果展示:


图6. 无向图示例

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的实现。图示例如下:


图7. 无向图示例

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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350