1. 并查集

并查集是树形结构,常常在题目中用来判断两个元素是否属于同一个集合,每个集合都有一个特征性元素称为这个集合的father,如果两个元素的father相同,则说明这两个元素属于同一集合,若这两个元素的father不相同,则说明这两个元素不属于一个集合。并查集就是这样一种支持合并和查询的树形数据结构。

  • 初始化
    把每个点所在集合初始化为其自身。
    通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为。
  • 查找
    查找元素所在的集合,即根节点。
  • 合并
    将两个元素所在的集合合并为一个集合。
    通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。

2. 547. 省份数量

2.1. 递归方法

  1. public int findCircleNum(int[][] isConnected) {
  2. int n = isConnected.length;
  3. UnionFind unionFind = new UnionFind(n);
  4. for (int i = 0; i < isConnected.length; i++) {
  5. for (int j = i + 1; j < isConnected[i].length; j++) {
  6. if (isConnected[i][j] == 1) { // j和i 相互认识
  7. unionFind.union(i, j);
  8. }
  9. }
  10. }
  11. return unionFind.sets;
  12. }
  13. public static class UnionFind {
  14. static int sets;
  15. static int[] arr;
  16. public UnionFind(int n) {
  17. this.sets = n;
  18. this.arr = new int[n];
  19. for (int i = 0; i < arr.length; i++) {
  20. arr[i] = i;// 初始化并查集数组 每个节点父元素为自己
  21. }
  22. }
  23. // 找父节点
  24. public static int find(int a) {
  25. if (a == arr[a]) {
  26. return a;
  27. }
  28. arr[a] = find(arr[a]);//路径压缩
  29. return arr[a];
  30. }
  31. //合并节点
  32. public void union(int i, int j) {
  33. if(find(i) != find(j)){ //如果他们两的父节点 不是一个
  34. arr[find(i)] = find(j); //
  35. sets--;
  36. }
  37. }
  38. }

2.2. 数组栈实现

  1. public int findCircleNum(int[][] isConnected) {
  2. int n = isConnected.length;
  3. UnionFind unionFind = new UnionFind(n);
  4. for (int i = 0; i < isConnected.length; i++) {
  5. for (int j = i + 1; j < isConnected[i].length; j++) {
  6. if (isConnected[i][j] == 1) {
  7. unionFind.union(i, j); // 两节点认识 合并节点
  8. }
  9. }
  10. }
  11. return unionFind.getSets();
  12. }
  13. public static class UnionFind {
  14. // 并查集数组
  15. private int[] parent;
  16. // i所在的集合 大小是多少
  17. private int[] size;
  18. // 辅助结构 用于实现栈
  19. private int[] help;
  20. // 一共有多少个集合
  21. private int sets;
  22. public UnionFind(int n) {
  23. parent = new int[n];
  24. size = new int[n];
  25. help = new int[n];
  26. sets = n;
  27. // 初始化并查集数组 每个节点的父节点是自身
  28. for (int i = 0; i < parent.length; i++) {
  29. parent[i] = i;
  30. size[i] = 1; // 只有自己一个节点
  31. }
  32. }
  33. // 查找父节点
  34. public int find(int i) {
  35. int hi = 0;// 辅助数组下标
  36. // 找父节点
  37. while (i != parent[i]) {
  38. help[hi++] = i;// 记录路径
  39. i = parent[i];
  40. }
  41. // 路径压缩
  42. for (hi--; hi >= 0; hi--) {
  43. parent[help[hi]] = i; // 将路径上的节点的父节点 全部标记为最顶的父节点i
  44. }
  45. return i; // 返回最顶的父节点
  46. }
  47. // 合并节点
  48. public void union(int i, int j) {
  49. int f1 = find(i); // 找到这个两节点的父节点
  50. int f2 = find(j);
  51. if (f1 != f2) { // 他们两认识 父节点又不相同 进行合并
  52. if (size[f1] >= size[f2]) { // 将小size的数组 合并到大size的去
  53. size[f1] += size[f2];
  54. parent[f2] = f1; // 更新f2的父节点为f1的父节点
  55. } else {
  56. size[f2] += size[f1];
  57. parent[f1] = f2;
  58. }
  59. sets--;
  60. }
  61. }
  62. public int getSets() {
  63. return sets;
  64. }
  65. public void setSets(int sets) {
  66. this.sets = sets;
  67. }
  68. }

