⑨ 数据结构之“图”

一、理论

1. 图简介

  • 图是 网络结构 的抽象模型,是一组由 连接的 节点

  • 图可以表示任何二元关系

  • js中没有图,但可以用Object和Array构建图

  • 图的表示法:邻接矩阵、邻接表、关联矩阵...

2. 图的表示法

2.1 邻接矩阵

2.2 邻接表

const graph = {
  0: [1, 2],
  1: [2],
  2: [0, 3],
  3: [3]
}

3. 图的常用操作

  • 深度优先遍历
  • 广度优先遍历

3.1 深度优先遍历

3.1.1 什么是深度优先遍历
  • 尽可能深的搜索图的分支
3.1.2 深度优先遍历算法口诀
  • 访问根节点
  • 对根节点的 没访问过的相邻节点 挨个进行深度优先遍历
3.1.3 coding part
const visited = new Set()
const dfs = n => {
  console.log(n);
  visited.add(n)
  graph[n].forEach(c => {
    if(!visited.has(c)) {
      dfs(c)
    }
  })
}
dfs(2)

3.2 广度优先遍历

3.1.1 什么是广度优先遍历
  • 先访问离根节点最近的节点
3.1.2 广度优先遍历算法口诀
  • 新建一个队列,把根节点入队
  • 把队头出队并访问
  • 把队头的 没访问过的相邻节点 入队
  • 重复第二三步,直到队列为空
3.1.3 coding part
const visited = new Set()
const q = [2]
visited.add(2)
while(q.length) {
  const n = q.shift()
  console.log(n)
  graph[n].forEach(c => {
    if(!visited.has(c)) {
      q.push(c)
      visited.add(c)
    }
  })
}

二、刷题

1. 有效的数字(65)

1.1 题目描述

  • 给你一个字符串 s ,如果 s 是一个 有效数字 ,请返回 true

  • 有效数字(按顺序)可以分成以下几个部分:

    1. 一个 小数 或者 整数
    2. (可选)一个 'e' 或 'E' ,后面跟着一个 整数
  • 小数(按顺序)可以分成以下几个部分:

    1. (可选)一个符号字符('+' 或 '-')
    2. 下述格式之一:
      1. 至少一位数字,后面跟着一个点 '.'
      2. 至少一位数字,后面跟着一个点 '.' ,后面再跟着至少一位数字
      3. 一个点 '.' ,后面跟着至少一位数字
  • 整数(按顺序)可以分成以下几个部分:

    1. (可选)一个符号字符('+' 或 '-')
    2. 至少一位数字

1.2 解题思路

1.3 解题步骤

  • 构建一个表示状态的图
  • 遍历字符串,并沿着图走,如果到了某节点无路可走则返回false
  • 遍历结束,如走到3/5/6,则返回true,否则返回false
function isNumber(s) {
  const graph = {
    0: { 'blank': 0, 'sign': 1, '.': 2, 'digit': 6 },
    1: { '.': 2, 'digit': 6 },
    2: { 'digit': 3 },
    3: { 'digit': 3, 'e': 4 },
    4: { 'digit': 5, 'sign': 7 },
    5: { 'digit': 5 },
    6: { '.': 3, 'e': 4, 'digit': 6 },
    7: { 'digit': 5 }
  }
  let state = 0
  for(c of s.trim()) {
    if(c >= '0' && c <= '9') {
      c = 'digit'
    } else if(c === '') {
      c = 'blank'
    } else if(c === '+' || c === '-') {
      c = 'sign'
    }
    state = graph[state][c]
    if(state === undefined) {
      return false
    }
  }
  if(state === 3 || state === 5 || state === 6) {
    return true
  }
  return false
}

1.4 时间复杂度&空间复杂度

  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

2. 太平洋大西洋水流问题(417)

2.1 题目描述

  • 给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界
  • 规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动
  • 请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标
  • 提示:
    • 输出坐标的顺序不重要
    • m 和 n 都小于150

2.2 解题思路

  • 把矩阵想象成图
  • 从海岸线逆流而上遍历图,所到之处就是可以流到某大洋的坐标

