深度优先搜索和广度优先搜索

2019-01-05

1 深度优先搜索(DFS)

深度优先搜索,从初始访问结点出发,我们知道初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点。

深度优先搜索需要使用栈结构来存储访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点。总结起来可以这样说:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点

我们从这里可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。

算法步骤:

  1. 访问顶点v;
  2. 依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
  3. 若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

1.1 无向图的深度优先搜索

下面以”无向图”为例,来对深度优先搜索进行演示。
无向图结构
对上面的图G1进行深度优先遍历,从顶点A开始。
深度优先搜索

  • 第1步: 访问A。
  • 第2步: 访问(A的邻接点)C。
    • 在第1步访问A之后,接下来应该访问的是A的邻接点,即”C,D,F”中的一个。但在本文的实现中,顶点ABCDEFG是按照顺序存储,C在”D和F”的前面,因此,先访问C。
  • 第3步: 访问(C的邻接点)B。
    • 在第2步访问C之后,接下来应该访问C的邻接点,即”B和D”中一个(A已经被访问过,就不算在内)。而由于B在D之前,先访问B。
  • 第4步: 访问(C的邻接点)D。
    • 在第3步访问了C的邻接点B之后,B没有未被访问的邻接点;因此,返回到访问C的另一个邻接点D。
  • 第5步: 访问(A的邻接点)F。
    • 前面已经访问了A,并且访问完了”A的邻接点B的所有邻接点(包括递归的邻接点在内)”;因此,此时返回到访问A的另一个邻接点F。
  • 第6步: 访问(F的邻接点)G。
  • 第7步: 访问(G的邻接点)E。

因此访问顺序是:A -> C -> B -> D -> F -> G -> E

1.2 有向图的深度优先搜索

下面以”有向图”为例,来对深度优先搜索进行演示。
有向图结构
对上面的图G2进行深度优先遍历,从顶点A开始。
深度优先搜索

  • 第1步: 访问A。
  • 第2步: 访问B。
    • 在访问了A之后,接下来应该访问的是A的出边的另一个顶点,即顶点B。
  • 第3步: 访问C。
    • 在访问了B之后,接下来应该访问的是B的出边的另一个顶点,即顶点C,E,F。在本文实现的图中,顶点ABCDEFG按照顺序存储,因此先访问C。
  • 第4步: 访问E。
    • 接下来访问C的出边的另一个顶点,即顶点E。
  • 第5步: 访问D。
    • 接下来访问E的出边的另一个顶点,即顶点B,D。顶点B已经被访问过,因此访问顶点D。
  • 第6步: 访问F。
    • 接下应该回溯”访问A的出边的另一个顶点F”。
  • 第7步: 访问G。

因此访问顺序是:A -> B -> C -> E -> D -> F -> G

2 广度优先搜索(BFS)

类似于一个分层搜索的过程,广度优先搜索需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点。 算法步骤:

  1. 访问顶点v
  2. 依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。
  3. 如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。

2.1 无向图的广度优先搜索

下面以”无向图”为例,来对广度优先搜索进行演示。还是以上面的图G1为例进行说明。
广度优先搜索

  • 第1步: 访问A。
  • 第2步: 依次访问C,D,F。
    • 在访问了A之后,接下来访问A的邻接点。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,C在”D和F”的前面,因此,先访问C。再访问完C之后,再依次访问D,F。
  • 第3步: 依次访问B,G。
    • 在第2步访问完C,D,F之后,再依次访问它们的邻接点。首先访问C的邻接点B,再访问F的邻接点G。
  • 第4步: 访问E。
    • 在第3步访问完B,G之后,再依次访问它们的邻接点。只有G有邻接点E,因此访问G的邻接点E。

因此访问顺序是:A -> C -> D -> F -> B -> G -> E

2.2 有向图的广度优先搜索

下面以”有向图”为例,来对广度优先搜索进行演示。还是以上面的图G2为例进行说明。
广度优先搜索

  • 第1步: 访问A。
  • 第2步: 访问B。
  • 第3步: 依次访问C,E,F。
    • 在访问了B之后,接下来访问B的出边的另一个顶点,即C,E,F。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,因此会先访问C,再依次访问E,F。
  • 第4步: 依次访问D,G。
    • 在访问完C,E,F之后,再依次访问它们的出边的另一个顶点。还是按照C,E,F的顺序访问,C的已经全部访问过了,那么就只剩下E,F;先访问E的邻接点D,再访问F的邻接点G。

因此访问顺序是:A -> B -> C -> E -> F -> D -> G

3 实现

# 使用广度优先搜索和深度优先搜索遍历二叉树
# -*- coding: utf-8 -*-
class BinaryTree(object):
    def __init__(self, data):
        self.__key = data
        self.__leftChild = None
        self.__rightChild = None

    def getKey(self):
        return self.__key

    def getLeft(self):
        return self.__leftChild

    def setLeft(self, tree):
        self.__leftChild = tree

    def getRight(self):
        return self.__rightChild

    def setRight(self, tree):
        self.__rightChild = tree

    def insertLeft(self, data):
        if self.__leftChild == None:
            self.setLeft(BinaryTree(data))
        else:
            t = BinaryTree(data)
            t.setLeft(self.getLeft())
            self.setLeft(t)

    def insertRight(self, data):
        if self.__rightChild == None:
            self.setRight(BinaryTree(data))
        else:
            t = BinaryTree(data)
            self.setRight(self.getRight())
            self.setRight(t)

# 深度优先搜索
def DFS(root):
    if not root:
        return []
    nodelist = []  # 存储遍历的结果
    stack = []  # 模拟栈
    stack.append(root)
    # 当栈不为空时
    while stack:
        # 取出栈顶节点
        tmp = stack.pop()
        nodelist.append(tmp.getKey())
        # 因为栈是先入后出的,左节点要比右节点先访问,所以先把右节点入栈,再把左节点入栈
        if tmp.getRight():
            stack.append(tmp.getRight())
        if tmp.getLeft():
            stack.append(tmp.getLeft())
    return nodelist

# 广度优先搜索
def BFS(root):
    if root == None:
        return []
    queue = []  # 模拟队列
    nodelist = []  # 存储遍历的结果
    node = root
    queue.append(node)
    while queue:
        node = queue.pop(0)
        nodelist.append(node.getKey())
        if node.getLeft():
            queue.append(node.getLeft())
        if node.getRight():
            queue.append(node.getRight())
    return nodelist

4 总结

一般来说,能用DFS解决的问题,都能用BFS。

  • DFS多用于连通性问题因为其运行思想与人脑的思维很相似,故解决连通性问题更自然。
  • BFS多用于解决最短路问题,其运行过程中需要储存每一层的信息,所以其运行时需要储存的信息量较大,如果人脑也可储存大量信息的话,理论上人脑也可运行BFS。

总的来说多数情况下运行BFS所需的内存会大于DFS需要的内存(DFS一次访问一条路,BFS一次访问多条路),DFS容易爆栈(栈不易”控制”),BFS通过控制队列可以很好解决”爆队列”风险。

参考文献

图的遍历之 深度优先搜索和广度优先搜索
图的理解:深度优先和广度优先遍历及其 Java 实现
图的广度优先和深度优先遍历(BFS和DFS)
图的基础遍历:深度优先和广度优先