JavaScript数据结构 + leetcode - 图

268 阅读4分钟

本文总结数据结构图相关的知识和leetcode高频题。本文前半部分知识点总结和代码参考自JavaScript数据结构与算法

什么是图

  • 图是网络结构的抽象模型,是一组由连接的节点
  • 图可以表示任何二元关系,比如道路,航班。
  • JS中没有图,但是可以用Object和Array构建图。
  • 图的表示法: 邻接矩阵,邻接表,关联矩阵。
  • 图的深度优先遍历:尽可能深的搜索图的分支。
  • 图的广度优先遍历:先访问离根节点最近的节点

图的深度优先遍历

  • 访问根节点
  • 对根节点的没访问过的相邻节点挨个进行深度优先遍历
const graph = {
  0: [1, 2],
  1: [2],
  2: [0, 3],
  3: [3]
};

const visited = new Set();
const dfs = (n) => {
    visited.add(n);
    graph[n].forEach(c => {
        if (!visited.has(c)) {
            dfs(c);
        }
    });
}
dfs(2);  // 2 -> 0 -> 1 -> 3

图的广度优先遍历

  • 新建一个队列,把根节点入队
  • 把队头出队并访问
  • 把队头的没访问过的相邻节点入队
  • 重复第二,三步,直到队列为空
const visited = new Set();
const bfs = (root) => {
  visited.add(root);
  const q = [root];
  while (q.length) {
    const n = q.shift();
    console.log(n);
    graph[n].forEach(c => {
      if (!visited.has(c)) {
        q.push(c);
        visited.add(c);
      }
    })
  }
}
bfs(2);  // 2 -> 0 -> 3 -> 1

leetcode题目

417. 太平洋大西洋水流问题

思路:

  • 图的深度优先遍历

代码展示:

/**
 * @param {number[][]} heights
 * @return {number[][]}
 */
var pacificAtlantic = function(heights) {
  if (!heights || !heights[0]) return [];
  const m = heights.length;
  const n = heights[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] &&
        // 保证逆流而上
        heights[nr][nc] >= heights[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;
};

复杂度分析:

  • 时间复杂度:O(mn)。m代表矩阵的列,n代表矩阵的行。
  • 空间复杂度:O(mn)

133. 克隆图

方法一:深度优先遍历

/**
 * @param {Node} node
 * @return {Node}
 */
var 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);
};

方法二:广度优先遍历

/**
 * @param {Node} node
 * @return {Node}
 */
var cloneGraph = function(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);
};

复杂度分析:

  • 时间复杂度:O(n)。n代表图的节点数。
  • 空间复杂度:O(n)

207. 课程表

参考自「图解」拓扑排序 | 课程表问题

代码展示:

/**
 * @param {number} numCourses
 * @param {number[][]} prerequisites
 * @return {boolean}
 */
var canFinish = function(numCourses, prerequisites) {
  const inDegree = new Array(numCourses).fill(0); // 入度数组
  const map = {}; // 邻接表
  for (let i = 0; i < prerequisites.length; i++) {
    inDegree[prerequisites[i][0]]++; // 求课程的初始入度值
    if (map[prerequisites[i][1]]) {  // 当前课程是否存在于邻接表中
      map[prerequisites[i][1]].push(prerequisites[i][0]); // 添加依赖当前i课程的后续课
    } else { // 当前课程不存在于邻接表中
      map[prerequisites[i][1]] = [prerequisites[i][0]]; // 创建一个存放依赖的数组,并且将依赖课放进去
    }
  }
  const queue = [];  // 存放入度为0的课
  for (let i = 0; i < inDegree.length; i++) { // 所有入度为0的课入列
    if (inDegree[i] === 0) queue.push(i); // i 对应课,inDegree[i]是当前课的入度值
  }
  let count = 0;
  while (queue.length) {
    const n = queue.shift(); // 当前选的课出列
    count++;  // 选课数 +1
    const toEnQueue = map[n]; // 获取这门课对应的后续课
    toEnQueue && toEnQueue.length && toEnQueue.forEach(item => { // 如果确实有这门课
      inDegree[item]--; // 依赖当前课的后续课 -1
      if (inDegree[item] === 0) { // 一直减为0
        queue.push(item); // 将依赖课也push到队列中
      }
    });
  }
  return count === numCourses;  // 选了的课等于总课数,则返回true,否则返回false
};

复杂度分析:

  • 时间复杂度:O(m+n):m代表课程数,n代表先修课程的要求数。
  • 空间复杂度:O(m+n)

210. 课程表 II

代码参考自拓扑排序思路一步步形成,类BFS

代码展示:

/**
 * @param {number} numCourses
 * @param {number[][]} prerequisites
 * @return {number[]}
 */
