数据结构与算法之图(七)

620 阅读2分钟
  • 图是网络结构的抽象模型,是一组由连接的节点
  • 图可以表示任何二元关系,比如道路、航班....
  • JS没有图,但是可以利用 Object 和 Array 构建图
  • 图的表示法:邻接矩阵、关联矩阵、邻接表....
  • 图的常用操作:深度优先遍历、广度优先遍历

邻接矩阵:

邻接表:

1、深度优先遍历

准备好一个图结构:

const graph = {
  0: [1, 2],
  1: [2],
  2: [0, 3],
  3: [3]
}
  • 尽可能深的搜索图分支
  • 口诀
    • 访问根节点
    • 对根节点的没访问过的相邻节点挨个进行深度优先遍历
// 记录节点是否访问过
const visited = new Set();

const dfs = (n) => {
  console.log(n);
  visited.add(n);
	// 遍历相邻节点
  graph[n].forEach((item) => {
    // 没访问过才可以,进行递归访问
    if (!visited.has(item)) {
      dfs(item);
    }
  });
}

dfs(2);

打印结果:

2
0
1
3

2、广度优先遍历

  • 新建一个队列,把根节点入队
  • 把对头出队并访问
  • 把对头的没访问过的相邻节点入队
  • 重复第二、三步,直到队列为空
const visited = new Set();
// 新建一个队列, 根节点入队, 设2为根节点,并记录已访问
const q = [2];
visited.add(2);

while (q.length) {
  const n = q.shift();
  console.log(n);
  graph[n].forEach((item) => {
    if (!visited.has(item)) {
      q.push(item);
      visited.add(item);
    }
  });
}

打印结果:

2
0
3
1

3、LeetCode: 65.有效数字

  • 以0为起始位置,沿着图结构往下走
  • 能走到3、5、6即为有效数字

解题步骤:

  • 构建一个表示状态的图
  • 遍历字符串,并沿着图走,如果到了某个节点无路可走就返回 false
  • 遍历结束,如走到 3/5/6,就返回 true,否则返回 false
// 时间复杂度 O(n) n是字符串长度
// 空间复杂度 O(1) 

const isNumber = function (s) {
  const graph = {
    0: { blank: 0, sign: 1, ".": 2, digit: 6 },
    1: { digit: 6, ".": 2 },
    2: { digit: 3 },
    3: { digit: 3, e: 4 },
    4: { digit: 5, sign: 7 },
    5: { digit: 5 },
    6: { digit: 6, ".": 3, e: 4 },
    7: { digit: 5 },
  };

  // 记录状态
  let state = 0;
  for(let c of s.trim()) {
    // 转换字符
    if(c >= "0" && c <= "9") {
      c = "digit";
    }else if(c === " ") {
      c = "blank"
    }else if(c === "+" || c === "-") {
      c = "sign";
    }else if(c === "E" || c === "e") {
      c = "e";
    }
  	// 寻找图,没找到返回false
    state = graph[state][c];
    if(!state) return false;
  }
	// 返判断是否是合法的数字
  const validNumArr = [3, 5, 6];
  if(validNumArr.includes(state)) {
    return true;
  }
  return false;
};

4、LeetCode: 417. 太平洋大西洋水流问题

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

解题步骤:

  • 新建两个矩阵,分别记录能流到两个大洋的坐标
  • 从海岸线,多管齐下,同时深度优先遍历图,过程中填充上述矩阵
  • 遍历两个矩阵,找出能流到两个大洋的坐标
// 时间复杂度:O(m * n)
// 空间复杂度:O(m * n)

const pacificAtlantic = function(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 = (row, column, flow) => {
    flow[row][column] = true;
    [[row - 1, column], [row + 1, column], [row, column -1], [row, column + 1]].forEach(([nr, nc]) => {
      if(
          // 保证矩阵中
          nr >= 0 && nr < m &&
          nc >= 0 && nc < n &&
          // 防止死循环
          !flow[nr][nc] &&
          matrix[nr][nc] >= matrix[row][column]
        ) {
          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;
};

5、LeetCode: 133.克隆图

解题思路:

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

解题步骤:

  • 深度或广度优先遍历所有节点
  • 拷贝所有的节点,存储起来
  • 将拷贝的节点,按照原图的连接方法进行连接
// 时间复杂度:O(n) n为图的节点数
// 空间复杂度:O(n) 有一个队列

/**
 * // Definition for a Node.
 * function Node(val, neighbors) {
 *    this.val = val === undefined ? 0 : val;
 *    this.neighbors = neighbors === undefined ? [] : neighbors;
 * };
 */

const cloneGraph = function(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);
};