搜索既要广度也要深度

223 阅读22分钟

深度搜索

1.1 深度搜索

深度搜索其实很简单,而且也特别通俗易懂,用俗话说就是一条道走到黑,撞到南墙之后,再往回看看,重新选择一条路走到黑。不断的重复这个过程。

深度搜索是首先走一个节点,然后把该节点能都走到的位置push进入一个栈,然后从栈中拿出一个节点,接着走该节点能走到的所有位置,再全部push进入栈。如此重复

130. 被围绕的区域

题目描述

给定一个二维的矩阵,包含 'X' 和 'O'(字母 O)。

找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。

例子1

Input:
X X X X X O O X X X O X X O X X

output: X X X X X X X X X X X X X O X X

解释: 被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

思考

很简单的深度搜索就可以就可以解决,从四周搜索,如果不是和四周连接在一起转为“X”就可以了

实现1

/**
 * @param {character[][]} board
 * @return {void} Do not return anything, modify board in-place instead.
 */
const dirs = [
  [-1, 0],
  [0, 1],
  [1, 0],
  [0, -1],
];
const gfs = (board, i, j, m, n) => {
  board[i][j] = -1;
  for (let k = 0; k < 4; k++) {
    const nextI = i + dirs[k][0];
    const nextJ = j + dirs[k][1];
    if (nextI >= 0 && nextI < m && nextJ >= 0 && nextJ < n && board[nextI][nextJ] === "O") {
      gfs(board, nextI, nextJ, m, n);
    }
  }
};
// Runtime: 96 ms, faster than 84.79% of JavaScript online submissions for Surrounded Regions.
// Memory Usage: 42.2 MB, less than 87.06% of JavaScript online submissions for Surrounded Regions.
export default (board) => {
  const m = board.length;

  if (m === 0) return board;
  const n = board[0].length;

  for (let i = 0; i < n; i++) {
    if (board[0][i] === "O") {
      gfs(board, 0, i, m, n);
    }
    if (board[m - 1][i] === "O") {
      gfs(board, m - 1, i, m, n);
    }
  }
  for (let i = 1; i < m - 1; i++) {
    if (board[i][0] === "O") {
      gfs(board, i, 0, m, n);
    }
    if (board[i][n - 1] === "O") {
      gfs(board, i, n - 1, m, n);
    }
  }
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (board[i][j] === -1) {
        board[i][j] = "O";
      } else if (board[i][j] === "O") {
        board[i][j] = "X";
      }
    }
  }
  return board;
};

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

695. 岛屿的最大面积

题目描述

给定一个包含了一些 0 和 1 的非空二维数组 grid 。

一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)

例子1

Input:
[[0,0,1,0,0,0,0,1,0,0,0,0,0], [0,0,0,0,0,0,0,1,1,1,0,0,0], [0,1,1,0,1,0,0,0,0,0,0,0,0], [0,1,0,0,1,1,0,0,1,0,1,0,0], [0,1,0,0,1,1,0,0,1,1,1,0,0], [0,0,0,0,0,0,0,0,0,0,1,0,0], [0,0,0,0,0,0,0,1,1,1,0,0,0], [0,0,0,0,0,0,0,1,1,0,0,0,0]]
对于上面这个给定矩阵应返回 6。注意答案不应该是 11 ,因为岛屿只能包含水平或垂直的四个方向的 1 。

例子2
Input:
[[0,0,0,0,0,0,0,0]]
对于上面这个给定的矩阵, 返回 0。

注意: 给定的矩阵grid 的长度和宽度都不超过 50。

思考 1

这是典型的深度搜索遍历,按照正常的搜索就可以了。

深度搜索一般可以使用栈或者递归,这两个差不多,只要抓住深度搜索的重点就可以

重点是深度搜索首先遍历当前节点可以遍历到的所有节点,然后再从里边取出一个节点,继续遍历可以遍历到的所有节点

递归写法可以看下实现2

实现1

/**
 * @param {number[][]} grid
 * @return {number}
 */
const dirs = [
  [-1, 0],
  [0, 1],
  [1, 0],
  [0, -1],
];

export default (grid) => {
  const visited = [];
  const m = grid.length;
  const n = grid[0].length;
  let maxArea = 0;
  let x = 0;
  let y = 0;
  let cur_maxArea = 0;
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (grid[i][j] === 1) {
        cur_maxArea = 1;
        grid[i][j] = 0;
        const island = [];
        island.push([i, j]);
        while (island.length > 0) {
          const [curI, curJ] = island.pop();
          for (let k = 0; k < 4; k++) {
            x = curI + dirs[k][0];
            y = curJ + dirs[k][1];
            if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] === 1) {
              grid[x][y] = 0;
              cur_maxArea++;
              island.push([x, y]);
            }
          }
        }
        maxArea = Math.max(maxArea, cur_maxArea);
      }
    }
  }
  return maxArea;
};

实现2

const dirs = [
  [-1, 0],
  [0, 1],
  [1, 0],
  [0, -1],
];
// 主函数
export default (grid) => {
  if (grid.length === 0 || grid[0].length === 0) {
    return 0;
  }
  let max_area = 0;
  for (let i = 0; i < grid.length; ++i) {
    for (let j = 0; j < grid[0].length; ++j) {
      if (grid[i][j] == 1) {
        max_area = Math.max(max_area, dfs(grid, i, j));
      }
    }
  }
  return max_area;
};
// 辅函数
const dfs = (grid, curI, curJ) => {
  grid[curI][curJ] = 0;
  let nextI;
  let nextJ;
  let area = 1;
  for (let i = 0; i < 4; ++i) {
    nextI = curI + dirs[i][0];
    nextJ = curJ + dirs[i][1];
    if (nextI >= 0 && nextI < grid.length && nextJ >= 0 && nextJ < grid[0].length && grid[nextI][nextJ] === 1) {
      area += dfs(grid, nextI, nextJ);
    }
  }
  return area;
};

实现1算法时间复杂度 O(n^2), 空间复杂度 O(1)
实现2算法时间复杂度 O(n^2), 空间复杂度 O(1)

547. 朋友圈

题目描述

班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。

给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

例子1