2.3 解题步骤

  • 新建两个矩阵,分别记录能流到两个大洋的坐标
  • 从海岸线,同时深度优先遍历图,过程中填充上述矩阵
  • 遍历两个矩阵,找出能流到两大洋的坐标
function pacificAtlantic(matrix) {
  if(!matrix || !matrix[0]) return []
  const m = matrix.length
  const n = matrix[0].length
  const flow1 = Array.from({ length: m }, () => new Array(n).fill(false))
  const flow2 = Array.from({ length: m }, () => new Array(n).fill(false))
  // 深度优先遍历
  const dfs = (r, c, flow) => {
    flow[r][c] = true
    [[r-1, c], [r+1, c], [r, c-1], [r, c+1]].forEach(([nr, nc]) => {
      if(
        // 保证下一个节点在矩阵中
        nr  >= 0 && nr < m && nc >= 0 && nc < n &&
        // 保证下一个节点还未访问 -- 防止死循环 
        !flow[nr][nc] &&
        // 保证逆流而上
        matrix[nr][nc] >= matrix[r][c]
      ) {
        dfs(nr, nc, flow)
      }
    })
  }
  // 沿着海岸线逆流而上
  for(let r = 0; r < m; r++) {
    // 第一列
    dfs(r, 0, flow1) // 流到太平洋
    // 最后一列
    dfs(r, n-1, flow2) // 流到大西洋
  }
  for(let c = 0; c < n; c++) {
    // 第一行
    dfs(0, c, flow1)
    // 最后一行
    dfs(m-1, c, flow2)
  }
  // 收集能流到两个大洋的坐标
  const res = []
  for(let r = 0; r < m; r++) {
    for(let c = 0; c < n; c++) {
      if(flow1[r][c] && flow2[r][c]) {
        res.push([r, c])
      }
    }
  }
  return res
}

2.4 时间复杂度&空间复杂度

  • 时间复杂度: O(m*n)
  • 空间复杂度: O(m*n)

3. 克隆图(133)

3.1 题目描述

  • 给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)
  • 图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])
  • 每个节点的值都和它的索引相同
function Node(val, neighbors) {
  this.val = val === undefined ? 0 : val;
  this.neighbors = neighbors === undefined ? [] : neighbors;
}

3.2 解题思路

输入:adjList = [[2,4],[1,3],[2,4],[1,3]]
输出:[[2,4],[1,3],[2,4],[1,3]]

  • 拷贝所有节点
  • 拷贝所有边

3.3 解题步骤

  • 深度/广度优先遍历所有节点
  • 拷贝所有节点存储起来
  • 将拷贝的节点按照原图的连接方法进行连接
// 深度优先遍历
function cloneGraph(node) {
  if(!node) return;
  const visited = new Map();
  const dfs = n => {
    const nCopy = new Node(n.val);
    visited.set(n, nCopy);
    (n.neighbors || []).forEach(ne => {
      if(!visited.has(ne)) {
        dfs(ne);
      }
      nCopy.neighbors.push(visited.get(ne));
    }) 
  };
  dfs(node);
  return visited.get(node);
}
// 广度优先遍历
function cloneGraph(node) {
  if(!node) return;
  const visited = new Map();
  const q = [node];
  visited.set(node, new Node(node.val));
  while(q.length) {
    const n = q.shift();
    (n.neighbors || []).forEach(ne => {
      if(!visited.has(ne)) {
        q.push(ne)
        visited.set(ne, new Node(ne.val));
      }
      visited.get(n).neighbors.push(visited.get(ne))
    })
  }
  return visited.get(node);
}

3.4 时间复杂度&空间复杂度

深度优先遍历
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)
广度优先遍历
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

三、总结 -- 技术要点

  • 图是 网络结构 的抽象模型,是一组由 连接的 节点
  • 图可以表示任何二元关系,比如道路、航班...
  • js中没有图,但可以用Object和Array构建图
  • 图的表示法:邻接矩阵、邻接表、关联矩阵...
  • 图的常用操作:深度/广度优先遍历
posted on 2022-01-26 14:37  pleaseAnswer  阅读(57)  评论(0编辑  收藏  举报