两个题解法其实是一致的,当多练一遍
1. 题目
- 给定一个由’1’(陆地)和’0’(水)组成的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或者垂直方向相邻的陆地连接而成。你可以假设网格的四个边均被水包围。https://leetcode-cn.com/problems/number-of-islands/
- 班上有N名学生,其中有些人是朋友,有些则不是。他们的友谊具有传递性。如果已知A是B的朋友,B是C的朋友,那么我们可以认为A也是C的朋友。所谓的朋友圈就是朋友的集合。https://leetcode-cn.com/problems/friend-circles/
2. 基本知识
2.1 并查集
-
定义
英文是(union&find)是一种树形的数据结构,用于处理一些不交集的合并和查询问题
-
find
确定元素属于哪一个子集,它可以被用来确定两个元素是否属于同一个子集
-
union
将两个子集合并成一个集合
2.2 生活中的例子
- 小弟->老大(黑帮人员的从属关系)
2.3 并查集的两种优化
-
路径压缩
寻找根节点,如果元素过多,就会比较复杂,树形层次太多。路径压缩就是:把祖先节点作为所有子孙节点的parent,这样祖先节点和所有子孙节点都是直连的,find操作就变成O(1)的时间复杂度。
-
按秩合并(rank合并)
秩表示树的高度,在合并的时候,总是将较小秩的树根指向较大的树根,这样合并后的数秩就不会变大。
2.4 并查集的代码实现
public class QuickUnionF {
private int[] roots;
/**
* 构造新的并查集
* @param N 并查集的长度
*/
public QuickUnionF(int N) {
this.roots = new int[N];
for (int i = 0; i < N; i++) {
// 表示自己指向自己
roots[i] = i;
}
}
/**
* 查看元素属于哪个集合,路径压缩版
* @param element 元素
* @return 元素所在集合
*/
public int find(int element) {
// 先找到字集所在的集合
int root = element;
// 只要节点没指向自己,继续遍历,直接找到所属集合为止
while (root != roots[root]) {
//把当前的所属集合取出来
root = roots[element];
}
//进行路径压缩
while(root != roots[element]) {
roots[element] = root;
}
return roots[element];
}
/**
* 判断两个元素是否在同一个集合
*/
public boolean isConnectd(int firstElement, int secondElement) {
return find(firstElement) == find(secondElement);
}
/**
* 两个字集合合并
*/
public void unionElements(int firstElement, int secondElement) {
int firstUnion = find(firstElement);
int secondUnion = find(secondElement);
if (firstUnion != secondUnion) {
roots[firstUnion] = secondUnion;
}
}
3. 算法题解
3.1 给定一个由’1’(陆地)和’0’(水)组成的二维网格,计算岛屿的数量。
一个岛被水包围,并且它是通过水平方向或者垂直方向相邻的陆地连接而成。你可以假设网格的四个边均被水包围。
示例:
11000
11000
00100
00011
输出:3
3.1.1 解法1:染色法(Flood fill)
遍历所有节点,如果节点为1,count++,然后将周围的是1的节点染色改为0,最后得到count的值即岛屿的个数。
-
DFS
public static int numsIsLands(char[][] grid) { int x = grid.length; int y = grid[0].length; int count = 0; for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { if (grid[i][j] == '1') { // count++ count++; // 将周边的1给换成0 dfsFloodFill(grid, i, j); } } } return count; } private static void dfsFloodFill(char[][] grid, int i, int j) { int x = grid.length; int y = grid[0].length; //边界及0不处理 if (i < 0 || j < 0 || i >=x || j >= y || grid[i][j] == '0') return; // 染色为'0' grid[i][j] = '0'; dfsFloodFill(grid, i-1, j); dfsFloodFill(grid, i, j-1); dfsFloodFill(grid, i+1,j); dfsFloodFill(grid, i, j+1); }
-
BFS
遍历逻辑跟DFS一样,染色的逻辑使用的广度优先搜索,借助LinkedList数据结构,将相邻节点放到暂存list中,挨个取出进行判断,直到周边节点为0为止。
public static int numsIsLands1(char[][] grid) { int x = grid.length; int y = grid[0].length; int count = 0; for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { if (grid[i][j] == '1') { // count++ count++; // 将周边的1给换成0 bfsFloodFill(grid, i, j); } } } return count; } private static void bfsFloodFill(char[][] grid, int i, int j) { LinkedList<int[]> chars = new LinkedList<>(); int x = grid.length; int y = grid[0].length; chars.add(new int[]{i, j}); while (!chars.isEmpty()) { int[] cur = chars.remove(); int a = cur[0]; int b = cur[1]; if (a < 0 || b < 0 || a >= x || b >= y || grid[a][b] == '0') continue; grid[a][b] = '0'; chars.add(new int[]{a - 1, b}); chars.add(new int[]{a + 1, b}); chars.add(new int[]{a, b - 1}); chars.add(new int[]{a, b + 1}); } }
3.1.2 解法2:并查集
-
初始化并查集,把所有为’1’的节点都指向自己
-
把相邻的’1’进行合并
-
遍历查看有多少个不同的集合
class UnionFind { int count; int[] parent; int[] rank; public UnionFind(char[][] grid) { count = 0; int x = grid.length; int y = grid[0].length; parent = new int[x * y]; rank = new int[x * y]; // 初始化并查集 for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { int index = i * x + j; if (grid[i][j] == '1') { parent[index] = index; count++; } rank[index] = 0; } } } public int find(int index) { if (index != parent[index]) parent[index] = find(parent[index]); return parent[index]; } public void union(int x, int y) { int rootX = find(x); int rootY = find(y); // rank合并优化 if (rootX != rootY) { if (rank[rootX] > rank[rootY]) { parent[rootY] = rootX; } if (rank[rootX] < rank[rootY]) { parent[rootX] = rootY; } if (rank[rootX] == rank[rootY]) { parent[rootX] = rootY; rank[rootY] += 1; } --count; } } public int getCount() { return count; } } public int numsIsLands(char[][] grid) { if (grid == null || grid.length == 0) return 0; int x = grid.length; int y = grid[0].length; // 1. 初始化并查集 UnionFind unionFind = new UnionFind(grid); // 2. 遍历合并为1的节点 for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { if (grid[i][j] == '1') { grid[i][j] = '0'; // 合并上下左右的为1的节点(上下是指座标的上下) // 合并上一个节点 if (j - 1 >= 0 && grid[i][j - 1] == '1') { unionFind.union(i * x + j, i * x + j - 1); } // 合并下一个节点 if (j + 1 < y && grid[i][j + 1] == '1') { unionFind.union(i * x + j, i * x + j + 1); } // 合并左边的节点 if (i - 1 >= 0 && grid[i - 1][j] == '1') { unionFind.union(i * x + j, (i - 1) * x + j); } //合并右边的节点 if (i + 1 < x && grid[i + 1][j] =='1') { unionFind.union(i * x + j, (i + 1) * x + j); } } } } // 3. 获取不同的集合个数 return unionFind.getCount(); }
3.2 班上有N名学生,其中有些人是朋友,有些则不是。他们的友谊具有传递性。如果已知A是B的朋友,B是C的朋友,那么我们可以认为A也是C的朋友。所谓的朋友圈就是朋友的集合。
给定一个N*N的矩阵M,表示班级中学生之间的朋友关系。如果M[i][j]=1,表示已知第i个和第j个学生之间互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
示例:
输入:
[[1,1,0].
[1,1,0],
[0,0,1]]
输出:2
3.2.1 染色法
-
dfs遍历染色
int x; int y; public int numsFriendCycles(int[][] grid) { x = grid.length; y = grid[0].length; int count = 0; for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { if (grid[i][j] == 1) { count++; //dfs染色 dfsFloodFill(grid, i, j); } } } return count; } private void dfsFloodFill(int[][] grid, int i, int j) { if (i < 0 || j < 0 || i >= x || j >=y || grid[i][j] == 0) return; grid[i][j] = 0; dfsFloodFill(grid, i - 1, j); dfsFloodFill(grid, i + 1, j); dfsFloodFill(grid, i, j - 1); dfsFloodFill(grid, i, j + 1); }
-
bfs
int x; int y; public int numsFriendCycles1(int[][] grid) { x = grid.length; y = grid[0].length; int count = 0; for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { if (grid[i][j] == 1) { count++; //bfs染色 bfsFloodFill(grid, i, j); } } } return count; } // 这里面的i,j 和循环里面的a和b不能混淆 private void bfsFloodFill(int[][] grid, int i, int j) { LinkedList<int[]> bfsList = new LinkedList<>(); bfsList.add(new int[]{i, j}); while (!bfsList.isEmpty()) { int[] item = bfsList.remove(); int a = item[0]; int b = item[1]; if (a < 0 || b < 0 || a >= x || b >=y || grid[a][b] == 0) continue; grid[a][b] = 0; // 将周边节点加入辅助Queue bfsList.add(new int[]{a-1, b}); bfsList.add(new int[]{a + 1, b}); bfsList.add(new int[]{a, b - 1}); bfsList.add(new int[] {a, b + 1}); } }
3.2.2 并查集
经过分析,可以看做一个并查集的问题,自己指向自己,然后合并相邻的节点,最后看有多少个集合即可。
-
先将所有1的节点初始化
-
遍历,如果当前节点为1,则将周边的所有1都合并到一个集合
-
得出总的集合数
class FriendUnionF{ int parent[]; int rank[]; int count = 0; public FriendUnionF(int[][] grid) { int m = grid.length; int n = grid[0].length; parent = new int[m * n]; rank = new int[m * n]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { int index = i * m + j; if (grid[i][j] == 1) { parent[index] = index; count ++; } rank[index] = 0; } } } public int find(int m) { if (parent[m] != m) { parent[m] = find(parent[m]); } return parent[m]; } // 合并时采用按rank合并优化 public void union(int m, int n) { // 分别查找root int rootM = find(m); int rootN = find(n); if (rootM != rootN) { if (rank[rootM] > rank[rootN]) { parent[rootN] = rootM; }else if (rank[rootM] < rank[rootN]) { parent[rootM] = rootN; }else{ parent[rootN] = rootM; rank[rootM] += 1; } --count; } } } public int numsFriendCycles(int[][] grid) { // 1. 先将所有1的节点初始化 FriendUnionF friendUnionF = new FriendUnionF(grid); int m = grid.length; int n = grid[0].length; // 遍历,如果当前节点为1,则将周边的所有1都合并到一个集合 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == 1) { grid[i][j] = 0; int index = i * m + j; //将周边的1合并 if (i - 1 >= 0 && grid[i - 1][j] == 1) { // 注意第二个参数简化的时候不要出错,第一遍就因为这个参数简化导致出现问题 friendUnionF.union(index, (i - 1) * m + j); } if (i + 1 < m && grid[i + 1][j] == 1) { friendUnionF.union(index, (i + 1) * m + j); } if (j - 1 >= 0 && grid[i][j - 1] == 1) { friendUnionF.union(index, i * m + j - 1); } if (j + 1< n && grid[i][j + 1] == 1) { friendUnionF.union(index, i * m + j + 1); } } } } return friendUnionF.count; }