Input:
[[1,1,0], [1,1,0], [0,0,1]]
output:2
已知学生 0 和学生 1 互为朋友,他们在一个朋友圈。 第2个学生自己在一个朋友圈。所以返回 2 。

例子2
Input:
[[1,1,0], [1,1,1], [0,1,1]]
output:1
已知学生 0 和学生 1 互为朋友,学生 1 和学生 2 互为朋友,所以学生 0 和学生 2 也是朋友,所以他们三个在一个朋友圈,返回 1 。

提示:

1 1 <= N <= 200
2 M[i][i] == 1
3 M[i][j] == M[j][i]

思考 1

这里和695差不多,只不过在搜索过程中,当搜到一个1的时候,我们就相当于发现了一个朋友圈,一个人自己和自己也是一个朋友圈,当我们发现了一个朋友圈之后,我们就深度搜索这个朋友圈,直到搜索完整个朋友圈。
可以实现1

这个时候可以换个思路,因为只有m个人,那肯定最多只能有m个朋友圈,我们可以挨着朋友圈去深度遍历,发现一个朋友圈,深度遍历在该朋友圈以内的。

可以看下实现2

实现1

/**
 * @param {number[][]} M
 * @return {number}
 */

const dirs = [
  [-1, 0],
  [0, 1],
  [1, 0],
  [0, -1],
];
const dfs = (M, curI, curJ) => {
  M[curI][curJ] = 0;
  for (let i = 0; i < M[0].length; i++) {
    const nextJ = i;
    const nextI = curI;
    if (nextI >= 0 && nextI < M.length && nextJ >= 0 && nextJ < M[0].length && M[nextI][nextJ] === 1) {
      dfs(M, nextI, nextJ);
    }
  }
  for (let i = 0; i < M.length; i++) {
    const nextI = i;
    const nextJ = curI;
    if (nextI >= 0 && nextI < M.length && nextJ >= 0 && nextJ < M[0].length && M[nextI][nextJ] === 1) {
      dfs(M, nextI, nextJ);
    }
  }

  for (let i = 0; i < M.length; i++) {
    const nextI = curJ;
    const nextJ = i;
    if (nextI >= 0 && nextI < M.length && nextJ >= 0 && nextJ < M[0].length && M[nextI][nextJ] === 1) {
      dfs(M, nextI, nextJ);
    }
  }
  for (let i = 0; i < M.length; i++) {
    const nextI = i;
    const nextJ = curJ;
    if (nextI >= 0 && nextI < M.length && nextJ >= 0 && nextJ < M[0].length && M[nextI][nextJ] === 1) {
      dfs(M, nextI, nextJ);
    }
  }
};
export default (M) => {
  if (M.length === 1) return 1;
  const m = M.length;
  const n = M[0].length;
  let res = 0;
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (M[i][j] === 1) {
        dfs(M, i, j);
        res++;
      }
    }
  }
  return res;
};

实现2

/**
 * @param {number[][]} M
 * @return {number}
 */
const dfs = (M, i, circles) => {
  // 已经访问过了
  circles[i] = 1;
  // 遍历和自己有关系的朋友
  for (let j = 0; j < M.length; j++) {
    if (circles[j] === 0 && M[i][j] === 1 && j !== i) {
      dfs(M, j, circles);
    }
  }
};
// Runtime: 84 ms, faster than 86.26% of JavaScript online submissions for Friend Circles.
// Memory Usage: 40.2 MB, less than 94.94% of JavaScript online submissions for Friend Circles.
export default (M) => {
  if (M.length === 1) return 1;
  const m = M.length;
  const n = M[0].length;
  let res = 0;
  const circles = new Array(m).fill(0);
  // 最多一共有m个朋友圈
  for (let i = 0; i < m; i++) {
    if (circles[i] === 0) {
      dfs(M, i, circles);
      res++;
    }
  }
  return res;
};

实现1算法时间复杂度 O(n^2),因为这里最多遍历n^2个节点, 空间复杂度 O(1)
实现2算法时间复杂度 O(n^2), 空间复杂度 O(1)

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

题目描述

给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。

规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。

请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。


提示:

1 输出坐标的顺序不重要
2 m 和 n 都小于150


例子1

Input:
给定下面的 5x5 矩阵:

太平洋 ~ ~ ~ ~ ~ ~ 1 2 2 3 (5) * ~ 3 2 3 (4) (4) * ~ 2 4 (5) 3 1 * ~ (6) (7) 1 4 5 * ~ (5) 1 1 2 4 * * * * * * 大西洋


output:[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).

思考 1

最简单的想法肯定是遍历每个节点,然后看下每个节点是否能够分别到达大西洋和太平洋,但是这里有个问题,超时了。
可以实现1

这里有另外一种方法,就是逆向思维,让水往里边灌,如果一个节点,太平洋的水可以到达,大西洋的水也可以到达,那么肯定可以实现。

这里其实以前做过一道类似的,也是通过水往里边灌水,但是在这里的时候,仍然没有考虑到。

可以看下实现2

实现1

/**
 * @param {number[][]} matrix
 * @return {number[][]}
 */
const dirs = [
  [-1, 0],
  [0, 1],
  [1, 0],
  [0, -1],
];
// 辅函数
const dfs = (matrix, curI, curJ, visited, hadEnd) => {
  if (curI === 0 || curJ === 0) {
    hadEnd[0] = true;
  }
  if (curI === matrix.length - 1 || curJ === matrix[0].length - 1) {
    hadEnd[1] = true;
  }
  let nextI;
  let nextJ;
  visited[curI][curJ] = 1;
  for (let i = 0; i < 4; ++i) {
    nextI = curI + dirs[i][0];
    nextJ = curJ + dirs[i][1];
    if (nextI >= 0 && nextI < matrix.length && nextJ >= 0 && nextJ < matrix[0].length) {
      if (matrix[nextI][nextJ] <= matrix[curI][curJ] && visited[nextI][nextJ] === 0) {
        dfs(matrix, nextI, nextJ, visited, hadEnd);
      }
    }
  }
  visited[curI][curJ] = 0;
};
// 主函数
export default (matrix) => {
  if (matrix.length === 0 || matrix[0].length === 0) {
    return [];
  }
  const visited = [];
  const m = matrix.length;
  const n = matrix[0].length;
  for (let i = 0; i < m; i++) {
    visited[i] = new Array(n).fill(0);
  }

  let res = [];
  for (let i = 0; i < m; ++i) {
    for (let j = 0; j < n; ++j) {
      // hadEnd[0] 表示到达Pacti,hadEnd[0] 表示到达Anti
      let hadEnd = [false, false];
      dfs(matrix, i, j, visited, hadEnd);
      // 如果都到达,则加入
      if (hadEnd[0] && hadEnd[1]) {
        res.push([i, j]);
      }
    }
  }
  return res;
};


