学习Python深度优先搜索(DFS)算法

371 阅读8分钟

什么是深度优先搜索(DFS)算法?

在我们之前关于图形和图形遍历算法的故事基础上,这次我们将研究深度优先搜索算法。深度搜索算法也是通过逐个顶点的探索来遍历图形,但它是按照顶点的垂直顺序进行的。

虽然深度搜索算法不能保证图中任何两个可达顶点之间的最短路径,但它被广泛用于许多应用中。其中一些是:寻找连接部件,进行拓扑排序,寻找图的桥梁,确定图或树中任何两个顶点的接近程度,以及解决具有唯一解决方案的难题,如迷宫。

算法概述

深度优先算法首先将起始顶点表示为已访问,并将其放入已访问节点的地图中。

该算法将检查该顶点是否对应于正在搜索的实体(在我们下面的例子中,这被注释为一个微不足道的检查)。

如果找到了被搜索的实体,算法将停止执行,它将返回相应的顶点。否则,该算法将在其相邻的顶点中循环,并递归到每个顶点中。

这样一来,该算法将

  • a) 最终沿着下降的路径找到目标实体。
  • b) 到达分支中的最后一个*(叶子*)顶点,在图中回溯(从实现上看:它将返回到函数调用栈中的前一个调用者),并沿着下一个相邻顶点重复下降。
  • c) 将所有的顶点标记为已访问,但没有找到目标实体,从而耗尽图形。
  • d) 在非终结的情况下,即无限图,永远不会结束。

简而言之,与其他一些算法相反(见关于广度优先搜索算法的博客),深度优先搜索算法将始终试图尽可能地去寻找解决方案,并尽可能地缩小范围,因此它被称为深度优先。

什么是DFS的特性?

深度优先搜索方法在遍历图形方面是高效和简单的。

然而,在深度图中,即使解决方案位于相对较浅的起始顶点,但远离起始路径,也可能需要大量的时间来找到解决方案。

具体来说,只有在搜索遍历了整个前一个路径之后,才能探索到图的下一个路径。

在某些情况下,这一特性可以通过限制具有熟悉结构的图形中的搜索深度(空间复杂度)来缓解,也就是说,通过知道在一个图形中哪里可以预期到解决方案。另外,搜索的总成本也可以被限制(时间复杂度),只允许遍历固定数量的顶点。

实施DFS Python

我们的深度优先搜索算法由一个函数DFS() 来实现,它有四个必要参数和一个可选参数。

  • graph 参数希望有一个初始化的Graph对象
  • start 参数需要起始顶点,我们可以自由选择(记住,图不是树,没有绝对的根)。
  • visited 参数引用一个地图,即一个被访问顶点的字典,其值是搜索路径上的边。这个参数是外部定义的,这样我们就可以在以后的某个时刻恢复搜索,并构建搜索路径。
  • target 参数是我们想在图中寻找的实体,被包围在一个顶点中。
  • depth 参数是可选的(默认为1),它跟踪当前探索的顶点的深度,用于可视化目的。

为了更好地理解该算法及其实现,下面的代码中精确地描述了每个步骤。

import graph
sep = '  '

# The 'depth' parameter tracks the depth in the call stack 
# the algorithm is currently at, for visualization purposes.
def DFS(graph, vertex, visited, target=None, depth=1):
    print(sep*depth + f'Exploring vertex {vertex.entity()}')
    
    # The starting vertex is visited first and has no leading edges.
    # If we did not put it into 'visited' in the first iteration,
    # it would end up here during the second iteration, pointed to
    # by one of its children vertices as a previously unvisited vertex.
    visited[vertex] = None
    result = None
        
    # Trivial check #1: searches for None are immediately terminated.
    if target is None:
        print(f' The vertex {target} does not exist')
        return result
    # Trivial check #2: if the entity is in the starting vertex.
    elif target == vertex.entity():
        result = vertex
        return result
    
    # Otherwise, search through the lower-level vertices
    for edge in graph.adjacent_edges(vertex):
        # Gets the second endpoint.
        v_2nd_endpoint = edge.opposite(vertex)
        
        # Examines the second endpoint.
        if v_2nd_endpoint not in visited:
            # Keep searching at the lower level, from the second endpoint.
            result = DFS(graph, v_2nd_endpoint, visited, target, depth+1)
            print(sep*depth + f'Returning to vertex {vertex.entity()}')
            
            # Add the second endpoint to 'visited' and maps the leading 
            # edge for the search path reconstruction.
            visited[v_2nd_endpoint] = edge
            
            # If the search was successful, stop the search
            if result is not None:
                break
    
    return result
Before we can test the algorithm, we have to initialize a graph and build it by adding vertices and edges to it:
# Initializes an empty graph (object).
g = Graph()

# Loads the graph with the first ten vertices.
for i in range(10):
    g.add_vertex(i)

# Constructs the 'vertices' dictionary for a more
# convenient access during the graph construction.
vertices = {k.entity():k for k in g.vertices()}

# Constructs an arbitrary graph from
# the existing vertices and edgs.
g.add_edge(vertices[0], vertices[1])
g.add_edge(vertices[0], vertices[2])
g.add_edge(vertices[0], vertices[4])
g.add_edge(vertices[4], vertices[3])
g.add_edge(vertices[3], vertices[5])
g.add_edge(vertices[0], vertices[5])
g.add_edge(vertices[2], vertices[6])

