在上一篇文章,我们共同探讨了广度优先搜索算法(BFS),在遍历下一层顶点之前,需要获取它的所有邻居顶点。在这篇文章里,我们将用另一种 depth-first search(DFS) 去遍历一个树。
DFS算法有很多的实际应用:
- 拓扑排序。
- 检测是否有环。
- 寻找路径,例如迷宫拼图中的路径。
- 在图中查找两个顶点是否相连。
在 DFS中,从一个给定的顶点开始,然后沿着它的边一直查找,直到到达该路线的尽头。为了实现这一点,我们将使用 Stack 来探索另一条边的下一个可达的顶点,直到访问完所有顶点。
示例
我们将使用 stack 的 last-in-last-out的特性,去跟踪每一层的顶点。每一次向 stack 中 push 顶点,意味着遍历深入了一层。当我们一直push到最底端的时候,可以pop弹出上一层的顶点。
1,我们选择任意顶点A作为起点,并将它加入到栈中。
2,只要stack不是空的,我们就访问栈顶的顶点元素,并且将它们第一个没有访问过的元素入栈,在这个例子中,访问顶点A,并且将顶点B入栈。
1,接下来,访问顶点B,因为,A已经访问过了,所以,将E入栈。
2,访问顶点E,并将顶点F入栈。
值得注意的是,每一次往栈中push元素,就相当于我们遍历更深了一层。我们只需要沿着一条路径一直访问下去,直到到达最底部。然后进行回溯。而不是访问所有的相邻顶点。
1,我们访问顶点 F ,并且将顶点 G 入栈。
2,我们访问顶点 G ,并且将顶点 C 入栈
1,接下来访问顶点C,它的邻居顶点为 [A, F, G],但是它们都已经被访问过了,所以该线路已经到达了尽头。这时候,我们就可以使栈出栈,来进行往上一层进行回溯。
2,通过1操作,我们就回到了顶点G,顶点G的邻居顶点为 [F, C],但是都已经访问过了,所以,我们需要使 G 出栈。
1,F的邻居都已经访问过了,接下来将F出栈。
2,现在我们回到了顶点E的位置,E的邻居H还没有访问过,所以将H入栈。
1,访问H顶点,H顶点没有下一层了,所以将H出栈。
2,顶点E的邻居顶点都被访问过了,下一步,将 E 出栈。
1,B和E的情况一样,所以将B出栈。
2,栈里就剩下A了,这样我们就回到了顶点A,顶点A的邻居 D还没有访问过,所以将D入栈。
1,访问顶点D,顶点D没有下一层,已经到最后面了,将D出栈,
2,我们再一次回到顶点A,A的邻居顶点都已经访问过了,将顶点A出栈,此时,栈已经空了,DFS遍历结束。
通过以上对顶点的遍历,我们可以构建出类似树的数据结构,DFS遍历形成的树会比较高一些。
实现
extension Graph where Element: Hashable {
func depthFirstSearch(from source: Vertex<Element>)
-> [Vertex<Element>] {
var stack: Stack<Vertex<Element>> = []
var pushed: Set<Vertex<Element>> = []
var visited: [Vertex<Element>] = []
stack.push(source)
pushed.insert(source)
visited.append(source)
// more to come ...
return visited
}
}
在以上代码中,我们定义了 depthFirstSearch(from:) 函数,传入一个顶点作为遍历的起点。并且,返回一个有序数组,来存放已经存放的顶点。在这里,我们使用了三种数据结构:
1,stack 用来存储我们遍历顶点的路径。
2,pushed 用来记录哪些顶点已经访问过了,防止同一个节点访问两次,我们使用Set 来确保 最快的查找速度。
3,visited 是一个数组,用来按顺序存储已访问的顶点。
接下来,在上面的代码部分完善该方法:
outer: while let vertex = stack.peek() { // 1
let neighbors = edges(from: vertex) // 2
guard !neighbors.isEmpty else { // 3
stack.pop()
continue
}
for edge in neighbors { // 4
if !pushed.contains(edge.destination) {
stack.push(edge.destination)
pushed.insert(edge.destination)
visited.append(edge.destination)
continue outer // 5
}
}
stack.pop() // 6
}
该方法做了以下操作:
1,当stack不为空时,我们需要一直检查栈顶的元素。并将该循环定义为 outer。
2,查找当前访问顶点的所有邻居节点。
3,如果当前节点没有邻居节点,出栈,继续访问下一个顶点。
4,对邻居顶点进行遍历,如果邻居顶点中含有未被访问的顶点,就将未访问的顶点入栈。
5,从邻居顶点中,找到了未访问过的顶点,然后继续 outer循环。
当 栈为空时,DFS算法就完成了。最后就是将有序存放顶点的数组返回。
最后附上本文的相关代码DataAndAlgorim