# 搜索算法

# BFS

广度优先搜索一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。

bfs

第一层:

- 0 -> {6,2,1,5}
1

第二层:

- 6 -> {4}
- 2 -> {}
- 1 -> {}
- 5 -> {3}
1
2
3
4

第三层:

- 4 -> {}
- 3 -> {}
1
2

每一层遍历的节点都与根节点距离相同。设 d~i~ 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 d~i~ <= d~j~。利用这个结论,可以求解最短路径等 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径,无权图是指从一个节点到另一个节点的代价都记为 1。

在程序实现 BFS 时需要考虑以下问题:

  • 队列:用来存储每一轮遍历得到的节点;
  • 标记:对于遍历过的节点,应该将它标记,防止重复遍历。

# 1091.二进制矩阵中的最短路径

[[1,1,0,1],
 [1,0,1,0],
 [1,1,1,1],
 [1,0,1,1]]
1
2
3
4

题目描述:1 表示可以经过某个位置,求解从 (0, 0) 位置到 (tr, tc) 位置的最短路径长度。

public int minPathLength(int[][] grids, int tr, int tc) {
    final int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    final int m = grids.length, n = grids[0].length;
    Queue<Pair<Integer, Integer>> queue = new LinkedList<>();
    queue.add(new Pair<>(0, 0));
    int pathLength = 0;
    while (!queue.isEmpty()) {
        int size = queue.size();
        pathLength++;
        while (size-- > 0) {
            Pair<Integer, Integer> cur = queue.poll();
            int cr = cur.getKey(), cc = cur.getValue();
            grids[cr][cc] = 0; // 标记
            for (int[] d : direction) {
                int nr = cr + d[0], nc = cc + d[1];
                if (nr < 0 || nr >= m || nc < 0 || nc >= n || grids[nr][nc] == 0) {
                    continue;
                }
                if (nr == tr && nc == tc) {
                    return pathLength;
                }
                queue.add(new Pair<>(nr, nc));
            }
        }
    }
    return -1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 279.完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

示例 1:

输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
1
2
3

动态规划:

动态转移方程为:dp[i] = MIN(dp[i], dp[i - j * j] + 1),i 表示当前数字,j*j 表示平方数

var numSquares = function(n) {
  const dp = [...Array(n + 1)].map((_) => 0); //数组长度n+1 值均为0
  for (let i = 1; i <= n; i++) {
    dp[i] = i;
    for (let j = 0; i - j * j >= 0; j++) {
      dp[i] = Math.min(dp[i], dp[i - j * j] + 1); //动态转移方程
    }
  }
  return dp[n];
};
1
2
3
4
5
6
7
8
9
10
public int numSquares(int n){
    int[] dp = new int[n+1];  //默认初始值为0
    for(int i=0;i<=n;i++){
        dp[i] = i;
        for(int j=1;i-j*j>=0;j++){
            dp[i]=Math.min(dp[i],dp[i-j*j]+1);
        }
    }
    return dp[n];
}
1
2
3
4
5
6
7
8
9
10

BFS

static class Node {
        int val;
        int step;

        public Node(int val, int step) {
            this.val = val;
            this.step = step;
        }
    }

    // 将问题转化成图论
    // 该算法在往队列里面添加节点的时候会 add 很多重复的节点,导致超时,
    // 优化办法是,加入 visited 数组,检查要 add 的数据是否已经出现过了,防止数据重复出现,从而影响图的遍历
    // 同时优化:num - i * i 表达式,只让他计算一次
    // 同时在循环体里面判断退出或返回的条件,而不是在循环体外
    public int numSquares(int n) {
        Queue<Node> queue = new LinkedList<>();
        queue.add(new Node(n, 0));
        // 其实一个真正的图的 BSF 是一定会加上 visited 数组来过滤元素的
        boolean[] visited = new boolean[n+1];
        while (!queue.isEmpty()) {
            int num = queue.peek().val;
            int step = queue.peek().step;
            queue.remove();
            for (int i = 1; ; i++) {
                int a = num - i * i;
                if (a < 0) {
                    break;
                }
                // 若 a 已经计算到 0 了,就不必再往下执行了
                if (a == 0) {
                    return step + 1;
                }
                if (!visited[a]) {
                    queue.add(new Node(a, step + 1));
                    visited[a] = true;
                }
            }
        }
        return -1;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 127.单词接龙

给定两个单词(beginWord  和 endWord)和一个字典,找到从  beginWord 到  endWord 的最短转换序列的长度。转换需遵循如下规则:

每次转换只能改变一个字母。 转换过程中的中间单词必须是字典中的单词。

示例 1:

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

输出: 5

解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
     返回它的长度 5。
1
2
3
4
5
6
7
8
9
10
11

官方题解

import javafx.util.Pair;

class Solution {
  public int ladderLength(String beginWord, String endWord, List<String> wordList) {

    // Since all words are of same length.
    int L = beginWord.length();

    // Dictionary to hold combination of words that can be formed,
    // from any given word. By changing one letter at a time.
    HashMap<String, ArrayList<String>> allComboDict = new HashMap<String, ArrayList<String>>();

    wordList.forEach(
        word -> {
          for (int i = 0; i < L; i++) {
            // Key is the generic word
            // Value is a list of words which have the same intermediate generic word.
            String newWord = word.substring(0, i) + '*' + word.substring(i + 1, L);
            ArrayList<String> transformations =
                allComboDict.getOrDefault(newWord, new ArrayList<String>());
            transformations.add(word);
            allComboDict.put(newWord, transformations);
          }
        });

    // Queue for BFS
    Queue<Pair<String, Integer>> Q = new LinkedList<Pair<String, Integer>>();
    Q.add(new Pair(beginWord, 1));

    // Visited to make sure we don't repeat processing same word.
    HashMap<String, Boolean> visited = new HashMap<String, Boolean>();
    visited.put(beginWord, true);

    while (!Q.isEmpty()) {
      Pair<String, Integer> node = Q.remove();
      String word = node.getKey();
      int level = node.getValue();
      for (int i = 0; i < L; i++) {

        // Intermediate words for current word
        String newWord = word.substring(0, i) + '*' + word.substring(i + 1, L);

        // Next states are all the words which share the same intermediate state.
        for (String adjacentWord : allComboDict.getOrDefault(newWord, new ArrayList<String>())) {
          // If at any point if we find what we are looking for
          // i.e. the end word - we can return with the answer.
          if (adjacentWord.equals(endWord)) {
            return level + 1;
          }
          // Otherwise, add it to the BFS Queue. Also mark it visited
          if (!visited.containsKey(adjacentWord)) {
            visited.put(adjacentWord, true);
            Q.add(new Pair(adjacentWord, level + 1));
          }
        }
      }
    }

    return 0;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# DFS

# Java实现

public Iterator DFSTraverse(Vertex v){
    LinkedList traverseSeq = new LinkedListDLNode();// 遍历结果
    resetVexStatus(); //重置顶点状态
    DFSRecursion(v,traverseSeq); // 从v 点出发深度优先搜索
    Iterator it = getVertext(); // 从图未曾访问的其他顶点重新搜索
    for(it.first();!it.isDone();it.next()){
        Vertex u = (Vertex)it.currentItem();
        if(!u.isVisited())DFSRecursion(u,traverseSeq);
    }
    return traverseSeq.elements();
}

private void DFSRecursion(Vertex v,LinkedList list){
    v.setToVisited(); // 设置顶点V已经访问过
    list.insertLast(v); // 访问顶点v
    Iterator it = adjVertexs(v); // 取得顶点v的所有邻接点
    for(it.first();!it.isDone();it.next()){
        Vertex u = (Vertex)it.currentItem();
        if(!u.isVisited())DFSRecursion(u,list);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点.

DFS

# 695.岛屿的最大面积

class Solution {
    int [][] grid;
    int rows,columns;
    int max = Integer.MIN_VALUE;
    int sum = 0;
    public int maxAreaOfIsland(int[][] grid) {
        if(grid.length==0||grid==null){
            return 0;
        }
        this.grid = grid;
        this.rows = grid.length;
        this.columns = grid[0].length;
        for(int i=0;i<grid.length;i++){
            for(int j=0;j<grid[0].length;j++){
                if(grid[i][j]==1){
                    findArea(i,j);
                    max = Math.max(sum,max);
                    sum=0;
                }
            }
        }
        return max == Integer.MIN_VALUE?0:max;
    }
    public void findArea(int i,int j){
        if(i<0||i>=rows||j<0||j>=columns||grid[i][j]==0||grid[i][j]==-1){
            return;
        }
        sum++;
        grid[i][j]=-1;
        findArea(i-1,j);
        findArea(i+1,j);
        findArea(i,j+1);
        findArea(i,j-1);
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 200.岛屿数量

给定一个由  '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。

class Solution {
    public int numIslands(char[][] grid) {
        if(grid==null||grid.length==0){
            return 0;
        }
        int nr = grid.length;
        int nc = grid[0].length;
        int num_islands = 0;
        for(int r=0;r<nr;++r){
            for(int c=0;c<nc;++c){
                if(grid[r][c]=='1'){
                    ++num_islands;
                    dfs(grid,r,c);
                }
            }
        }
        return num_islands;
    }

     void dfs(char[][] grid,int r,int c){
        int nr = grid.length;
        int nc = grid[0].length;
        if(r<0||c<0||r>=nr||c>=nc||grid[r][c]=='0'){
            return;
        }
        grid[r][c] = '0';
        dfs(grid,r-1,c);
        dfs(grid,r+1,c);
        dfs(grid,r,c-1);
        dfs(grid,r,c+1);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 547.朋友圈

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

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

public class Solution{
    public void dfs(int[][] M,int[] visited,int i){
        for(int j=0;j<M.length;j++){
            if(M[i][j]==1&&visited[j]==0){
                visited[j]=1;
                dfs(M,visited,j);
            }
        }
    }

    public int findCircleNum(int[][] M){
        int[] visited = new int[M.length];
        int count = 0;
        for(int i=0;i<M.length;i++){
            if(visited[i]==0){
                dfs(M,visited,i);
                count++;
            }
        }
        return count;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 130.被围绕的区域

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

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

示例:

X X X X
X O O X
X X O X
X O X X
1
2
3
4

运行你的函数后,矩阵变为:

X X X X
X X X X
X X X X
X O X X
1
2
3
4

解答

  1. bfs+递归
class Solution {
    public void solve(char[][] board) {
        if (board == null || board.length == 0) return;
        int m = board.length;
        int n = board[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 从边缘o开始搜索
                //判断当前点是否是边界情况
                boolean isEdge = i == 0 || j == 0 || i == m - 1 || j == n - 1;
                if (isEdge && board[i][j] == 'O') {
                    dfs(board, i, j);
                }
            }
        }

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
                if (board[i][j] == '#') {
                    board[i][j] = 'O';
                }
            }
        }
    }

    public void dfs(char[][] board, int i, int j) {
        if (i < 0 || j < 0 || i >= board.length  || j >= board[0].length || board[i][j] == 'X' || board[i][j] == '#') {
            // board[i][j] == '#' 说明已经搜索过了.
            return;
        }
        board[i][j] = '#';
        dfs(board, i - 1, j); // 上
        dfs(board, i + 1, j); // 下
        dfs(board, i, j - 1); // 左
        dfs(board, i, j + 1); // 右
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  1. dfs+递归
var solve = function(board) {
  let m = board.length;
  if (m == 0) {
    return;
  }
  let n = board[0].length;
  let cannot = {};
  let dfs = (i, j) => {
    //越界 标志过或者非相连O下return
    if (
      i < 0 ||
      j < 0 ||
      i == m ||
      j == n ||
      board[i][j] != "O" ||
      cannot[i + "-" + j]
    ) {
      return;
    }
    cannot[i + "-" + j] = true;
    dfs(i - 1, j);
    dfs(i + 1, j);
    dfs(i, j - 1);
    dfs(i, j + 1);
  };
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      //从边缘O出发寻找其相连接点都标示为不可替换
      if (
        i == 0 ||
        j == 0 ||
        i == m - 1 ||
        (j == n - 1 && board[i][j] == "O")
      ) {
        dfs(i, j);
      }
    }
  }
  //规避边界条件去循环 将边界不相互连接的联通到一起
  for (let i = 1; i < m - 1; i++) {
    for (let j = 1; j < n - 1; j++) {
      if (!cannot[i + "-" + j] && board[i][j] == "O") {
        board[i][j] = "X";
      }
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

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

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

DFS+递归

public  List<List<Integer>> pacificAtlantic(int[][] matrix){
    List<List<Integer>> result = new ArrayList<>();
    if(matrix.length==0||matrix[0].length==0){
        return new ArrayList<>();
    }

    int m = matrix.length;
    int n = matrix[0].length;
    int[][] pacific = new int[m][n];
    int[][] atlantic = new int[m][n];

    //海洋边界
    for(int i=0;i<m;i++){
        for(int j=0;j<n;j++){
            if(i==0||j==0){
                dfs(matrix,pacific,i,j,matrix[i][j]);
            }
            if(i==m-1||j==n-1){
                dfs(matrix,atlantic,i,j,matrix[i][j]);
            }
        }
    }

    List<List<Integer>> res = new ArrayList<>();
    for(int i=0;i<m;i++){
        for(int j=0;j<n;j++){
            if(pacific[i][j]==1&&atlantic[i][j]==1){
                res.add(Arrays.asList(i,j));
            }
        }
    }
    return res;
}

private void dfs(int[][] matrix,int[][] aux,int i,int j,int pre){
    //判断边界
    if(i<0||j<0||i>matrix.length-1||j>matrix[0].length-1||aux[i][j]==1||matrix[i][j]<pre){
        return;
            //aux[i][j]==1 已经流过了
            //matrix[i][j]<pre不能流动
    }

    aux[i][j]=1;
    dfs(matrix,aux,i-1,j,matrix[i][j]);
    dfs(matrix,aux,i+1,j,matrix[i][j]);
    dfs(matrix,aux,i,j-1,matrix[i][j]);
    dfs(matrix,aux,i,j+1,matrix[i][j]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

BFS+递归

public List<List<Integer>>pacificAtlantic(int[][] matrix){
    if(matrix.length==0||matrix[0].length==0){
        return new ArrayList<>();
    }

    int m = matrix.length;
    int n = matrix[0].length;

    Queue<int[]> pacificQueue = new LinkedList<>();
    Queue<int[]> atlanticQueue = new LinkedList<>();

    int[][] pacificAux = new int[m][n];
    int[][] atlanticAux = new int[m][n];

    //从海洋边界开始
    for(int i=0;i<m;i++){
        for(int j=0;j<n;j++){
            if(i==0||j==0){
                pacificQueue.add(new int[]{i,j});
            }
            if(i==m-1||j==n-1){
                atlanticQueue,add(new int[]{i,j});
            }
        }
    }

    bfs(matrix,pacificAux,pacificQueue);
    bfs(matrix,atlanticAux,atlanticQueue);

    List<List<Integer>> res = new ArrayList<>();
    for(int i=0;i<m;i++){
        for(int j=0;j<n;j++){
            //路未被标记
            if(pacificAux[i][j]==1&&atlanticAux[i][j]==1){
                res.add(Arrays.asList(i,j));
            }
        }
    }
    return res;
}

private void bfs(int[][] matrix,int[][] aux,Queue<int[]> queue){
    while(!queue.isEmpty()){
        int[] array = queue.remove();
        int i = array[0];
        int j = array[1];
        //流到的位置置为1
        aux[i][j]=1;
        //数组位置不越界 从高到低或者同等高度移动
        if(i-1>=0&&matrix[i][j]<=matrix[i-1][j]&&aux[i-1][j]!=1){
            queue.add(new int[]{i-1,j});
        }
        if(i+1<matrix.length&&matrix[i][j]<=matrix[i+1][j]&&aux[i+1][j]!=1){
            queue.add(new int[]{i+1,j});
        }
        if(j-1>=0&&matrix[i][j]<=matrix[i][j-1]&&aux[i][j-1]!=1){
            queue.add(new int[]{i,j-1});
        }
        if(j+1<matrix[0].length&&matrix[i][j]<=matrix[i][j+1]&&aux[i][j+1]!=1){
            queue.add(new int[]{i,j+1});
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

# 100.相同的树

给定两个二叉树,编写一个函数来检验它们是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

public boolean isSameTree(TreeNode p,TreeNode q){
    //左右子树都为空
    if(p==null&&q==null) return true;

    //左右任一个为空
    if(p==null||q==null) return false;
    if(p.val!=q.val) return false;

    return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right);
}
1
2
3
4
5
6
7
8
9
10

# 101.对称二叉树

给定一个二叉树,检查它是否是镜像对称的。

例如,二叉树  [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3
1
2
3
4
5

但是下面这个  [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   / \
  2   2
   \   \
   3    3
1
2
3
4
5
public boolean isSymmetric(TreeNode root){
    if(root==null) return true;
    return judge(root.left,root.right);

}
private boolean judge(TreeNode left,TreeNode right){
    if(left==null&&right==null) return true;
    if(left==null||right==null) return false;
    if(left.val!=right.val) return false;
    return judge(left.left,right.rigth)&&judge(left.right,right.left);
}
1
2
3
4
5
6
7
8
9
10
11

# 102.层序遍历

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。

例如: 给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7
1
2
3
4
5

返回其层次遍历结果:

[
  [3],
  [9,20],
  [15,7]
]
1
2
3
4
5

bfs+递归实现

List<List<Integer>> levels = new ArrayList<List<Integer>>();

public void helper(TreeNode node,int level){
    //从当前层次开始
    if(levels.size()==level){
        levels.add(new ArrayList<Integer>());
    }
    //填充当前层次
    levels.get(level).add(node.val);

    //处理当前节点的下一节点
    if(node.left!=null){
        helper(node.left,level+1);
    }
    if(node.right!=null){
        helper(node.right,level+1);
    }
}

public List<List<Integer>> levelOrder(TreeNode root)
{
    if(root==null) return levels;
    helper(root,0);
    return levels;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

迭代实现

public List<List<Integer>> levelOrder(TreeNode root){
    List<List<Integer>> levels = new ArrayList<List<Integer>>();
    if(root==null) return levels;

    Queue<TreeNode> queue = new LinkedList<TreeNode>();
    queue.add(root);
    int level =0;
    while(!queue.isEmpty()){
        //开始当前层
        levels.add(new ArrayList<Integer>());
        //当前层下面的所有数
        int level_length = queue.size();
        for(int i=0;i<levle_length;++i){
            TreeNode node = queue.remove();
            //填充当前节点
            levels.get(level).add(node.val);

            //将当前节点的子节点添加到队列中
            if(node.left!=null) queue.add(node.left);
            if(node.right!=null)queue.add(node.right);
        }
        //下一层开始
        level++;
    }
    return levels;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 116.填充每个节点的下一个右侧节点指针

给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}
1
2
3
4
5
6

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。

初始状态下,所有  next 指针都被设置为 NULL。

public Node connect(Node root){
    if(root==null) return null;
    Node pre = root;
    Node cur = null;
    Node start = pre;
    while(pre.left!=null){
        //遍历到最右边的节点,要将pre和cur更新到下一层,并且用start记录
        if(cur==null){
            //把pre的左节点的next指向右节点
            pre.left.next = pre.right;

            pre = start.left;
            cur = start.right;
            start = pre;
            //连接下一层的next,同时pre next 后移
        }else{
            //把pre的左节点的next指向右节点
            pre.left.next = pre.right;
            //pre的右节点的next指向cur的左节点
            pre.right.next = cur.left;

            pre = pre.next;
            cur = cur.next;

        }
    }
    return root;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# Backtrace

回溯是一种通过穷举所有可能情况来找到所有解的算法。如果一个候选解最后被发现并不是可行解,回溯算法会舍弃它,并在前面的一些步骤做出一些修改,并重新尝试找到可行解。

套用算法模板 直接上回溯算法框架。解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

for 选择 in 选择列表:
    做选择
    backtrack(路径, 选择列表)
    撤销选择
1
2
3
4
5
6
7
8
9
10

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。

List<List<Integer>> res = new LinkedList<>();

/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
    // 记录「路径」
    // 这里的 选择列表 即包含在nums中
    LinkedList<Integer> track = new LinkedList<>();
    backtrack(nums, track);
    return res;
}

// 路径:记录在 track 中
// 选择列表:nums 中的元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
    // 触发结束条件
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }

    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择
        if (track.contains(nums[i]))
            continue;
        // 做选择
        track.add(nums[i]);
        // 进入下一层决策树
        backtrack(nums, track);
        // 取消选择,返回上一层决策树
        track.removeLast();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

解决 LeetCode 39 题的回溯算法

public class combinationSum_39 {

    public static void main(String[] args) {
        combinationSum(new int[]{2,3,6,7},7);
        System.out.println(res);

    }
    public static List<List<Integer>> res =  new LinkedList<>();

    public static List<List<Integer>> combinationSum(int[] candidates, int target) {
        LinkedList<Integer> track = new LinkedList<>();
        //排序后剪枝更容易
        Arrays.sort(candidates);
        //套用公式,candidates是指选择列表。target用来判断是否结束和用于剪枝
        //track是路径 已经做出的选择
        backtrack(candidates, 0, target, track);
        return res;
    }

    private static void backtrack(int[] candidates, int start, int target, LinkedList<Integer> track) {
        //先判断结束条件
        if (target < 0) return;
        if (target == 0){
            //target为0时 将路径加入结果列表
            res.add(new LinkedList<>(track));
            return;
        }
        //遍历选择列表
        for(int i = start;i < candidates.length;i++){
            if(target < candidates[i]) break;
            track.add(candidates[i]);
            backtrack(candidates,i,target-candidates[i],track);
            track.removeLast();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 17.电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 示例: “ 输入:"23" 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. ”

class Solution {

    Map<String,String> phone = new HashMap<String,String>(){{
        put("2","abc");
        put("3","def");
        put("4","ghi");
        put("5","jkl");
        put("6","mno");
        put("7","pqrs");
        put("8","tuv");
        put("9","wxyz");
    }};

    List<String> output = new ArrayList<String>();

    public void backtrace(String combination,String next_digits){
        //没有数字
        if(next_digits.length()==0){
            //组合完成
            output.add(combination);

        //如果next_digits不为空
        }else{
            //枚举在map中的所有字母中可用的字符
            String digit = next_digits.substring(0,1);
            String letters = phone.get(digit);
            for(int i=0;i<letters.length();i++){
                String letter = phone.get(digit).substring(i,i+1);
                //将当前字符添加到组合中
                //处理下一个数字
                backtrace(combination+letter,next_digits.substring(1));
            }
        }
    }

    public List<String> letterCombinations(String digits) {
        if(digits.length()!=0)
        {
            backtrace("",digits);
        }
        return output;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 复原 IP 地址

给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。

示例:

输入: "25525511135"
输出: ["255.255.11.135", "255.255.111.35"]
1
2

BFS+回溯

    public List<String> restoreIpAddresses(String s) {
        List<String> ans = new ArrayList<>(); //保存最终的所有结果
        getAns(s,0,new StringBuilder(),ans,0);
        return ans;
    }

    private void getAns(String s,int start,StringBuilder temp,List<String> ans,int count){
        //长度大于剩下部分的最长长度
        if(s.length()-start>3*(4-count)){
            return;
        }
        //刚好到达了末尾
        if(start==s.length()){
            //当前刚好是4的部分 将结果加入
            if(count==4){
                ans.add(new Sring(temp.substring(0,temp.length()-1)));
            }
            return;
        }

        //当前超过末尾,或者已经到达了4部分结束
        if(start>s.length()||count==4){
            return;
        }

        //保存当前解
        StringBuilder before = new StringBuilder(temp);

        //加入1位数
        temp.append(s.charAt(start)+""+'.');
        getAns(s,start+1,temp,ans,count+1);

        //如果开头是0
        if(s.charAt(start)=='0'){
            return;
        }

        //加入2位数
        if(start+1<s.length()){
            temp = new StringBuilder(before);//恢复为之前的解
            temp.append(s.substring(start,start+2)+""+'.');
            getAns(s,start+2,temp,ans,count+1);
        }

        //加入3位数
        if(start+2<s.length()){
            temp = new  StringBuilder(before);
            int num = Integer.parseInt(s.substring(start,start+3));
            if(num>=0&&num<=255){
                temp.append(s.substring(start,start+3)+""+'.');
                getAns(s,start+3,temp,ans,count+1);
            }
        }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 79.单词搜索

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

给定 word = "ABCCED", 返回 true.
给定 word = "SEE", 返回 true.
给定 word = "ABCB", 返回 false.
1
2
3
4
5
6
7
8
9
10

DFS 我们需要做的就是,在深度优先遍历过程中,判断当前遍历元素是否对应 word 元素,如果不匹配就结束当前的遍历,返回上一次的元素,尝试其他路径。当然,和普通的 dfs 一样,我们需要一个 visited 数组标记元素是否访问过。

public boolean exist(char[][] board,String word){
    int rows = board.length;
    if(rows == 0){
        return false;
    }
    int cols = board[0].length;
    boolean[][] vivisted = new boolean[rows][cols];
    //从不同位置开始
    for(int i=0;i<rows;i++){
        for(int j=0;j<cols;j++){
            //从当前位置 开始 符合就返回true
            if(existRecursive(board,i,j,word,0,visited)){
                return true;
            }
        }
    }
    return false;
}

private boolean existRecursive(char[][] board,int row,int col,String word,int index,boolean[][] visited){
    //判断是否越界
    if(row<0||row>=board.length||col<0||col>=board[0].length){
        return false;
    }

    //当前元素访问过或者当前word不匹配返回false
    if(visited[row][col]||board[row][col]!=word.charAt(index)){
        return false;
    }
    //匹配到了最后一个字母 反击true
    if(index==word.length()-1){
        return true;
    }

    //将当前位置标记为已访问
    visited[row][col] = true;
    //对四个位置分别尝试
    boolean up = existRecursive(board,row-1,col,word,index+1,visited);
    if(up){
        return true;
    }
    boolean down = existRecursive(board,row+1,col,word,index+1,visited);
    if(down){
        return true;
    }
    boolean left = existRecursive(board,row,col-1,word,index+1,visited);
    if(left){
        return true;
    }
    boolean right = existRecursive(board,row,col+1,word,index+1,visited);
    if(right){
        return true;
    }
    //当前位置未被访问
    visited[row][col] = false;
    return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

# 257.二叉树的所有路径

给定一个二叉树,返回所有从根节点到叶子节点的路径。 说明:  叶子节点是指没有子节点的节点。 示例:

输入:

   1
 /   \
2     3
 \
  5

输出: ["1->2->5", "1->3"]
解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3
1
2
3
4
5
6
7
8
9
10
class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        LinkedList<String> paths = new LinkedList();
        constructor_paths(root,"",paths);
        return paths;
    }

    private void construct_paths(TreeNode root,String path,LinkedList<String> paths){
        //递归的结束条件
        if(root!=null){
            path+=Integer.toString(root.val);
            if((root.left==null)&&(root.right==null)){
                //当前节点是叶子结点
                //将当前节点加入路径
                paths.add(path);
            }else{
                //当前节点不是叶子节点
                path+="->";
                //从左右节点开始递归
                //递归的返回值是 path 和 paths
                construct_paths(root.left,path,paths);
                construct_paths(root.right,path,paths);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • 迭代实现 迭代(宽度优先搜索)的方法实现。我们维护一个队列,存储节点以及根到该节点的路径。一开始这个队列里只有根节点。在每一步迭代中,我们取出队列中的首节点,如果它是一个叶子节点,则将它对应的路径加入到答案中。如果它不是一个叶子节点,则将它的所有孩子节点加入到队列的末尾。当队列为空时,迭代结束。
class solution{
    public List<String> binarTreePaths(TreeNode root){
        LinkedList<String> paths = new LinkedList();
        if(root==null)
        {
            return paths;
        }

        LinkedList<TreeNode> node_stack = new LinkedList();
        LinkedList<String> path_stack = new LinkedList();
        node_stack.add(root);
        path_stack.add(Integer.toString(root.val));
        TreeNode node;
        String path;
        while(!node_stack.isEmpty()){
            node = node_stack.pollLast();
            path = path_stack.pollLast();
            if((node.left==null)&&(node.right==null)){
                paths.add(path);
            }
            if(node.left!=null){
                node_stack.add(node.left);
                path_stack.add(path+"->"+Integer.toString(node.left.val));
            }
            if(node.right!=null){
                node_stack.add(node.right);
                path_stack.add(path+"->"+Integer.toString(node.right.val));
            }
        }
        return path;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 113.路径总和 II

给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。

说明:  叶子节点是指没有子节点的节点。

示例: 给定如下二叉树,以及目标和  sum = 22,

              5
             / \
            4   8
           /   / \
          11  13  4
         /  \    / \
        7    2  5   1
1
2
3
4
5
6
7

返回:

[
   [5,4,11,2],
   [5,8,4,5]
]
1
2
3
4

解题思路改造一下上面的算法,将 sum 在每次遍历中减去,实现如下:

class Solution{
    private List<List<Integer>> resultList = new ArrayList<>();
    private List<Integer> tempList = new ArrayList<>();

    public List<List<Integer>> pathSum(TreeNode root,int sum){
        dfs(root,sum);
        return resultList;
    }

    private void dfs(TreeNode node,int sum){
        if(node==null){
            return ;
        }
        tempList.add(node.val);
        sum-=node.val;
        if(node.left==null&&node.right==null&&sum==0){
             resultList.add(new ArrayList<>(tempList));
        }

        if(node.left!=null){
            dfs(node.left,sum);
        }
        if(node.right!=null){
            dfs(node.right,sum);
        }
        tempList.remove(tempList.size()-1);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 437.路径总和 III

给定一个二叉树,它的每个结点都存放着一个整数值。

找出路径和等于给定数值的路径总数。

路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

二叉树不超过 1000 个节点,且节点数值范围是 [-1000000,1000000] 的整数。

示例:

root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8

      10
     /  \
    5   -3
   / \    \
  3   2   11
 / \   \
3  -2   1

返回 3。和等于 8 的路径有:

1.  5 -> 3
2.  5 -> 2 -> 1
3.  -3 -> 11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int pathSum(TreeNode root, int sum) {
 return pathSum(root, sum, new int[1000], 0);
 }

 public int pathSum(TreeNode root, int sum, int[] array/*保存路径*/, int p/*指向路径终点*/) {
  if (root == null) {
   return 0;
  }
    int tmp = root.val;
    int n = root.val == sum ? 1 : 0;
    for (int i = p - 1; i >= 0; i--) {//从后往前,后面的才是新增的,前面的已经算过了。
        tmp += array[i];
        if (tmp == sum) {
            n++;
        }
    }
    array[p] = root.val;
    int n1 = pathSum(root.left, sum, array, p + 1);//往左走,往右走
    int n2 = pathSum(root.right, sum, array, p + 1);
    return n + n1 + n2;//当前子树、左子树、右子树存在的满足路径的数量
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) { val = x; }
    }
    public  int pathSum(TreeNode root,int sum) {
        if(root==null) {
            return 0;
        }
        return PathS(root,sum)+PathS(root.left,sum)+PathS(root.right,sum);
    }

    private  int PathS(TreeNode root,int sum) {
        if(root==null) {
            return 0;
        }
        int res = 0;
        if(root.val==sum) {
            res +=1;
        }
        res += PathS(root.left,sum-root.val);
        res += PathS(root.right,sum-root.val);
        return res;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 46.全排列

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

示例:

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]
1
2
3
4
5
6
7
8
9
10
public List<List<Integer>> permute(int[] nums){
    //初始化
    List<List<Integer>> output = new LinkedList();
    //将数组转化为list
    ArrayList<Integer> nums_list = new ArrayList<Integer>();
    for(int num:nums){
        nums_list.add(num);
    }
    int n = nums.length;
    backtrace(n,nums_list,output,0);
    return output;
}

private void backtrace(int n,ArrayList<Integer> nums,List<List<Integer>> output,int first){
    //所有的数字已使用
    if(first==n){
        output.add(new ArrayList<Integer>(nums));
    }
            for(int i=first;i<n;i++){
            //将第i个整数到当前排列
            Collections.swap(nums,first,i);
            //添加下一个数字到排列中
            backtrace(n,nums,output,first+1);
            //回溯
            Collections.swap(nums,first,i);
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 47.含有相同元素求排列

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

示例:

输入: [1,1,2]
输出:
[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]
1
2
3
4
5
6
7
  • 回溯+剪枝算法
    • 对数组排序后,发现有重复元素则跳过当前分支,达到剪枝效果。
    • 在进入一个新的分支之前,看一看这个数是不是和之前的数一样,如果这个数和之前的数一样,并且之前的数还未使用过,那接下来如果走这个分支,就会使用到之前那个和当前一样的数,就会发生重复,此时分支和之前的分支一模一样
class Solution{
    private List<List<Integer>> res = new ArrayList<>();
    private boolean[] used;

    private void findPermuteUnique(int[] nums,int depth,Stack<Integer> stack){
        if(depth == nums.length){
            res.add(new ArrayList<>(stack));
            return ;
        }
        for(int i=0;i<nums.length;i++){
            if(!used[i]){
                //修改2:因为排序后重复的数一定不会出现在开始,故i>0
                //和之前的数相等,并且之前的数还未使用过,只有出现这种情况,
                //才会出现相同分支,跳过这种情况
                if(i>0&&nums[i]==nums[i-1]&&!used[i-1]){
                    continue;
                }
                used[i] = true;
                stack.add(nums[i]);
                findPermuteUnique(nums,depth+1,stack);
                stack.pop();
                used[i] = false;
            }
        }
    }

    public List<List<Integer>> permuteUnique(int[] nums){
        int len = nums.length;
        if(len==0){
            return res;
        }
        //修改1,首先排序,之后才可能发现重复分支
        Arrays.sort(nums);

        used = new boolean[len];
        findPermuteUnique(nums,0,new Stack<>());
        return res;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 77.组合

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

示例:

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
1
2
3
4
5
6
7
8
9
10
  • 回溯法 是一种通过遍历所有可能成员来寻找全部可行解的算法。若候选 不是 可行解 (或者至少不是 最后一个 解),回溯法会在前一步进行一些修改以舍弃该候选,换而言之, 回溯 并再次尝试。

这是一个回溯法函数,它将第一个添加到组合中的数和现有的组合作为参数。 backtrack(first, curr)

若组合完成- 添加到输出中。

遍历从 first t 到 n 的所有整数。

将整数 i 添加到现有组合 curr 中。

继续向组合中添加更多整数 : backtrack(i + 1, curr).

将 i 从 curr 中移除,实现回溯

链接:https://leetcode-cn.com/problems/combinations/solution/zu-he-by-leetcode/

class Solution{
    List<List<Integer>> output = new LinkedList();
    int n;
    int k;

    public void backtrace(int first,LinkedList<Integer> curr){
        //如果组合被使用了
        if(curr.size()==k){
            output.add(new LinkedList(curr));
        }

        for(int i=first;i<n+1;++i){
            //将i添加到当前组合中
            curr.add(i);
            //将下一个整数加入
            backtrace(i+1,curr);
            //回溯
            curr.removeLast();
        }
    }

    public List<List<Integer>> combine(int n,int k){
        this.n = n;
        this.k = k;
        backtrace(1,new LinkedList<Integer>());
        return output;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 39.组合求和

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

candidates  中的数字可以无限制重复被选取。

说明:

所有数字(包括  target)都是正整数。 解集不能包含重复的组合。  示例  1:

输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
  [7],
  [2,2,3]
]
1
2
3
4
5
6

回溯算法

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;

public class Solution {

    private List<List<Integer>> res = new ArrayList<>();
    private int[] candidates;
    private int len;

    private void findCombinationSum(int residue, int start, Stack<Integer> pre) {
        if (residue == 0) {
            // Java 中可变对象是引用传递,因此需要将当前 path 里的值拷贝出来
            res.add(new ArrayList<>(pre));
            return;
        }
        // 优化添加的代码2:在循环的时候做判断,尽量避免系统栈的深度
        // residue - candidates[i] 表示下一轮的剩余,如果下一轮的剩余都小于 0 ,就没有必要进行后面的循环了
        // 这一点基于原始数组是排序数组的前提,因为如果计算后面的剩余,只会越来越小
        for (int i = start; i < len && residue - candidates[i] >= 0; i++) {
            pre.add(candidates[i]);
            // 【关键】因为元素可以重复使用,这里递归传递下去的是 i 而不是 i + 1
            findCombinationSum(residue - candidates[i], i, pre);
            pre.pop();
        }
    }

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        int len = candidates.length;
        if (len == 0) {
            return res;
        }
        // 优化添加的代码1:先对数组排序,可以提前终止判断
        Arrays.sort(candidates);
        this.len = len;
        this.candidates = candidates;
        findCombinationSum(target, 0, new Stack<>());
        return res;
    }

    public static void main(String[] args) {
        int[] candidates = {2, 3, 6, 7};
        int target = 7;
        Solution solution = new Solution();
        List<List<Integer>> combinationSum = solution.combinationSum(candidates, target);
        System.out.println(combinationSum);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public List<List<Integer>> combinationSum(int[] candidates,int target){
    List<List<Integer>> combinations = new ArrayList<>();
    backtracking(new ArrayList<>(),combinations,0,target,candidates);
    return combinations;
}
private void backtracking(List<Integer> tempCombination,List<List<Integer>> combinations,int start,int target,final int[] candidates){
    if(target==0){
        combinations.add(new ArrayList<>(tempCombination));
        return;
    }

    for(int i=start;i<candidates.length;i++){
        if(candidates[i]<=target){
            tempConbination.add(candidates[i]);
            backtracking(tempCombination,combinations,i,target-candidates[i]);
            tempCombination.remove(tempCombination.size()-1);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 40.含有相同元素的组合求和

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

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

说明:

所有数字(包括目标数)都是正整数。 解集不能包含重复的组合。  示例  1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]
1
2
3
4
5
6
7
8
class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        int len = candidates.length;
        List<List<Integer>> res = new ArrayList<>();
        if(len == 0){
            return  res;
        }

        //先将数组排序,这一步很关键
        Arrays.sort(candidates);
        Deque<Integer> path = new ArrayDeque<>(len);
        dfs(candidates,len,0,target,path,res);
        return res;
    }
    /**
     * @param candidates 候选数组
     * @param len
     * @param begin      从候选数组的 begin 位置开始搜索
     * @param residue    表示剩余,这个值一开始等于 target,基于题目中说明的"所有数字(包括目标数)都是正整数"这个条件
     * @param path       从根结点到叶子结点的路径
     * @param res
     */
    private void dfs(int[] candidates,int len,int begin,int residue,Deque<Integer> path,List<List<Integer>> res){
        if(residue==0){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=begin;i<len;i++){
            //大剪枝
            if(residue-candidates[i]<0){
                break;
            }
            //小剪枝
            if(i>begin&&candidates[i]==candidates[i-1]){
                continue;
            }
            path.addLast(candidates[i]);

            //因为元素不可重复使用,这传递下去的是i+1而不是i
            dfs(candidates,len,i+1,residue-candidates[i],path,res);
            path.removeLast();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 10.1-9 数字组合

找出所有相加之和为  n 的  k  个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

所有数字都是正整数。 解集不能包含重复的组合。  示例 1:

输入: k = 3, n = 7 输出: [[1,2,4]] 示例 2:

输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]

回溯+剪枝

public List<List<Integer>> combinationSum3(int k,int n){
    List<List<Integer>> res = new ArrayList<>();
    //开始做一些特殊判断
    if(k<=0||n<=0||k>=n){
        return res;
    }
    //寻找n的上限:[9,8,7,...(9k+1)],他们的和为(19-k)*k/2
    //大于上限,结束搜索
    if(n>(19-k)*k/2){
        return res;
    }

    // 用deque代替stack
    Deque<Integer> path = new ArrayDeque<>();
    dfs(k,n,1,path,res);
    return res;
}

private void dfs(int k,int residue,int start,Deque<Integer> path,List<List<Integer>> res){
    //剪枝:[start,9] 区间里面的数不够k个,不用继续向下搜索
    if(10-start<k){
        return;
    }
    if(k==0){
        if(residue==0){
            res.add(new ArrayList<>(path));
            return;
        }
    }

    //枚举起点值[...,7,8,9]
    //找3个数,起点最多到7
    //找2个数,起点最多到8
    //规律是,起点上界+k=10,故起点上界=10-k

    for(int i=start;i<=10-k;i++){
        if(residue-i<0){
            break;
        }
        path.addLast(i);
        dfs(k-1,residue-i,i+1,path,res);
        path.removeLast();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 78. 子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 1 2

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        backtrace(0,nums,res,new ArrayList<Integer>());
        return res;
    }

    private void backtrace(int i,int[] nums,List<List<Integer>> res,ArrayList<Integer> tmp){
        res.add(new ArrayList<>(tmp));
        for(int j=i;j<nums.length;j++){
            tmp.add(nums[j]);
            backtrace(j+1,nums,res,tmp);
            tmp.remove(tmp.size()-1);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 二进制和位运算
public static List<List<Integer>> binaryBit(int[] nums){
    List<List<Integer>> res = newArrayList<List<Integer>>();
    for(int i=0;i<(1<<nums.length);i++){
        List<Integer> sub = new ArrayList<Integer>();
        for(int j=0;j<nums.length;i++){
            if(((i>>j)&1)==1)sub.add(nums[j]);
        }
        res.add(sub);
    }
    return res;
}
1
2
3
4
5
6
7
8
9
10
11

# 1.两数之和

给定一个整数数组 nums  和一个目标值 target,请你在该数组中找出和为目标值的那   两个   整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
1
2
3
4
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
// 1.利用hash 保存数组下标和数字
var twoSum = function(nums, target) {
  if (nums.length < 2) {
    return null;
  }
  var map = new Map();
  var res = [];
  map.set(nums[0], 0);
  for (let i = 1; i < nums.length; i++) {
    var temp = nums[i];
    if (map.has(target - temp)) {
      res.push(i);
      res.push(map.get(target - temp));
      return res;
    }
    map.set(nums[i], i);
  }
  return [];
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 【1046.最后一块石头的重量](https://leetcode-cn.com/problems/last-stone-weight/)

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为  x 和  y,且  x <= y。那么粉碎的可能结果如下:

如果  x == y,那么两块石头都会被完全粉碎; 如果  x != y,那么重量为  x  的石头将会完全粉碎,而重量为  y  的石头新重量为  y-x。 最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0。

示例:

输入:[2,7,4,1,8,1]
输出:1
解释:
先选出 78,得到 1,所以数组转换为 [2,4,1,1,1],
再选出 24,得到 2,所以数组转换为 [2,1,1,1],
接着是 21,得到 1,所以数组转换为 [1,1,1],
最后选出 11,得到 0,最终数组转换为 [1],这就是最后剩下那块石头的重量。

1
2
3
4
5
6
7
8
9
10
/**
 * @param {number[]} stones
 * @return {number}
 */

// 大顶堆
var lastStoneWeight = function(stones) {
  const pq = new MaxPriorityQueue();
  for (const stone of stones) {
    pq.enqueue("x", stone);
  }

  while (pq.size() > 1) {
    const a = pq.dequeue()["priority"];
    const b = pq.dequeue()["priority"];
    if (a > b) {
      pq.enqueue("x", a - b);
    }
  }
  return pq.isEmpty() ? 0 : pq.dequeue()["priority"];
};

// 暴力算法
var lastStoneWeight = function(stones) {
  var len = stones.length;
  if (len < 2) {
    return stones[0];
  }
  stones = stones.sort((a, b) => (a - b > 0 ? -1 : 1));
  while (stones.length > 1) {
    var first = stones.shift();
    var second = stones.shift();
    if (first == second) {
    } else {
      stones.push(first - second);
    }
    stones = stones.sort((a, b) => (a - b > 0 ? -1 : 1));
  }
  return stones.length < 1 ? 0 : stones[0];
};

// [大神解法](https://leetcode-cn.com/problems/last-stone-weight/solution/di-gui-die-dai-dui-lie-cha-pai-1xing-dai-70hv/)

var lastStoneWeight = function(stones) {
  stones = stones.sort((a, b) => a - b);
  if (stones.length > 1) {
    const temp = stones.shift() - stones.shift();
    if (temp) stones.push(temp);
    return lastStoneWight(stones);
  }
  return stones.length ? stones[0] : 0;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 435.无重叠区间

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 示例 1:

输入: [ [1,2], [2,3], [3,4], [1,3] ]

输出: 1

解释: 移除 [1,3] 后,剩下的区间没有重叠。

输入: [ [1,2], [1,2], [1,2] ]

输出: 2

解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

输入: [ [1,2], [2,3] ]

输出: 0

解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

解答

// 1.动态规划
var eraseOverlapIntervals = function(intervals) {
  if (!intervals.length) {
    return 0;
  }

  intervals.sort((a, b) => a[0] - b[0]);
  const n = intervals.length;
  const f = new Array(n).fill(1);

  for (let i = 1; i < n; i++) {
    for (let j = 0; j < i; j++) {
      if (intervals[j][1] <= intervals[i][0]) {
        f[i] = Math.max(f[i], f[j] + 1);
      }
    }
  }
  return n - Math.max(...f);
};

var eraseOverlapIntervals = function(intervals) {
  if (!intervals.length) {
    return 0;
  }

  intervals.sort((a, b) => a[0] - b[0]);
  const n = intervals.length;
  const f = new Array(n).fill(1);

  for (let i = 1; i < n; i++) {
    for (let j = 0; j < i; j++) {
      if (intervals[j][1] <= intervals[i][0]) {
        f[i] = Math.max(f[i], f[j] + 1);
      }
    }
  }
  return n - Math.max(...f);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 547. 省份数量

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2

提示:

1 <= n <= 200
n == isConnected.length
n == isConnected[i].length
isConnected[i][j]10
isConnected[i][i] == 1
isConnected[i][j] == isConnected[j][i]
1
2
3
4
5
6
7
8
9
10
11

官方题解

// 1.深度优先搜索 DFS

var findCircleNum = function(isConnected) {
  var provinces = isConnected.length;
  var visited = new Set();
  let circles = 0;
  for (let i = 0; i < provinces; i++) {
    if (!visited.has(i)) {
      dfs(isConnected, visited, provinces, i);
      circles++;
    }
  }
  return circles;
};

var dfs = (isConnected, visited, provinces, i) => {
  for (let j = 0; j < provinces; j++) {
    if (isConnected[i][j] == 1 && !visited.has(j)) {
      visited.add(j);
      dfs(isConnected, visited, provinces, j);
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 并查集

算法学习笔记:并查集


Last Updated: 2/4/2021, 12:42:02 AM