如何彻底搞懂深度优先搜索和广度优先搜索

让我们以一种简单易懂的方式来聊聊深度优先搜索(DFS)和广度优先搜索(BFS)这两种算法。

深度优先搜索(DFS)

想象一下,你在探索一座由许多房间组成的大宫殿。你进入了宫殿,目标是探索每一个房间。如果你采用深度优先搜索的策略,那你会这样做:

  1. 从入口开始,选择一个门进入下一个房间。

  2. 在新房间中,再次选择一个门进入下一个房间。

  3. 重复这个过程,直到你遇到一个没有更多新门(即没有未探索的连接房间)的房间,然后你会退回到上一个房间,寻找是否有未探索的门。

  4. 如果所有门都已经探索过,你会继续回退到更早的房间,寻找新的未探索的门。

  5. 重复这个过程,直到所有房间都被探索过。

简而言之,深度优先搜索就像是你试图通过探索一个方向上尽可能深的路径来探索整个宫殿。

广度优先搜索(BFS)

现在,假设你换了一种策略来探索同一座宫殿。这次,你采用的是广度优先搜索的方式,这意味着:

  1. 从入口开始,你先探索所有入口房间直接相连的房间。

  2. 然后,对于每一个你刚刚探索过的房间,你探索与它们直接相连的所有未被探索的房间。

  3. 重复这个过程,每次都在上一轮刚探索过的房间的基础上,向外扩展一层,探索更多的房间。

  4. 持续这个过程,直到所有房间都被探索过。

可以把广度优先搜索想象成一波波向外扩散的波浪,每一波都会探索离你开始点更远一层的房间。

代码示例

我们使用这两种算法来探索一个图(Graph),图是一种由节点(也称为顶点)和连接这些节点的边组成的数据结构。为了简单起见,我们假设图是无向的,并且以邻接列表的形式表示。

深度优先搜索(DFS)

使用递归实现,它将从一个指定的节点开始,探索尽可能深的分支,直到没有未访问的邻居为止,然后回溯。

// 图的邻接列表表示
const graph = {
  A: ['B', 'C'],
  B: ['A', 'D', 'E'],
  C: ['A', 'F'],
  D: ['B'],
  E: ['B', 'F'],
  F: ['C', 'E'],
}

function dfs(visited, graph, node) {
  if (!visited.includes(node)) {
    console.log(node)
    visited.push(node)
    const neighbors = graph[node]
    for (const neighbor of neighbors) {
      dfs(visited, graph, neighbor)
    }
  }
}

const visited = [] // 用来记录访问过的节点
dfs(visited, graph, 'A') // 从节点'A'开始深度优先搜索

在这个例子中,dfs 函数接受三个参数:visited 是一个数组,记录已经访问过的节点;graph 是图的邻接列表表示;node 是当前访问的节点。函数首先检查当前节点是否已经被访问过,如果没有,则记录并递归地对所有邻接的节点调用自身。

广度优先搜索(BFS)

使用队列来实现。从指定的起始节点开始,它先访问所有相邻的节点,然后对每一个已访问的节点,再访问其相邻节点,以此类推,直到所有节点都被访问。

// 图的邻接列表表示
const graph = {
  A: ['B', 'C'],
  B: ['A', 'D', 'E'],
  C: ['A', 'F'],
  D: ['B'],
  E: ['B', 'F'],
  F: ['C', 'E'],
}

function bfs(graph, start) {
  const visited = [start]
  const queue = [start]

  while (queue.length > 0) {
    const node = queue.shift() // 移除并返回第一个元素
    console.log(node)
    const neighbors = graph[node]
    for (const neighbor of neighbors) {
      if (!visited.includes(neighbor)) {
        visited.push(neighbor)
        queue.push(neighbor)
      }
    }
  }
}

bfs(graph, 'A') // 从节点'A'开始广度优先搜索

在这个 BFS 实现中,我们同样从一个指定节点开始。不同的是,我们使用一个队列来记录下一步需要访问的节点。对于每个节点,我们访问它的所有未访问过的邻居,并将它们加入队列中。这个过程重复进行,直到队列为空,即没有更多节点可以访问。

通过这两段代码,你可以看到深度优先搜索和广度优先搜索在探索图时的不同策略:DFS 倾向于沿着图的一条路径探索到底,而 BFS 则按层次一层层向外扩展。

区别与应用

深度优先搜索很适合于任务需要探索尽可能深的路径,比如解迷宫出口,或者是需要找到所有可能解的问题。

广度优先搜索则更适合于寻找最短路径或者是需要按照距离开始点逐层探索的情况。

想象深度优先搜索就像是一根针不断向前穿过布料,寻找边缘,而广度优先搜索则像是一滴墨水在纸上扩散,逐渐覆盖整张纸。