Python中的深度优先搜索(DFS)--理论与实现

835 阅读6分钟

简介

图起源于数学,现在是计算机科学中广泛使用的数据结构。在构建任何有关图处理或遍历的算法时,我们首先遇到的问题之一是如何表示图,然后是如何遍历该表示

图的遍历不是一个微不足道的问题,考虑到任务的难度--许多算法已经被设计出来用于高效(但不完美)的图的遍历。

在本指南中,我们将看一下图形遍历的两种基本和最简单的算法之一。 ***深度优先搜索(DFS)。***它是最常用的算法,同时也是相关的 ***广度优先搜索(BFS)***的简单性。在了解了DFS的主要思想后,我们将在Python中实现它的图形表示--一个 毗连列表.

深度优先搜索--理论

深度优先搜索(DFS)*是一种算法,用于 遍历或定位一个 目标节点的算法。它优先考虑深度,沿着一个分支进行搜索,能走多远走多远--直到该分支的末端。一旦到了那里,它就回溯到**该分支的第一个可能的分歧点,并搜索到该*分支的末端,重复这一过程。

鉴于该算法的性质,你可以很容易地实现它的递归--你也总是可以迭代地实现一个递归算法。

这个 起始节点根节点**树形数据结构,而对于更通用的图,它可以是任何节点。

DFS被广泛用作解决图表示问题的许多其他算法的一部分。从循环搜索、路径寻找、拓扑排序,到寻找衔接点和强连接组件。DFS算法被广泛使用的原因在于其整体的简单性和易于递归实现。

DFS算法

DFS算法非常简单,由以下步骤组成。

  1. 将当前节点标记为已访问节点。
  2. 遍历未被访问的 相邻节点,并递归调用该节点的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 实例中。

下面是我们的例子图的样子。

graph

我们正在寻找一个从节点0 到节点3 的路径,如果它存在,这个路径将被保存到一个被访问的节点集合中,称为traversal_path ,这样我们就可以重建它进行打印。

traversal_path = []
traversal_path = dfs(adj_list, 0, 3, traversal_path)
print(traversal_path)

我们的算法将采取以下步骤。

  • 将节点0 加入到遍历路径中,并将其标记为已访问节点。检查节点0 是否等于目标节点3 ,因为它不是,所以继续并遍历它的邻居(12 )。
  • 邻居1 是否被访问过?- 没有。那么,该算法将递归调用该节点的函数。
    • 递归调用节点1 。将节点1 加入到遍历路径中,并将其标记为已访问, 。1 是否等于我们的目标节点3 ? - 不,继续并遍历它的邻居 (03)。
    • 邻居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]

搜索结束后,图上的标记节点代表我们到达目标节点的路径。

marked graph

如果在开始节点和目标节点之间没有路径,那么该遍历路径将是空的

注意:图也可以是断开的,也就是说,至少有两个节点不能通过路径连接。在这种情况下,DFS会忽略它不能到达的节点。

disconnected graph

例如在这个图中,如果我们要从节点0 到节点4 开始DFS,就没有这样的路径,因为它没有办法到达目标节点。

总结

在这篇文章中,我们已经解释了深度优先搜索算法背后的理论。我们描述了广泛使用的Python递归实现,并讨论了该算法不能正常工作的边界情况。