var findOrder = function(numCourses, prerequisites) {
  const inDegree = new Array(numCourses).fill(0); // 入度数组
  const map = {}; // 邻接表
  for (let i = 0; i < prerequisites.length; i++) {
    inDegree[prerequisites[i][0]]++;     // 求课程的初始入度值
    if (map[prerequisites[i][1]]) {      // 当前课程是否存在于邻接表中
      map[prerequisites[i][1]].push(prerequisites[i][0]); // 添加依赖当前i课程的后续课
    } else {                             // 当前课程不存在于邻接表中
      map[prerequisites[i][1]] = [prerequisites[i][0]]; // 创建一个存放依赖的数组,并且将依赖课放进去
    }
  }
  const queue = []; // 存放入度为0的课
  for (let i = 0; i < inDegree.length; i++) {  // 所有入度为0的课入列
    if (inDegree[i] === 0) queue.push(i);  // i 对应课,inDegree[i]是当前课的入度值
  }
  const res = [];  // 存放结果数组
  while (queue.length) {
    const n = queue.shift(); // 当前选的课出列
    res.push(n);  // 将所有入度为0的课推入结果数组
    const toEnQueue = map[n]; // 获取这门课对应的后续课
    toEnQueue && toEnQueue.length && toEnQueue.forEach(item => { // 如果确实有这门课
      inDegree[item]--; // 依赖当前课的后续课 -1
      if (inDegree[item] === 0) { // 一直减为0
        queue.push(item); // 将依赖课也push到队列中
      }
    });
  }
  return res.length === numCourses ? res : [];  // 选了的课等于总课数,返回res,否则返回[]
};

复杂度分析:

  • 时间复杂度:O(m+n):m代表课程数,n代表先修课程的要求数。
  • 空间复杂度:O(m+n)

785. 判断二分图

参考自「手画图解」BFS 思路 | 判断二分图

代码展示:

/**
 * @param {number[][]} graph
 * @return {boolean}
 */
var isBipartite = function(graph) {
  const visited = new Array(graph.length);  // undefined为未染色,1为蓝色,-1为黄色
  for (let i = 0; i < visited.length; i++) {  // 遍历每个顶点
    if (visited[i]) continue;  // 如果这个顶点已上色,继续遍历
    const queue = [i];  // 队列初始推入顶点 i
    visited[i] = 1;  // 染为蓝色
    while (queue.length) { // 遍历顶点 i 所有相邻的顶点
      const cur = queue.shift();  // 考察出列的顶点
      const curColor = visited[cur];  // 出列顶点的颜色
      const neighborColor = -curColor;    // 它的相邻顶点应该有的颜色
      for (let i = 0; i < graph[cur].length; i++) {  // 给相邻节点都上色
        const neighbor = graph[cur][i];
        if (visited[neighbor] == undefined) { // 这个相邻节点没上色的话
          visited[neighbor] = neighborColor;  // 上色
          queue.push(neighbor);  // 推入队中
        } else if (visited[neighbor] != neighborColor) { // 上的颜色不对
          return false;
        }
      }
    }
  }
  return true;  // 遍历完所有节点,并没有发现错误的颜色
};

复杂度分析:

  • 时间复杂度:O(m+n):n是顶点个数,m是边数。
  • 空间复杂度:O(n)

1334. 阈值距离内邻居最少的城市

参考解法

代码展示:

/**
 * @param {number} n
 * @param {number[][]} edges
 * @param {number} distanceThreshold
 * @return {number}
 */
var findTheCity = function(n, edges, distanceThreshold) {
  const max = Number.MAX_SAFE_INTEGER;
  const distance = Array.from({length: n}, () => new Array(n).fill(max)); // 构造一个n*n的二维数组
  // console.log(distance);
  for (let i = 0; i < edges.length; i++) {
    distance[edges[i][0]][edges[i][1]] = distance[edges[i][1]][edges[i][0]] = edges[i][2];
  }
  for (let i = 0; i < n; ++i) {
    for (let j = 0; j < n; ++j) {
      if (i === j || distance[j][i] === max) continue;
      for (let k = j + 1; k < n; ++k) {
        distance[k][j] = distance[j][k] = Math.min(distance[j][k], distance[j][i] + distance[i][k]);
      }
    }
  }
  let city = 0;
  let minNum = n;
  for (let i = 0; i < n; ++i) {
    let curNum = 0;
    for (let j = 0; j < n; ++j) {
      distance[i][j] <= distanceThreshold && ++curNum;
    }
    if (curNum <= minNum) { minNum = curNum; city = i; }
  }
  return city;
};

总结:

图和树很类似,树是分层数据结构的抽象模型,图是网络结构的抽象模型。图的可能性会更多更复杂一些。