3. 200. 岛屿数量

感染问题,经典写法使用dfs搜索,但可以改为并查集实现。

3.1. 深搜写法

  1. public int numIslands(char[][] grid) {
  2. int ans = 0;
  3. for (int i = 0; i < grid.length; i++) {
  4. for (int j = 0; j < grid[i].length; j++) {
  5. if (grid[i][j] == '1') {
  6. ans++;
  7. dfs(grid, i, j);
  8. }
  9. }
  10. }
  11. return ans;
  12. }
  13. private void dfs(char[][] grid, int i, int j) {
  14. if (i < 0 || i == grid.length || j < 0 || j == grid[i].length || grid[i][j] != '1') {
  15. // 越界 或 当前岛屿不是1(陆地)
  16. return;
  17. }
  18. grid[i][j] = 3; //感染更改标记
  19. dfs(grid, i + 1, j);
  20. dfs(grid, i - 1, j);
  21. dfs(grid, i, j + 1);
  22. dfs(grid, i, j - 1);
  23. }

3.2. 包装成类存储写法

由于0,0是没有数据,我们先遍历第0行和第0列的岛屿(优化时间),再遍历其他岛屿,每个岛屿只搜索上方和左方岛屿。

这里我们将岛屿为1的位置存储为我们的自定义类,存储的为内存地址防止多个岛屿值相同情况,

  1. public static int numIslands1(char[][] board) {
  2. int row = board.length;
  3. int col = board[0].length;
  4. Dot[][] dots = new Dot[row][col];
  5. List<Dot> dotList = new ArrayList<>();
  6. for (int i = 0; i < row; i++) {
  7. for (int j = 0; j < col; j++) {
  8. if (board[i][j] == '1') {
  9. dots[i][j] = new Dot();
  10. dotList.add(dots[i][j]);
  11. }
  12. }
  13. }
  14. UnionFind1<Dot> uf = new UnionFind1<>(dotList);
  15. for (int j = 1; j < col; j++) {
  16. // (0,j) (0,0)跳过了 (0,1) (0,2) (0,3)
  17. if (board[0][j - 1] == '1' && board[0][j] == '1') {
  18. uf.union(dots[0][j - 1], dots[0][j]);
  19. }
  20. }
  21. for (int i = 1; i < row; i++) {
  22. if (board[i - 1][0] == '1' && board[i][0] == '1') {
  23. uf.union(dots[i - 1][0], dots[i][0]);
  24. }
  25. }
  26. for (int i = 1; i < row; i++) {
  27. for (int j = 1; j < col; j++) {
  28. if (board[i][j] == '1') {
  29. if (board[i][j - 1] == '1') {
  30. uf.union(dots[i][j - 1], dots[i][j]);
  31. }
  32. if (board[i - 1][j] == '1') {
  33. uf.union(dots[i - 1][j], dots[i][j]);
  34. }
  35. }
  36. }
  37. }
  38. return uf.sets();
  39. }
  40. public static class Dot {
  41. }
  42. public static class Node<V> {
  43. V value;
  44. public Node(V v) {
  45. value = v;
  46. }
  47. }
  48. public static class UnionFind1<V> {
  49. public HashMap<V, Node<V>> nodes;
  50. public HashMap<Node<V>, Node<V>> parents;
  51. public HashMap<Node<V>, Integer> sizeMap;
  52. public UnionFind1(List<V> values) {
  53. nodes = new HashMap<>();
  54. parents = new HashMap<>();
  55. sizeMap = new HashMap<>();
  56. for (V cur : values) {
  57. Node<V> node = new Node<>(cur);
  58. nodes.put(cur, node);
  59. parents.put(node, node);
  60. sizeMap.put(node, 1);
  61. }
  62. }
  63. public Node<V> findFather(Node<V> cur) {
  64. Stack<Node<V>> path = new Stack<>();
  65. while (cur != parents.get(cur)) {
  66. path.push(cur);
  67. cur = parents.get(cur);
  68. }
  69. while (!path.isEmpty()) {
  70. parents.put(path.pop(), cur);
  71. }
  72. return cur;
  73. }
  74. public void union(V a, V b) {
  75. Node<V> aHead = findFather(nodes.get(a));
  76. Node<V> bHead = findFather(nodes.get(b));
  77. if (aHead != bHead) {
  78. int aSetSize = sizeMap.get(aHead);
  79. int bSetSize = sizeMap.get(bHead);
  80. Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
  81. Node<V> small = big == aHead ? bHead : aHead;
  82. parents.put(small, big);
  83. sizeMap.put(big, aSetSize + bSetSize);
  84. sizeMap.remove(small);
  85. }
  86. }
  87. public int sets() {
  88. return sizeMap.size();
  89. }
  90. }