实现2

/**
 * @param {number[][]} matrix
 * @return {number[][]}
 */
const dirs = [
  [-1, 0],
  [0, 1],
  [1, 0],
  [0, -1],
];
// 辅函数
const dfs = (matrix, curI, curJ, reach) => {
  let nextI;
  let nextJ;
  if (reach[curI][curJ]) {
    return;
  }
  reach[curI][curJ] = 1;
  for (let i = 0; i < 4; i++) {
    nextI = curI + dirs[i][0];
    nextJ = curJ + dirs[i][1];
    if (nextI >= 0 && nextI < matrix.length && nextJ >= 0 && nextJ < matrix[0].length) {
      if (matrix[nextI][nextJ] >= matrix[curI][curJ] && reach[nextI][nextJ] === 0) {
        dfs(matrix, nextI, nextJ, reach);
      }
    }
  }
};

// Runtime: 112 ms, faster than 94.23% of JavaScript online submissions for Pacific Atlantic Water Flow.
// Memory Usage: 46.9 MB, less than 64.74% of JavaScript online submissions for Pacific Atlantic Water Flow.
export default (matrix) => {
  if (matrix.length === 0 || matrix[0].length === 0) {
    return [];
  }
  const m = matrix.length;
  const n = matrix[0].length;
  const canReachPacific = [];
  const canReachAtlantic = [];
  for (let i = 0; i < m; i++) {
    canReachPacific[i] = new Array(n).fill(0);
    canReachAtlantic[i] = new Array(n).fill(0);
  }
  let res = [];
  for (let i = 0; i < m; i++) {
    dfs(matrix, i, 0, canReachPacific);
  }
  for (let i = 0; i < n; i++) {
    dfs(matrix, 0, i, canReachPacific);
  }
  for (let i = 0; i < m; i++) {
    dfs(matrix, i, n - 1, canReachAtlantic);
  }
  for (let i = 0; i < n; i++) {
    dfs(matrix, m - 1, i, canReachAtlantic);
  }
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (canReachPacific[i][j] === 1 && canReachAtlantic[i][j] === 1) {
        res.push([i, j]);
      }
    }
  }
  return res;
};

实现1算法时间复杂度 O (mn), 因为这里最多遍历 mn个节点4次, 空间复杂度 O(1)

实现2算法时间复杂度 O (mn), 因为这里最多遍历 mn个节点4次, 空间复杂度 O(1)

回溯法

回溯法(backtracking)是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状 态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。

回溯其实也很简单,其实和普通的深度搜索没什么区别,就是发现不合适可以退出

46. 全排列

题目描述

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

例子1

