1.朋友圈问题
假设给你一个二维数组图中为1的位置为互相认识,该二维数组根据左对角线对称 ,{2,0}位置是1,说明2和0认识,{0,2},{0,4}位置为1说明0除了和2认识之外,还和4认识,虽然 2 和 4 互相不认识,但他们都和 0 认识,所以 {0,2,4} 是一个朋友圈,该问题是给你一个二维数组,求一共有多少个朋友圈?
分析:这是一个很典型的并查集的问题,0,1,2,3,4自己本来就认识自己,可以当做一个集合,假如说0和1认识,就合并0和1 将所有的合并完之后集合的数量就是该题朋友圈的数量。该题需要对经典并查集改写,该题是采用数组作为并查集来实现的。当然使用HashMap也可以,但是,使用HashMap它没有数组寻址速度快,所以我们采用数组来实现。因为该二维数据关于左对角线对称,所以我们只遍历右上角就行。
代码如下:
public class FriendCircle {
private int[] parent;
private int[] size;
private int[] help;
private Integer seats;
public FriendCircle(int N){
//parent[i] = k 说明 i位置的父亲是 k
parent = new int[N];
//size[i] = k 如果i是代表节点,size[i]才有意义,不是代表节点无意义,如果有意义,表示该代表节点所在集合大小为k
size = new int[N];
//辅助数组,用于压缩路径
help = new int[N];
//一个有多少个集合
seats = N;
for (int i = 0; i < N; i++) {
parent[i] = i;
size[i] = 1;
}
}
//一直往上,往上到不能再往上,是代表节点
//同时要做路径压缩
public int findFather(int a){
int index = 0;
while (a != parent[a]){
help[index] = a;
a = parent[a];
index++;
}
while (index > 0){
parent[help[--index]] = a;
}
return a;
}
public void union(int a,int b){
int aHead = findFather(a);
int bHead = findFather(b);
if (aHead != bHead){
if (size[aHead] >= size[bHead]){
size[aHead] += size[bHead];
parent[bHead] = aHead;
}else {
size[bHead] += size[aHead];
parent[aHead] = bHead;
}
seats--;
}
}
public static int findCircleNum(int[][] m){
int N = m.length;
FriendCircle friendCircle = new FriendCircle(N);
for (int i = 0; i < N; i++) {
for (int j = i+1; j < N; j++) {
//i 和 j 互相认识, i所在的集合 和 j 所在的集合合并
if (m[i][j] == 1){
friendCircle.union(i,j);
}
}
}
return friendCircle.seats;
}
}
2.岛问题
给定一个二维数组matrix,里面的值不是 1 就是 0。上下左右相邻的 1 视一片岛,返回matrix中岛的数量。
利用递归实现:
当我们发现一个位置是'1'时,我们就把他的上下左右都改成0,我们看一共调了多少次infect方法,就是有多少个岛。
public static int numIsland(char[][] arr){
int isLand = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[0].length; j++) {
if (arr[i][j] == '1'){
infect(arr,i,j);
isLand++;
}
}
}
return isLand;
}
//从 i,j 这个位置出发,把所有'1'位置变成0
public static void infect(char[][] arr,int i,int j){
if (i < 0 || j < 0 || i == arr.length || j== arr[0].length || arr[i][j] != '1'){
return;
}
arr[i][j] = 0;
infect(arr,i,j-1);
infect(arr,i,j+1);
infect(arr,i-1,j);
infect(arr,i+1,j);
}
下面介绍一下使用并查集来实现,其实这道题用递归来实现已经是最优解了,那为啥还要用并查集来实现呢,其一就是可以练习并查集,其二 岛问题的加强版就只能用并查集来实现,用递归就不行了,首先 ,使用并查集实现如下:
利用并查集实现:
根据第一题我们知道,并查集都是一维的,那这题我们给的是二维数组,该怎么办呢?
我们可以把它变成一维的,在我们想象的空间中,我们把第二列拼到第一列后面,假如说一共有 3行4列,第二行第二列,转换成一维就是 1 * 4 + 2 = 6 1是第二行下标为 1,4 是一共四列 , 2 是第二列。这样就可以把一个二维数组转换为一维数据,当然,这么写的话当行数过大时,可能会有溢出风险,但如果我们的数据量真到达那么大的时候,我们可以再想其他方法。譬如将并查集改为二维数组的形式,不过该题还是用一维数组来实现。
public class UnionFind{
private int[] parent;
private int[] size;
private int[] help;
private int col;
private int sits;
public UnionFind(char[][] arr){
int row = arr.length;
col = arr[0].length;
int N = row * col;
parent = new int[N];
size = new int[N];
help = new int[N];
sits = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[0].length; j++) {
if (arr[i][j] == '1'){
int index = index(i, j);
parent[index] = index;
size[index] = 1;
sits++;
}
}
}
}
public int index(int i,int j){
return i * col + j;
}
//原始位置下标
private int find(int a){
int index = 0;
while (a != parent[a]){
help[index] = a;
a = parent[a];
index++;
}
while (index > 0){
parent[help[--index]] = a;
}
return a;
}
public void union(int a1,int a2,int b1,int b2){
int aHead = find(index(a1,a2));
int bHead = find(index(b1,b2));
if (aHead != bHead){
if (size[aHead] >= size[bHead]){
size[aHead] += size[bHead];
parent[bHead] = aHead;
}else {
size[bHead] += size[aHead];
parent[aHead] = bHead;
}
sits--;
}
}
}
public int numIsLandUnionFind(char[][] grid){
UnionFind unionFind = new UnionFind(grid);
int col = grid[0].length;
int row = grid.length;
for (int i = 1; i < col; i++) {
if (grid[0][i] == '1' && grid[0][i-1] == '1'){
unionFind.union(0,i,0,i-1);
}
}
for (int i = 1; i < row; i++) {
if (grid[i][0] == '1' && grid[i-1][0] == '1'){
unionFind.union(i,0,i-1,0);
}
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (grid[i][j] == '1'){
if (grid[i-1][j] == '1'){
unionFind.union(i-1,j,i,j);
}
if (grid[i][j-1] == '1'){
unionFind.union(i,j-1,i,j);
}
}
}
}
return unionFind.sits;
}
岛问题 II
给一个m和一个n。 一个position{[0,0],[0,1],[1,2],[2,1]}
m 和n 形成一个二维的数组,初始全为0, 第一个position[0,0] 二维数组中 0,0 位置变为 1 这是有 几个岛? 1 个 第二个position[0,1] 数组中 [0,1]位置变1 这是还是1个岛,第三个position[1,2],二维数组中[1,2]变1,这是数组中有两个岛,第四个position[2,1],数组中[2,1]位置变1,这时候数组中有3个岛,需要输出的就是以上每个位置有多少个岛。
这道题就不能用递归来写了,用并查集是最好的写法。因为并查集有一个很重要的技巧就是动态连接,我们每空降一个位置,就动态的初始化去连接。
使用并查集实现:
public class UnionFind2{
private int[] parent;
private int[] size;
private int[] help;
private int col;
private int row;
private int sits;
public UnionFind2(int m,int n){
parent = new int[m * n];
size = new int[m * n];
help = new int[m * n];
col = n;
row = m;
sits = 0;
}
public int find(int num){
int index = 0;
while (num != parent[num]){
help[index++] = num;
num = parent[num];
}
while (index > 0){
parent[help[--index]] = num;
}
return num;
}
public int index(int m,int n){
return m * col + n;
}
public void union(int r1,int c1,int r2,int c2){
if (r1 < 0 || r1 == row || r2 < 0 || r2 == row || c1 < 0 || c1 ==col || c2 < 0 || c2 == col){
return;
}
int aPosition = index(r1,c1);
int bPosition = index(r2,c2);
if (size[aPosition] == 0 || size[bPosition] == 0){
return;
}
int f1 = find(aPosition);
int f2 = find(bPosition);
if (f1 != f2){
if (size[f1] >= size[f2]){
size[f1] += size[f2];
parent[f2] = f1;
}else {
size[f2] += size[f1];
parent[f1] = f2;
}
sits--;
}
}
public int connet(int r,int c){
int index = index(r,c);
if (size[index] == '0'){
parent[index] = index;
size[index] = 1;
sits++;
union(r-1,c,r,c);
union(r+1,c,r,c);
union(r,c-1,r,c);
union(r,c+1,r,c);
}
return sits;
}
}
public List<Integer> numIsLand2(int m,int n,int[][] position){
UnionFind2 unionFind2 = new UnionFind2(m,n);
List<Integer> list = new ArrayList<>();
for (int[] ints : position) {
list.add(unionFind2.connet(ints[0],ints[1]));
}
return list;
}