图的表示与 DFS / BFS

本节目标

图的两种表示:

// 运行环境:Node.js 14+
// 保存为 dsa-l15.js,执行:node dsa-l15.js

// 邻接表建无向图
function buildGraph(edges) {
  const g = new Map()
  const add = (a, b) => { if (!g.has(a)) g.set(a, []); g.get(a).push(b) }
  for (const [a, b] of edges) { add(a, b); add(b, a) } // 无向:双向
  return g
}

// DFS(递归)
function dfs(g, start, visited = new Set()) {
  visited.add(start)
  const order = [start]
  for (const nxt of (g.get(start) || [])) {
    if (!visited.has(nxt)) order.push(...dfs(g, nxt, visited))
  }
  return order
}

// BFS(队列)
function bfs(g, start) {
  const visited = new Set([start])
  const q = [start], order = []
  while (q.length) {
    const cur = q.shift()
    order.push(cur)
    for (const nxt of (g.get(cur) || [])) {
      if (!visited.has(nxt)) { visited.add(nxt); q.push(nxt) }
    }
  }
  return order
}

const g = buildGraph([['A','B'],['A','C'],['B','D']])
console.log('DFS', dfs(g, 'A')) // ['A','B','D','C'](路径依赖)
console.log('BFS', bfs(g, 'A')) // ['A','B','C','D'](逐层)

岛屿数量(网格图 = 隐式图,遇 '1' 就 DFS/BFS 淹没连通块):

function numIslands(grid) {
  const m = grid.length, n = grid[0].length
  let count = 0
  const sink = (i, j) => {
    if (i < 0 || j < 0 || i >= m || j >= n || grid[i][j] !== '1') return
    grid[i][j] = '0' // 淹没,防止重复访问
    sink(i+1,j); sink(i-1,j); sink(i,j+1); sink(i,j-1)
  }
  for (let i = 0; i < m; i++)
    for (let j = 0; j < n; j++)
      if (grid[i][j] === '1') { count++; sink(i, j) }
  return count
}

名词解释

课后练习

  1. 岛屿数量为什么 DFS 时要 grid[i][j]='0' 淹没?
    • 答案:防止同一块陆地被重复计数——访问过的 '1' 改成 '0',下次遇到就知道"已处理",等价于 visited 标记。
  2. 邻接表存无向图为什么边要加两次?
    • 答案:无向边 A-B 意味着"A 的邻居含 B"且"B 的邻居含 A",双向加入才能从任一侧走到另一侧。

总结

图是现实世界关系的最强建模工具——社交网络、地图导航、依赖关系,本质都是图。学图的第一步是选对"表示法":绝大多数工程场景是稀疏图,邻接表最实惠;只有需要频繁判断"两点是否直接相连"时才上邻接矩阵。图的遍历只有两种底色——DFS 和 BFS,它们和树的遍历同源,只是图有环、必须靠 visited 防死循环。岛屿数量是我最爱的图入门题:它把"网格"伪装成图,教你用 DFS/BFS"淹没连通块"来计数,思路一旦通了,什么"迷宫最短路""朋友圈"都是同一套路。我的体会是:图的题不怕难,先看"是连通性、最短路径、还是有环",再决定用并查集、BFS 还是 DFS——选对武器,图题就不可怕。