# Initializes the visited dictionary
# and the search path.
visited = {}
path = []

在我们测试算法之前,我们必须初始化一个图,并通过添加顶点和边来构建它。

# Starts the search.
result = DFS(g, vertices[5], visited, 6)

# If the entity is found...
if result is not None:
    # The search path ends with the found vertex 
    # (entity). Each vertex is a container for
    # its real-world entity.
    path_vertex = result

    # The entity is added to the 'path'.
    path.append(path_vertex.entity())

    # Constructs the rest of the search path
    # (if it exists)...
    while True:
        # Gets a discovery edge 
        # leading to the vertex.
        path_edge = visited.get(path_vertex)

        # If the path vertex is the root, 
        # it has no discovery edge...
        if path_edge is None:
            break

        # Otherwise, gets the second
        # (parent vertex) endpoint.
        path_vertex = path_edge.opposite(path_vertex)

        # The entity is added to the 'path'.
        path.append(path_vertex.entity())
    print('Search path found:', end=' ')
    # The path is reversed and starts
    # with the root vertex.
    print(*reversed(path), sep=' -> ')

# Otherwise...
else:
    print('\nEntity is not found')

现在我们已经准备好了一切,我们可以测试DFS() ,看看它是如何工作的。这里是代码的一部分,它运行算法,构建搜索路径(如果有的话),并以逐步的方式显示它如何在图中进行。

# Starts the search.
result = DFS(g, vertices[5], visited, 6)

# If the entity is found...
if result is not None:
    # The search path ends with the found vertex 
    # (entity). Each vertex is a container for
    # its real-world entity.
    path_vertex = result

    # The entity is added to the 'path'.
    path.append(path_vertex.entity())

    # Constructs the rest of the search path
    # (if it exists)...
    while True:
        # Gets a discovery edge 
        # leading to the vertex.
        path_edge = visited.get(path_vertex)

        # If the path vertex is the root, 
        # it has no discovery edge...
        if path_edge is None:
            break

        # Otherwise, gets the second
        # (parent vertex) endpoint.
        path_vertex = path_edge.opposite(path_vertex)

        # The entity is added to the 'path'.
        path.append(path_vertex.entity())
    print('Search path found:', end=' ')
    # The path is reversed and starts
    # with the root vertex.
    print(*reversed(path), sep=' -> ')

# Otherwise...
else:
    print('\nEntity is not found')

测试运行给了我们输出。

  Exploring vertex 5
    Exploring vertex 3
      Exploring vertex 4
        Exploring vertex 0
          Exploring vertex 1
        Returning to vertex 0
          Exploring vertex 2
            Exploring vertex 6
          Returning to vertex 2
        Returning to vertex 0
      Returning to vertex 4
    Returning to vertex 3
  Returning to vertex 5
Search path found: 5 -> 3 -> 4 -> 0 -> 2 -> 6

根据输出,我们可以看到搜索从顶点5开始,DFS() ,找到了实体顶点6。整个搜索路径也被显示出来,然而,我们应该注意到,搜索路径并不是最短的路径。

5 -> 0 -> 2 -> 6

如果我们对一个不存在的实体进行搜索,该算法将遍历整个图,并形成一棵遍历树,显示顶点被访问的顺序。

# Starts the search.
result = DFS(g, vertices[5], visited, 66)
…

  Exploring vertex 5
    Exploring vertex 3
      Exploring vertex 4
        Exploring vertex 0
          Exploring vertex 1
        Returning to vertex 0
          Exploring vertex 2
            Exploring vertex 6
          Returning to vertex 2
        Returning to vertex 0
      Returning to vertex 4
    Returning to vertex 3
  Returning to vertex 5

该实体未被发现。

效率分析

从理论上讲,深度优先搜索算法的时间复杂度为 O(|V|+|E|),其中V代表顶点的数量,E代表边的数量。

然而,实际的时间和空间复杂度取决于具体的实现,由其应用领域指导。该算法将对每个顶点处理一次,对每条边处理两次,在处理一条边时需要恒定的时间。

该算法比其他一些算法(如广度优先搜索算法)更节省空间,因为它只依靠顶点的邻接边来跟踪其当前路径。然而,它使用递归 ,并在本质上受到调用栈最大深度的限制。当遍历一个非常深的图时,这个特性就会变得非常明显了。

该算法的速度主要由图的深度和邻接边的顺序决定。

总结

在这篇文章中,我们了解了深度优先搜索算法。

  • 首先,我们解释了什么是深度优先搜索算法。
  • 第二,我们看了一下它的常见目的和应用。
  • 第三,我们通过解释该算法的工作原理。
  • 第四,我们研究了该算法的主要属性。
  • 第五,我们经历了该算法的实现,它是基于Graph 抽象数据结构的(关于类的实现,见博客中的广度优先搜索算法)。我们还通过调用其主函数DFS()来测试该算法,并分析了其执行步骤。
  • 第六,我们分析了算法的效率,并将其与另一个领域代表算法进行了比较。

最后,我们得出结论,无论其效率如何,如果解决方案存在,深度优先搜索算法不一定能找到它,或者在实际到达解决方案之前,可能需要花费无限的时间。然而,我们也确定,可以采取某些措施来提高算法的效率和适用性,比如限制深度或遍历顶点的总数。