input:[1,2,3]
output:[ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

例子2

input:[0,1]
output:[[0,1],[1,0]]

例子1

input:[1]
output:[[1]]

提示:

1 1 <= nums.length <= 6
2 -10 <= nums[i] <= 10
3 All the integers of nums are unique.

思考1

简单的回溯法,

怎样输出所有的排列方式呢?对于每一个当前位置 i,我们可以将其于之后的任意位置交换, 然后继续处理位置 i+1,直到处理到最后一位。

实现1

const swap = (nums, i, j) => {
  const temp = nums[i];
  nums[i] = nums[j];
  nums[j] = temp;
};
// count 遍历的次数
const backtracking = (nums, count, res) => {
  if (count === nums.length - 1) {
    res.push(nums);
  }

  for (let i = count; i < nums.length; i++) {
    // 修改状态
    if (i !== count) {
      swap(nums, i, count);
    }
    backtracking([...nums], count + 1, res);
    // 恢复状态
    if (i !== count) {
      swap(nums, count, i);
    }
  }
};
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
export default (nums) => {
  if (nums.length === 1) {
    return [[nums[0]]];
  }
  let res = [];
  backtracking(nums, 0, res);
  return res;
};

时间复杂度0(n^2(n)!), 空间复杂度是O(n^3)

47. 全排列II

题目描述

给定一个 没有含有重复数字的序列,返回其所有可能的全排列。

例子1

input:[1,2,3]
output:[ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

例子2

input:[1,1,2]
output: [[1,1,2], [1,2,1], [2,1,1]]

例子1

input:[1]
output:[[1]]

提示:

1 1 <= nums.length <= 8
2 -10 <= nums[i] <= 10

思考1

1 可以使用46的解法,不过结果会有重复的,可以在结果中删除掉重复的。

可以看下实现1

2 另外这里主要麻烦在会有重复的元素,怎么解决掉重复的元素才是重点?

这里举个测试用例,比如[1,2,2], 可以模拟一下,为什么会出现重复的结果?

解决办法就是如何去防止重复的结果出现

通过[1,2,2]的测试用例,可以发现重复的原因就是2和2之间重复了
所以这里可以加一个used 数组,用来标记数组中那些数被使用了
每次从数组中取出一个数来,如果发现使用过或者是重复的数字,但是前面的重复的数字还没使用过,则跳出,否则使用回溯法解决就可以了。

可以看下实现2

实现1

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
const swap = (nums, i, j) => {
  const temp = nums[j];
  nums[j] = nums[i];
  nums[i] = temp;
};
const backTracking = (nums, level, res) => {
  if (level === nums.length - 1) {
    for (let k = 0; k < res.length; k++) {
      if (res[k].join("") === nums.join("")) {
        return;
      }
    }
    res.push(nums);
    return;
  }
  for (let i = level; i < nums.length; i++) {
    if (nums[level] === nums[i] && i !== level) {
      continue;
    }
    if (level !== i) {
      swap(nums, i, level);
    }
    backTracking([...nums], level + 1, res);
    if (i !== level) {
      swap(nums, level, i);
    }
  }
};
// Runtime: 4384 ms, faster than 5.01% of JavaScript online submissions for Permutations II.
// Memory Usage: 45.7 MB, less than 21.99% of JavaScript online submissions for Permutations II.
export default (nums) => {
  if (nums.length === 1) return [nums];
  const res = [];
  nums.sort((a, b) => a - b);
  backTracking(nums, 0, res);
  return res;
};


实现2

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
const swap = (nums, i, j) => {
  const temp = nums[j];
  nums[j] = nums[i];
  nums[i] = temp;
};
const backTracking = (nums, level, res) => {
  if (level === nums.length - 1) {
    for (let k = 0; k < res.length; k++) {
      if (res[k].join("") === nums.join("")) {
        return;
      }
    }
    res.push(nums);
    return;
  }
  for (let i = level; i < nums.length; i++) {
    if (nums[level] === nums[i] && i !== level) {
      continue;
    }
    if (level !== i) {
      swap(nums, i, level);
    }
    backTracking([...nums], level + 1, res);
    if (i !== level) {
      swap(nums, level, i);
    }
  }
};
// Runtime: 4384 ms, faster than 5.01% of JavaScript online submissions for Permutations II.
// Memory Usage: 45.7 MB, less than 21.99% of JavaScript online submissions for Permutations II.
export default (nums) => {
  if (nums.length === 1) return [nums];
  const res = [];
  nums.sort((a, b) => a - b);
  backTracking(nums, 0, res);
  return res;
};

77. 组合

题目描述

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

例子1

input:n = 4, k = 2
output:[ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]

思考1

简单的回溯法,

回溯法掌握三个点就可以了

1 修改状态
2 进入递归
3 恢复状态

实现1

/**
 * @param {number} n
 * @param {number} k
 * @return {number[][]}
 */
const backTrack = (nums, index, k, loopRes, res) => {
  if (loopRes.length === k) {
    res.push(loopRes);
    return;
  }
  for (let i = index; i < nums.length; i++) {
    loopRes.push(nums[i]);
    backTrack(nums, i + 1, k, [...loopRes], res);
    loopRes.pop(nums[i]);
  }
};
export default (n, k) => {
  const res = [];
  const loopRes = [];
  const nums = [];
  for (let i = 1; i <= n; i++) {
    nums.push(i);
  }
  backTrack(nums, 0, k, [], res);
  return res;
};

40. 组合总和 II

题目描述

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。


例子1

input:candidates = [10,1,2,7,6,1,5], target = 8,
output:[ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]

例子2

input:candidates = [2,5,2,1,2], target = 5,
output:[ [1,2,2], [5] ]

思考1

1 简单的回溯法,

这里主要是考虑如何处理重复元素,不导致重复的结果,当然可以采用前面的,对所有的结果进行筛选,排除重复的,或者在遍历的时候,把重复的过滤掉,也就是再遍历的时候,跳过重复的数字。
可以看下实现1,

2 另外这里应该也可以采用前面47采用的使用一个used数组标记是否遍历过,来解决重复数字的问题。

参考实现2

实现1

/**
 * @param {number[]} candidates
 * @param {number} target
 * @return {number[][]}
 */

// Runtime: 100 ms, faster than 38.63% of JavaScript online submissions for Combination Sum II.
// Memory Usage: 44.6 MB, less than 22.16% of JavaScript online submissions for Combination Sum II.
const dfs = (candidates, level, target, singleRes, res) => {
  let sum = 0;
  if (singleRes.length > 0) {
    sum = singleRes.reduce((a, b) => a + b);
  }
  if (sum === target) {
    res.push(singleRes);
    return;
  } else if (sum > target) {
    return;
  }
  for (let i = level; i < candidates.length; ) {
    singleRes.push(candidates[i]);
    dfs(candidates, i + 1, target, [...singleRes], res);
    singleRes.pop(candidates[i]);
    if (candidates[i + 1] === candidates[i]) {
      while (candidates[i + 1] === candidates[i]) {
        i++;
      }
      i++;
    } else {
      i++;
    }
  }
};
export default (candidates, target) => {
  candidates.sort((a, b) => a - b);
  const res = [];
  dfs(candidates, 0, target, [], res);
  return res;
};

实现2

/**
 * @param {number[]} candidates
 * @return {number[][]}
 */
const backTracking = (candidates, used, list, level, target, res) => {
  let sum = 0;
  if (list.length > 0) {
    sum = list.reduce((a, b) => a + b);
  }
  if (sum === target) {
    res.push([...list]);
    return;
  } else if (sum > target) {
    return;
  }
  for (let i = level; i < candidates.length; i++) {
    if (used[i]) {
      continue;
    }
    // 去除掉重复,防止重复
    if (i > 0 && candidates[i - 1] === candidates[i] && !used[i - 1]) {
      continue;
    }
    used[i] = 1;
    list.push(candidates[i]);
    backTracking(candidates, used, list, i + 1, target, res);
    used[i] = 0;
    list.pop();
  }
};

// Runtime: 92 ms, faster than 68.43% of JavaScript online submissions for Combination Sum II.
// Memory Usage: 40.5 MB, less than 54.71% of JavaScript online submissions for Combination Sum II.
export default (candidates, target) => {
  if (!candidates || candidates.length === 0) return [];
  const res = [];
  // 是否被使用过
  const used = new Array(candidates.length).fill(0);
  candidates.sort((a, b) => a - b);
  backTracking(candidates, used, [], 0, target, res);
  // console.log(candidates);
  return res;
};

79 单词搜索

题目描述

给定一个字母矩阵,所有的字母都与上下左右四个方向上的字母相连。给定一个字符串,求 字符串能不能在字母矩阵中寻找到。

例子1

input:word = "ABCCED",
board = [ ["A","B","C","E"], ["S","F","C","S"], ["A","D","E","E"] ]
output:true

例子2

input:word = "SEE",
board = [ ["A","B","C","E"], ["S","F","C","S"], ["A","D","E","E"] ]
output:true

例子3

input:word = "ABCB",
board = [ ["A","B","C","E"], ["S","F","C","S"], ["A","D","E","E"] ]
output:false

思考1

题目本身比较简单,就是简单的通过回溯递归来解决

实现1

  /**
  * @param {character[][]} board
  * @param {string} word
  * @return {boolean}
  */
  const dirs = [
    [-1, 0],
    [0, 1],
    [1, 0],
    [0, -1],
  ];
  const dfs = (board, m, n, i, j, res, word, visted) => {
    if (res.length === word.length) {
      if (res.join("") === word) {
        return true;
      } else {
        return false;
      }
    }
    for (let k = 0; k < 4; k++) {
      const nextI = i + dirs[k][0];
      const nextJ = j + dirs[k][1];
      if (
        nextI >= 0 &&
        nextI < m &&
        nextJ >= 0 &&
        nextJ < n &&
        word.charAt(res.length) === board[nextI][nextJ] &&
        visted[nextI][nextJ] === 0
      ) {
        res.push(board[nextI][nextJ]);
        visted[nextI][nextJ] = 1;
        if (dfs(board, m, n, nextI, nextJ, [...res], word, visted)) {
          return true;
        }
        res.pop();
        visted[nextI][nextJ] = 0;
      }
    }
    return false;
  };
  export default (board, word) => {
    const m = board.length;
    const n = board[0].length;
    const res = [];
    const visted = [];
    for (let i = 0; i < m; i++) {
      visted[i] = new Array(n).fill(0);
    }
    for (let i = 0; i < m; i++) {
      for (let j = 0; j < n; j++) {
        if (board[i][j] === word.charAt(0)) {
          res.push(board[i][j]);
          visted[i][j] = 1;
          if (dfs(board, m, n, i, j, [...res], word, visted)) {
            return true;
          }
          res.pop();
          visted[i][j] = 0;
        }
      }
    }
    return false;
  };


257. 二叉树的所有路径

题目描述

给定一个二叉树,返回所有从根节点到叶子节点的路径。


例子1

input:
1
/ <br/> 2 3
<br/> 5

output:输出: ["1->2->5", "1->3"]

提示:

叶子节点是指没有子节点的节点。

思考1

直接深度遍历就可以了

实现1

 /**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {string[]}
 */

//     1
//   /   \
//  2     3
//   \
//    5
//  Output: ["1->2->5", "1->3"]
const dfs = (root, tempRes, res) => {
  tempRes.push(root.val);
  if (!root.left && !root.right) {
    res.push(tempRes.join("->"));
    return;
  }
  if (root.left) {
    dfs(root.left, [...tempRes], res);
  }
  if (root.right) {
    dfs(root.right, [...tempRes], res);
  }
};
// Runtime: 84 ms, faster than 72.61% of JavaScript online submissions for Binary Tree Paths.
// Memory Usage: 40.5 MB, less than 30.10% of JavaScript online submissions for Binary Tree Paths.
export default (root) => {
  if (!root) return [];
  const res = [];
  dfs(root, [], res);
  return res;
};

时间复杂度O(n),空间复杂度O(n^2)

51 N皇后问题

题目描述

给定一个大小为 n 的正方形国际象棋棋盘,求有多少种方式可以放置 n 个皇后并使得她们互 不攻击,即每一行、列、左斜、右斜最多只有一个皇后。

例子1

input:4

output:[ [".Q..", // Solution 1 "...Q", "Q...", "..Q."], ["..Q.", // Solution 2 "Q...", "...Q", ".Q.."] ]

例子2

input:1
output:[["Q"]]

提示:

1 <= n <= 9

思考1

题目虽然在leetcode上显示是困难,但是思想其实很简单,就是通过简单的回溯来解决

这里需要注意的是传递数组得注意传递复制的数组

不能重复遍历,比如在在i,j这个位置遍历到下一个位置了,肯定不能再重新遍历回到i,j这个位置了

思路比较简单,但是代码可以写的很复杂,也可以写的很简单

实现1

实现2

实现1

 /**
 * @param {number} n
 * @return {string[][]}
 */
const copy = (dfsRes) => {
  const res = [];
  for (let i = 0; i < dfsRes.length; i++) {
    for (let j = 0; j < dfsRes[0].length; j++) {
      res[j] = new Array(dfsRes[0].length).fill(0);
    }
  }
  for (let i = 0; i < dfsRes.length; i++) {
    for (let j = 0; j < dfsRes[0].length; j++) {
      res[i][j] = dfsRes[i][j];
    }
  }
  return res;
};
const dfs = (dfsRes, level, res) => {
  if (level === dfsRes.length) {
    let temp = [];
    dfsRes.forEach((item) => {
      temp.push(item.join(""));
    });
    console.log(temp);
    res.push(temp);
    return;
  }
  for (let i = 0; i < dfsRes.length; i++) {
    if (canSet(dfsRes, level, i)) {
      dfsRes[level][i] = "Q";
      const temp = copy(dfsRes);
      dfs(temp, level + 1, res);
      dfsRes[level][i] = ".";
    }
  }
};

const canSet = (dfsRes, curI, curJ) => {
  for (let i = 0; i < dfsRes.length; i++) {
    for (let j = 0; j < dfsRes[0].length; j++) {
      if (dfsRes[i][j] === "Q") {
        if (i === curI || j === curJ) {
          return false;
        }
        const n = dfsRes.length;
        // 设置i,j对角线不可放置
        let nextI = i;
        let nextJ = j;
        while (nextI >= 1 && nextJ >= 1 && nextI < n + 1 && nextJ < n + 1) {
          nextI = nextI - 1;
          nextJ = nextJ - 1;
          if (nextI === curI && nextJ === curJ) {
            return false;
          }
        }
        let nextI1 = i;
        let nextJ1 = j;
        while (nextI1 >= -1 && nextJ1 >= -1 && nextI1 < n - 1 && nextJ1 < n - 1) {
          nextI1 = nextI1 + 1;
          nextJ1 = nextJ1 + 1;
          if (nextI1 === curI && nextJ1 === curJ) {
            return false;
          }
        }

        let nextI2 = i;
        let nextJ2 = j;
        while (nextI2 >= -1 && nextJ2 >= 1 && nextI2 < n - 1 && nextJ2 < n + 1) {
          nextI2 = nextI2 + 1;
          nextJ2 = nextJ2 - 1;
          if (nextI2 === curI && nextJ2 === curJ) {
            return false;
          }
        }

        let nextI3 = i;
        let nextJ3 = j;
        while (nextI3 >= 1 && nextJ3 >= -1 && nextI3 < n + 1 && nextJ3 < n - 1) {
          nextI3 = nextI3 - 1;
          nextJ3 = nextJ3 + 1;
          if (nextI3 === curI && nextJ3 === curJ) {
            return false;
          }
        }
      }
    }
  }
  return true;
};
const creatRes = (n) => {
  const dfsRes = [];
  for (let j = 0; j < n; j++) {
    dfsRes[j] = new Array(n).fill(".");
  }
  return dfsRes;
};
export default (n) => {
  let res = [];
  const visited = [];
  for (let i = 0; i < n; i++) {
    visited[i] = new Array(n).fill(0);
  }
  for (let i = 0; i < n; i++) {
    const dfsRes = creatRes(n);
    if (canSet(dfsRes, 0, i)) {
      dfsRes[0][i] = "Q";
      dfs(dfsRes, 1, res);
      dfsRes[0][i] = ".";
    }
  }
  return res;
};


实现2

/**
 * @param {number} n
 * @return {string[][]}
 */
const valid = (chess, row, col) => {
  // 检查所有列
  for (let i = 0; i < row; i++) {
    if (chess[i][col] == "Q") {
      return false;
    }
  }
  // 检查45度角的
  for (let i = row - 1, j = col + 1; i >= 0 && j < chess.length; i--, j++) {
    if (chess[i][j] == "Q") {
      return false;
    }
  }
  // 检查135度角的
  for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
    if (chess[i][j] == "Q") {
      return false;
    }
  }
  return true;
};
const solve = (res, chess, row) => {
  if (row === chess.length) {
    const temp = chess.map((item) => {
      return item.join("");
    });
    res.push(temp);
    return;
  }
  for (let col = 0; col < chess.length; col++) {
    if (valid(chess, row, col)) {
      chess[row][col] = "Q";
      solve(res, chess, row + 1);
      chess[row][col] = ".";
    }
  }
};
// Runtime: 80 ms, faster than 97.11% of JavaScript online submissions for N-Queens.
// Memory Usage: 40.8 MB, less than 76.53% of JavaScript online submissions for N-Queens.
// Next challenges:
export default (n) => {
  let res = [];
  // 定义棋盘
  const chess = [];
  for (let i = 0; i < n; i++) {
    chess[i] = new Array(n).fill(".");
  }
  solve(res, chess, 0);
  return res;
};

37. 解数独

题目描述

编写一个程序,通过填充空格来解决数独问题。

一个数独的解法需遵循如下规则:

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

空白格用 '.' 表示。

例子1

input: [ ["5", "3", ".", ".", "7", ".", ".", ".", "."], ["6", ".", ".", "1", "9", "5", ".", ".", "."], [".", "9", "8", ".", ".", ".", ".", "6", "."], ["8", ".", ".", ".", "6", ".", ".", ".", "3"], ["4", ".", ".", "8", ".", "3", ".", ".", "1"], ["7", ".", ".", ".", "2", ".", ".", ".", "6"], [".", "6", ".", ".", ".", ".", "2", "8", "."], [".", ".", ".", "4", "1", "9", ".", ".", "5"], [".", ".", ".", ".", "8", ".", ".", "7", "9"], ]

output: [ ["5", "3", "4", "6", "7", "8", "9", "1", "2"], ["6", "7", "2", "1", "9", "5", "3", "4", "8"], ["1", "9", "8", "3", "4", "2", "5", "6", "7"], ["8", "5", "9", "7", "6", "1", "4", "2", "3"], ["4", "2", "6", "8", "5", "3", "7", "9", "1"], ["7", "1", "3", "9", "2", "4", "8", "5", "6"], ["9", "6", "1", "5", "3", "7", "2", "8", "4"], ["2", "8", "7", "4", "1", "9", "6", "3", "5"], ["3", "4", "5", "2", "8", "6", "1", "7", "9"], ]

提示:

1 给定的数独序列只包含数字 1-9 和字符 '.' 。
2 你可以假设给定的数独只有唯一解
3 给定数独永远是 9x9 形式的。

思考1

刚开始的时候想使用上下左右的深度搜索来解决此问题,可是发现逻辑越写越乱

后来看了题解,思路其实很简单

采用从上到下,一行行的遍历,如果遇到数字,则选择同一行的下一个位置继续遍历,这样就避免了上下左右的遍历出现逻辑复杂的问题

同时这里也提示了一个思路,如果是原地修改的,可以采用这种一行行遍历的手段

有了按行遍历,其他的就好解决了。

可以看下实现1

实现1

const isValid = (board, i, j, setChar) => {
  // 每列不能有重复的元素
  for (let m1 = 0; m1 < 9; m1++) {
    if (board[m1][j] === setChar) {
      return false;
    }
  }
  // 每行不能有重复的元素
  for (let m2 = 0; m2 < 9; m2++) {
    if (board[i][m2] === setChar) {
      return false;
    }
  }
  // 确定每个粗线格子是否存在同样的
  let row = i - (i % 3);
  let col = j - (j % 3);
  for (let x = 0; x < 3; x++) {
    for (let y = 0; y < 3; y++) {
      if (board[x + row][y + col] === setChar) {
        return false;
      }
    }
  }
  return true;
};
// 深层遍历,先行后列
const dfs = (board, i, j) => {
  // 如果全部遍历完了,返回true
  if (i === 9) {
    return true;
  }
  // 一行遍历完了,继续下一行
  if (j >= 9) {
    return dfs(board, i + 1, 0);
  }
  // 如果是数字,到下一个
  if (board[i][j] !== ".") {
    return dfs(board, i, j + 1);
  }
  for (let m1 = 1; m1 <= 9; m1++) {
    const temp = "" + m1;
    if (!isValid(board, i, j, temp)) {
      continue;
    }
    board[i][j] = temp;
    if (dfs(board, i, j + 1)) {
      return true;
    }
    board[i][j] = ".";
  }
  return false;
};
// Runtime: 116 ms, faster than 69.58% of JavaScript online submissions for Sudoku Solver.
// Memory Usage: 38.9 MB, less than 96.44% of JavaScript online submissions for Sudoku Solver.
export default (board) => {
  dfs(board, 0, 0);
  return board;
};


时间复杂度O(9^m),m表示需要填充的“.”长度
空间复杂度O(9^m),因为每次需要创建一个临时变量

广度搜索

广度搜索就是一层层的搜索,使用先进先出队列,一层层的搜索,思想也很简单。

934 两个岛屿的最短距离

题目描述

给定一个二维 0-1 矩阵,其中 1 表示陆地,0 表示海洋,每个位置与上下左右相连。已知矩 阵中有且只有两个岛屿,求最少要填海造陆多少个位置才可以将两个岛屿相连。

例子1

input: [[1,1,1,1,1], [1,0,0,0,1], [1,0,1,0,1], [1,0,0,0,1], [1,1,1,1,1]]

output: 1

思考

就是求两个岛屿的最短距离,难点也是如何求出两个岛屿的最短距离?

一个是深度搜索,找到最短距离

一个是广度搜索,直接找到最短距离

实现1

/**
 * @param {number[][]} A
 * @return {number}
 */
const dirs = [
  [-1, 0],
  [0, 1],
  [1, 0],
  [0, -1],
];

const dfs = (firstBounds, A, m, n, i, j) => {
  if (A[i][j] === 0) {
    firstBounds.push([i, j]);
    return;
  }
  A[i][j] = 2;
  for (let k = 0; k < 4; k++) {
    const nextI = i + dirs[k][0];
    const nextJ = j + dirs[k][1];
    if (nextI >= 0 && nextI < m && nextJ >= 0 && nextJ < n && A[nextI][nextJ] != 2) {
      dfs(firstBounds, A, m, n, nextI, nextJ);
    }
  }
};

export default (A) => {
  const m = A.length;
  const n = A[0].length;

  let flipped = false;
  // 第一个岛屿的边界
  const firstBounds = [];
  for (let i = 0; i < m; i++) {
    if (flipped) {
      break;
    }
    for (let j = 0; j < n; j++) {
      if (A[i][j] === 1) {
        dfs(firstBounds, A, m, n, i, j);
        flipped = true;
        break;
      }
    }
  }
  let x, y;
  let level = 0;
  while (firstBounds.length > 0) {
    level++;
    let n_firstBounds = firstBounds.length;
    while (n_firstBounds--) {
      const [r, c] = firstBounds[0];
      firstBounds.shift();
      for (let k = 0; k < 4; k++) {
        x = r + dirs[k][0];
        y = c + dirs[k][1];
        if (x >= 0 && y >= 0 && x < m && y < n) {
          if (A[x][y] === 2) {
            continue;
          }
          if (A[x][y] === 1) {
            console.log(level);
            return level;
          }
          firstBounds.push([x, y]);
          A[x][y] = 2;
        }
      }
    }
  }
  return 0;
};

算法时间复杂度O(m * n)
空间复杂度O(m * n)

126 单词搜索II

题目描述

给定一个起始字符串和一个终止字符串,以及一个单词表,求是否可以将起始字符串每次改 一个字符,直到改成终止字符串,且所有中间的修改过程表示的字符串都可以在单词表里找到。 若存在,输出需要修改次数最少的所有更改方式。

例子1:

input: beginWord = "hit", endWord = "cog",wordList = ["hot","dot","dog","lot","log","cog"]

output: [["hit","hot","dot","dog","cog"], ["hit","hot","lot","log","cog"]]

例子2:

input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]

output: []

解释:因为cog不在wordList里边,所以不可能转换成功

思考

1 这个的思路也很简单,就是采用从beginWord和endWord开始进行广度遍历,如果两者相碰,就找到了结果

这里有两点提示
如果寻找两者最短距离,一般采用广度遍历

代码里边有个技巧,从开始beginword遍历的集合和从endword遍历的集合,交换运行

实现1

// Runtime: 132 ms, faster than 98.14% of JavaScript online submissions for Word Ladder II.
// Memory Usage: 42.8 MB, less than 100.00% of JavaScript online submissions for Word Ladder II.
const wordCanTransformOtherWord = (word, otherWord) => {
  let count = 0;
  for (let i = 0; i < word.length; i++) {
    if (word.charAt(i) !== otherWord.charAt(i)) {
      count++;
    }
    if (count > 1) {
      return false;
    }
  }
  return count === 1;
};
var findLadders = function (beginWord, endWord, wordList) {
  const wordSet = new Set(wordList);

  if (!wordSet.has(endWord)) {
    return [];
  }
  wordSet.delete(beginWord);
  wordSet.delete(endWord);
  // 从beiginWord开始广度搜索
  let beginSet = new Set([beginWord]);
  // 从endWord开始广度搜索
  let endSet = new Set([endWord]);
  // 从该word开始的路径
  const fromWordPath = {};
  // 结果
  const res = [];
  // 如果从beginWord开始广度遍历和从结束广度遍历都没结束
  while (beginSet.size > 0 && endSet.size > 0) {
    // 轮流进行遍历,从开头遍历之后,接着从结尾开始遍历
    if (beginSet.size > endSet.size) {
      const temp = beginSet;
      beginSet = endSet;
      endSet = temp;
    }

    const newSet = new Set();
    // hit
    for (let w of beginSet) {
      for (let i = 0; i < wordList.length; i++) {
        if (wordCanTransformOtherWord(w, wordList[i])) {
          const newWord = wordList[i];
          // 找到了从前遍历和从后遍历的交点
          if (endSet.has(newWord)) {
            // console.log(fromWordPath, w, newWord);
            buildPath(w, [w, newWord], newWord);
          }
          if (wordSet.has(newWord)) {
            newSet.add(newWord);
            fromWordPath[newWord] = fromWordPath[newWord] || [];
            // fromWordPath[newWord] = [];
            fromWordPath[newWord].push(w);
          }
        }
      }
    }
    if (res.length > 0) {
      return res;
    }
    // 删除已经被选择过的单词
    newSet.forEach((w) => wordSet.delete(w));
    beginSet = newSet;
  }
  return [];

  function buildPath(s, path, d) {
    console.log(fromWordPath);
    if (!fromWordPath[s] && !fromWordPath[d]) {
      return res.push(path[0] === beginWord ? path.slice() : path.slice().reverse());
    }

    if (fromWordPath[s]) {
      fromWordPath[s].forEach((w) => buildPath(w, [w, ...path], d));
    } else {
      fromWordPath[d].forEach((w) => buildPath(s, [...path, w], w));
    }
  }
};
export default findLadders;

310 找出给出最小高度数的根节点

题目描述

树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。

给你一棵包含 n 个节点的数,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。

可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。

请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。 树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。

例子1:

input:n = 4, edges = [[1,0],[1,2],[1,3]]

output: [1]
解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。

例子2:

input: n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]]