3.3. 将二维数组转为一维写法

二维转一维:i*列数+j

  1. public int numIslands(char[][] grid) {
  2. int row = grid.length; // 行
  3. int col = grid[0].length; // 列
  4. UnionFind uf = new UnionFind(grid);
  5. for (int i = 1; i < col; i++) {
  6. if (grid[0][i - 1] == '1' && grid[0][i] == '1') {
  7. uf.union(0, i - 1, 0, i);
  8. }
  9. }
  10. for (int i = 1; i < row; i++) {
  11. if (grid[i - 1][0] == '1' && grid[i][0] == '1') {
  12. uf.union( i - 1, 0, i,0);
  13. }
  14. }
  15. for (int i = 1; i < row; i++) {
  16. for (int j = 1; j < col; j++) {
  17. if (grid[i][j] == '1') {
  18. if (grid[i][j - 1] == '1') {
  19. uf.union(i, j - 1, i, j);
  20. }
  21. if (grid[i - 1][j] == '1') {
  22. uf.union(i - 1, j, i, j);
  23. }
  24. }
  25. }
  26. }
  27. return uf.getSets();
  28. }
  29. public static class UnionFind {
  30. int[] parent; // 并查集数组
  31. int[] size; // 存储为大小
  32. int[] help; // 压缩路径辅助数组 用于模拟栈
  33. int col; // 列
  34. int sets; // 父节点数量
  35. // 初始化
  36. public UnionFind(char[][] grid) {
  37. col = grid[0].length; // 列
  38. sets = 0;
  39. int row = grid.length; // 行
  40. int len = row * col;// 陆地和水的总数量
  41. parent = new int[len];
  42. size = new int[len];
  43. help = new int[len];
  44. for (int i = 0; i < row; i++) {
  45. for (int j = 0; j < col; j++) {
  46. if (grid[i][j] == '1') {
  47. int ind = index(i, j);
  48. parent[ind] = ind;
  49. size[ind] = 1;
  50. sets++;
  51. }
  52. }
  53. }
  54. }
  55. private int index(int i, int j) {
  56. // 二维转一维:i*列数+j
  57. return i * col + j;
  58. }
  59. private int find(int i) {
  60. int hi = 0;
  61. // 找父节点
  62. while (i != parent[i]) {
  63. help[hi++] = i;
  64. i = parent[i];
  65. }
  66. // 压缩路径
  67. for (hi--; hi >= 0; hi--) {
  68. parent[help[hi]] = i;
  69. }
  70. return i;
  71. }
  72. private void union(int r1, int c1, int r2, int c2) {
  73. int a = index(r1, c1);
  74. int b = index(r2, c2);
  75. int f1 = find(a);
  76. int f2 = find(b);
  77. if (f1 != f2) {
  78. if (size[f1] >= size[f2]) {
  79. size[f1] += size[f2];
  80. parent[f2] = f1;
  81. } else {
  82. size[f2] += size[f1];
  83. parent[f1] = f2;
  84. }
  85. sets--; // 减少父节点数量
  86. }
  87. }
  88. public int getSets() {
  89. return sets;
  90. }
  91. public void setSets(int sets) {
  92. this.sets = sets;
  93. }
  94. }

