什么是深度优先搜索(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()来测试该算法,并分析了其执行步骤。
- 第六,我们分析了算法的效率,并将其与另一个领域代表算法进行了比较。
最后,我们得出结论,无论其效率如何,如果解决方案存在,深度优先搜索算法不一定能找到它,或者在实际到达解决方案之前,可能需要花费无限的时间。然而,我们也确定,可以采取某些措施来提高算法的效率和适用性,比如限制深度或遍历顶点的总数。