简介
图起源于数学,现在是计算机科学中广泛使用的数据结构。在构建任何有关图处理或遍历的算法时,我们首先遇到的问题之一是如何表示图,然后是如何遍历该表示。
图的遍历不是一个微不足道的问题,考虑到任务的难度--许多算法已经被设计出来用于高效(但不完美)的图的遍历。
在本指南中,我们将看一下图形遍历的两种基本和最简单的算法之一。 ***深度优先搜索(DFS)。***它是最常用的算法,同时也是相关的 ***广度优先搜索(BFS)***的简单性。在了解了DFS的主要思想后,我们将在Python中实现它的图形表示--一个 毗连列表.
深度优先搜索--理论
深度优先搜索(DFS)*是一种算法,用于 遍历或定位一个 目标节点的算法。它优先考虑深度,沿着一个分支进行搜索,能走多远走多远--直到该分支的末端。一旦到了那里,它就回溯到**该分支的第一个可能的分歧点,并搜索到该*分支的末端,重复这一过程。
鉴于该算法的性质,你可以很容易地实现它的递归--你也总是可以迭代地实现一个递归算法。
这个 起始节点是 根节点**树形数据结构,而对于更通用的图,它可以是任何节点。
DFS被广泛用作解决图表示问题的许多其他算法的一部分。从循环搜索、路径寻找、拓扑排序,到寻找衔接点和强连接组件。DFS算法被广泛使用的原因在于其整体的简单性和易于递归实现。
DFS算法
DFS算法非常简单,由以下步骤组成。
- 将当前节点标记为已访问节点。
- 遍历未被访问的 相邻节点,并递归调用该节点的DFS函数。
当找到目标节点或整个图被遍历(所有节点都被访问过)时,该算法就会停止。
由于图可以有循环,我们需要一个系统来避免它们,这样我们就不会陷入无限的循环。这就是为什么我们要把我们通过的每一个节点都 "标记 "为已访问的节点,把它们添加到一个只包含唯一条目的
Set。
通过将节点标记为 "已访问",如果我们再次遇到该节点--我们就陷入了一个循环!"。无尽的计算能力和时间被浪费在循环上,消失在太虚之中。
伪代码
鉴于这些步骤,我们可以用伪代码来总结DFS。
DFS(G, u):
# Input processing
u.visited = true
for each v in G.adj[u]:
if !v.visited:
DFS(G, v)
# Output processing
根据图搜索的目的,进行输入和输出处理。我们对DFS的输入处理将是检查当前节点是否与目标节点相等。
有了这个观点,你就可以真正开始体会到这个算法是多么简单而有用。
深度优先搜索--实现
深度优先搜索的实现在代码中通常是递归的,因为那是多么自然的一对,但它也可以很容易地以非递归方式实现。我们将使用递归方法,因为它更简单、更适合。
def dfs(adj_list, start, target, path, visited = set()):
path.append(start)
visited.add(start)
if start == target:
return path
for neighbour in adj_list[start]:
if neighbour not in visited:
result = dfs(adj_list, neighbour, target, path, visited)
if result is not None:
return result
path.pop()
return None
我们将起始节点添加到我们的遍历路径的开头,并通过将其添加到一组visited ,将其标记为已访问。然后,我们遍历尚未被访问的起始节点的邻居,并为每个节点递归调用该函数。递归调用的结果是沿着一个 "分支 "尽可能地深入。
我们将递归结果保存在一个变量中--在返回None 的情况下,这意味着在这个分支中没有找到目标节点,我们应该尝试另一个。如果递归调用实际上没有返回None ,那就意味着我们已经找到了目标节点,我们把遍历路径作为结果返回。
最后,如果我们发现自己在for 循环之外,这意味着我们已经访问了当前节点的所有相邻分支,并且没有一个分支能通向我们的目标节点。因此,我们将当前节点从路径中移除,并将None 作为结果。
运行DFS
让我们通过一个例子来说明该代码是如何工作的。我们将使用一个Python字典来表示图的邻接列表。这里是我们在下面的例子中要使用的图。
adj_list = {
0 : [1, 2],
1 : [0, 3],
2 : [0, 3],
3 : [1, 2, 4],
4 : [3]
}
邻接列表是代码中图形表示的一种类型,它由代表每个节点的键和每个键的值的列表组成,包含用边连接到键节点的节点。
使用字典来表示是在 Python 中快速表示图的最简单的方法,尽管你也可以定义你自己的Node 类并把它们添加到Graph 实例中。
下面是我们的例子图的样子。
我们正在寻找一个从节点0 到节点3 的路径,如果它存在,这个路径将被保存到一个被访问的节点集合中,称为traversal_path ,这样我们就可以重建它进行打印。
traversal_path = []
traversal_path = dfs(adj_list, 0, 3, traversal_path)
print(traversal_path)
我们的算法将采取以下步骤。
- 将节点
0加入到遍历路径中,并将其标记为已访问节点。检查节点0是否等于目标节点3,因为它不是,所以继续并遍历它的邻居(1和2)。 - 邻居
1是否被访问过?- 没有。那么,该算法将递归调用该节点的函数。- 递归调用节点
1。将节点1加入到遍历路径中,并将其标记为已访问, 。1是否等于我们的目标节点3? - 不,继续并遍历它的邻居 (0和3)。 - 邻居
0是否被访问?- 是的,转到下一个节点。 - 邻居
3,是否被访问?- 没有,为这个节点递归地调用函数。- 递归调用节点
3。将节点3加入到遍历路径中,并将其标记为已访问。3是否等于我们的目标节点3? - 是,目标节点已被找到,返回遍历路径。
- 递归调用节点
- 递归调用节点
| 当前节点 | 路径 | 已访问 |
|---|---|---|
0 | [0] | {0} |
1 | [0,1] | {0,1} |
3 | [0,1,3] | {0,1,3} |
算法停止,我们的程序会打印出从节点0 到节点3 的结果遍历路径。
[0, 1, 3]
搜索结束后,图上的标记节点代表我们到达目标节点的路径。
如果在开始节点和目标节点之间没有路径,那么该遍历路径将是空的。
注意:图也可以是断开的,也就是说,至少有两个节点不能通过路径连接。在这种情况下,DFS会忽略它不能到达的节点。
例如在这个图中,如果我们要从节点0 到节点4 开始DFS,就没有这样的路径,因为它没有办法到达目标节点。
总结
在这篇文章中,我们已经解释了深度优先搜索算法背后的理论。我们描述了广泛使用的Python递归实现,并讨论了该算法不能正常工作的边界情况。