4. 305.岛屿数据 II

m行和列的 2d 网格图n最初充满水。我们可以执行addLand操作,将位置 (row, col) 的水变成陆地。给定要操作的位置列表,计算每次addLand操作后的岛数。岛屿四面环水,由相邻陆地水平或垂直连接而成。您可以假设网格的所有四个边缘都被水包围。

例子:

  1. 输入: m = 3n = 3,位置 = [[0,0],[0,1],[1,2],[2,1]]
  2. 输出: [1,1,2,3]

解释:

最初,二维网格grid充满水。(假设 0 代表水,1 代表土地)。

  1. 0 0 0
  2. 0 0 0
  3. 0 0 0

操作 #1:addLand(0, 0) 将 grid[0][0] 处的水变成陆地。

  1. 1 0 0
  2. 0 0 0 岛屿数 = 1
  3. 0 0 0

操作 #2:addLand(0, 1) 将 grid[0][1] 处的水变成陆地。

  1. 1 1 0
  2. 0 0 0 岛屿数 = 1
  3. 0 0 0

操作 #3:addLand(1, 2) 将 grid[1][2] 处的水变成陆地。

  1. 1 1 0
  2. 0 0 1 岛屿数 = 2
  3. 0 0 0

操作 #4:addLand(2, 1) 将 grid[2][1] 处的水变成陆地。

  1. 1 1 0
  2. 0 0 1 岛屿数 = 3
  3. 0 1 0
  1. public static List<Integer> numIslands21(int m, int n, int[][] positions) {
  2. UnionFind unionFind = new UnionFind(m, n);
  3. List<Integer> ans = new ArrayList<>();
  4. for (int[] i : positions) {
  5. ans.add(unionFind.connect(i[0], i[1]));
  6. }
  7. return ans;
  8. }
  9. public static class UnionFind {
  10. int[] parent;
  11. int[] help;
  12. int[] size;
  13. final int row;
  14. final int col;
  15. int sets;
  16. public UnionFind(int row, int col) {
  17. this.row = row;
  18. this.col = col;
  19. int len = row * col;
  20. parent = new int[len];
  21. help = new int[len];
  22. size = new int[len];
  23. sets = 0;
  24. }
  25. public int index(int r, int c) {
  26. return r * col + c;
  27. }
  28. public int find(int i) {
  29. int hi = 0;
  30. while (i != parent[i]) {
  31. help[hi++] = i;
  32. i = parent[i];
  33. }
  34. for (hi--; hi >= 0; hi--) {
  35. parent[help[hi]] = i;
  36. }
  37. return i;
  38. }
  39. public void union(int r1, int c1, int r2, int c2) {
  40. if (r1 < 0 || r1 == row || c1 < 0 || c1 == col || r2 < 0 || r2 == row || c2 < 0 || c2 == col) {
  41. // 边界处理
  42. return;
  43. }
  44. int a = index(r1, c1);
  45. int b = index(r2, c2);
  46. if (size[a] == 0 || size[b] == 0) {
  47. // 两节点 有任意一节点未经过 合并无意义
  48. return;
  49. }
  50. int f1 = find(a);
  51. int f2 = find(b);
  52. if (f1 != f2) {
  53. if (size[f1] >= size[f2]) {
  54. size[f1] += size[f2];
  55. parent[f2] = f1;
  56. } else {
  57. size[f2] += size[f1];
  58. parent[f1] = f2;
  59. }
  60. sets--;
  61. }
  62. }
  63. public int connect(int r, int c) {
  64. int index = index(r, c);
  65. // 该节点之前没有经过的
  66. if (size[index] == 0) {
  67. // 初始化
  68. parent[index] = index;
  69. size[index] = 1;
  70. sets++;
  71. // 上下左右合并
  72. union(r + 1, c, r, c);
  73. union(r - 1, c, r, c);
  74. union(r, c + 1, r, c);
  75. union(r, c - 1, r, c);
  76. }
  77. return sets;
  78. }
  79. }