output: [3,4]

例子3:

input: n = 1, edges = []

output: [0]

例子4:

input: n = 2, edges = [[0,1]]

output: [0,1]

提示:

1 1 <= n <= 2 * 10^4
2 edges.length == n - 1
3 0 <= ai, bi < n
4 ai != bi
5 所有 (ai, bi) 互不相同
6 给定的输入 保证 是一棵树,并且 不会有重复的边

思考

1 直接进行广度遍历,遍历每个节点,然后查看每个节点作为根节点的高度,查找到高度最小的节点输出就可以。
可以查看实现1,不过这里会超时

2 还有一种方法,不过这种不容易想到。

首先可以把它想象成一张图,类似一张网,因为我们想找到座位根节点最低的树,肯定是在图最里边的,越靠近里边,肯定是越低

这里主要是如何想到使用图这种来解决此问题?

为什么在图最里边的就是最低树的根节点?

根据基本常识应该可以理解在图最里边的是最低树的根节点

那么剩下的问题就是如何发现最里边的节点了?

可以想下,如果是你,如何像剥葱一样,一层层的剥掉外面的节点找到最里边的节点

其实很简单,最外面的叶子节点的入度都是1,不断的删掉叶子节点,同时把与叶子节点相邻的节点的入度减1就可以了

这里还有一点需要注意的是因为图里是没有环的,所以最后的结果肯定是只有一个节点或者两个节点。可以反证一下,如果最后三个节点会是什么样?

可以看下实现2

实现1

/**
 * @param {number} n
 * @param {number[][]} edges
 * @return {number[]}
 */
const gfs = (reached, visited, edges, deep) => {
  if (reached.length === 0) {
    return deep;
  }
  const nextRoot = [];
  for (let i = 0; i < reached.length; i++) {
    visited.push(reached[i]);
    for (let j = 0; j < edges.length; j++) {
      if (edges[j].includes(reached[i])) {
        if (edges[j][0] !== reached[i] && !visited.includes(edges[j][0])) {
          nextRoot.push(edges[j][0]);
        } else if (!visited.includes(edges[j][1])) {
          nextRoot.push(edges[j][1]);
        }
      }
    }
  }
  return gfs(nextRoot, visited, edges, deep + 1);
};
export default (n, edges) => {
  let min = Number.MAX_VALUE;
  let res = [];
  for (let i = 0; i < n; i++) {
    let visited = [];
    const nextRoot = [];
    visited.push(i);
    for (let j = 0; j < edges.length; j++) {
      if (edges[j].includes(i)) {
        if (edges[j][0] !== i && !visited.includes(edges[j][0])) {
          nextRoot.push(edges[j][0]);
        } else if (!visited.includes(edges[j][1])) {
          nextRoot.push(edges[j][1]);
        }
      }
    }
    const deep = gfs(nextRoot, visited, edges, 0);
    console.log(i, deep);
    min = Math.min(min, deep);
    res.push({
      node: i,
      deep,
    });
  }
  res = res
    .filter((item) => item.deep === min)
    .map((item) => {
      return item.node;
    });
  console.log(res);
  return res;
};


实现2

/**
 * @param {number} n
 * @param {number[][]} edges
 * @return {number[]}
 */
//  Runtime: 104 ms, faster than 90.27% of JavaScript online submissions for Minimum Height Trees.
//  Memory Usage: 46.4 MB, less than 81.88% of JavaScript online submissions for Minimum Height Trees.
export default (n, edges) => {
  if (n === 1) return [0];

  const adj = new Array(n);
  for (let i = 0; i < n; ++i) {
    adj[i] = new Set();
  }
  // 建立边的链接
  // 记录每个节点和它相连的节点个数
  for (let edge of edges) {
    adj[edge[0]].add(edge[1]);
    adj[edge[1]].add(edge[0]);
  }

  let leaves = new Set();
  for (let i = 0; i < n; ++i) {
    // 如果是叶子节点
    if (adj[i].size === 1) {
      leaves.add(i);
    }
  }
  // 因为没有环,结果要么是一个节点,要么是两个节点
  while (n > 2) {
    n -= leaves.size;
    const newLeaves = new Set();
    for (let i of leaves) {
      // 查找叶子节点,
      for (let j of adj[i]) {
        // 从连接的j节点中删除叶子节点
        adj[j].delete(i);
        // 如果也是叶子节点,加入叶子节点
        if (adj[j].size === 1) {
          newLeaves.add(j);
        }
      }
    }
    leaves = newLeaves;
  }
  return Array.from(leaves);
};

实现2时间复杂度和空间复杂度都是O(n)

深度广度搜索总结

深度主要是一直向下走,直到走不动了,然后再从自己走过的点重新选择一个继续走

广度就是一层层的遍历,遍历完一层,继续遍历下一层

这里需要注意的是不管深度还是广度都不能重复访问已经访问过的节点

还有在查找两个节点之间的最短距离的时候,一般使用广度搜索

至于回溯算法也比较简单,一般三步,首先修改状态,然后进入递归,递归完成之后,恢复状态。