并查集
1. 并查集原理与实现
原文参考链接:一个非常实用而且精妙的算法-并查集
1.1 原理
话说在江湖上有很多门派,这些门派相互争夺武林霸主。毕竟是江湖中人,两个人见面一言不合就开干。但是打归打,总是要判断一下是不是自己人,免得误伤。
于是乎,分了各种各样的门派,比如说张无忌和岳灵珊俩人要打架,就先看看是不是同一门派的,不是的话那就再开干。要是张无忌和岳灵珊觉得俩人合得来,那就合并门派。
而且规定了,每一个门派都有一个掌门人,比如武当派就是张三丰。华山派就是岳不群等等。
现在我们把目光转到并查集上。
(1)张无忌和岳灵珊打架之前,先判断是否是同一门派,这就涉及到了并查集的查找操作。
(2)张无忌和岳灵珊觉得俩人合得来,那就合并门派,这就涉及到了并查集的合并操作。
(3)每一个门派都有一个掌门人,这涉及到了并查集的存储方式。掌门人代表了这个门派的根节点。
现在我们从这个例子的思想开始认识一下并查集。
1.2 简单实现
我们实现一个并查集的时候首先要考虑的就是存储结构,一般情况下有两种:数组和链表。现在我们使用哈希表来实现一下。 使用哈希表的优势是:能快速的找到自己所属门派。
并查集主要涉及到两种操作,合并和查找。 假设有一个链表List< Node>,其中每个元素都是未入江湖的习武之人。makeSets(nodes)
操作将这帮人推入江湖斗争之中。
-
类架构
public static class Node{//一个人未入江湖 } public static class UnionFindSet{ public HashMap<Node, Node> fatherMap;//map中,value是key的掌门人 public HashMap<Node, Integer> sizeMap;//map中,key:某门派的掌门人,value:该门派总共有几人 public UnionFindSet(List<Node> nodes){ makeSets(nodes); } //划分门派 private void makeSets(List<Node> nodes) { fatherMap = new HashMap<>(); sizeMap = new HashMap<>(); for(Node node : nodes){ fatherMap.put(node, node);//每个人初入江湖,都自成一派,当然掌门人就是自己。 sizeMap.put(node, 1);//自己一派,则以node为掌门人的门派中总共有1个人。 } } }
-
查找操作
Find操作就是查找某个元素所在的集合,返回该集合的代表元素。通俗的理解就是根据张无忌找到其相应门派的掌门人张三丰。
private Node findHead(Node node){ Node father = fatherMap.get(node); if(father != node){ return findHead(father); } return father; }
这样做,每次查找掌门人的时候,都要一级一级的往上找,怎么改进呢?
-
合并操作
Union操作就是将两个不相交的子集合合并成一个大集合。如何去合并呢?其实原理很简单,只需要把一棵子树的根结点指向另一棵子树即可完成合并。也就是指定其中一个人是另外一个人的上级就好了。
比如说张无忌和岳灵珊这两个门派合并。
public void union(Node a, Node b){ if(a == null || b == null){ return; } Node aHead = findHead(a); Node bHead = findHead(b); if(aHead != bHead){//两个门派掌门人不同,直接将一个门派合并到另一个门派里 fatherMap.put(aHead, bHead);//A门派合并到B门派,aHead屈尊于bHead sizeMap.put(bHead, aSetSize + bSetSize);//B门派人数增加 } }
这样做法简单粗暴,不考虑哪个门派强大。
到目前为止,我们可算是把并查集的基本实现都给完成了,但是前文中不是提到了嘛,合并的时候其实是有很多问题,而且查找的时候依然也有很多问题。别着急,想要我们的算法更加的高效,就必须要好好地改进一波。
1.3 并查集改进
-
查找操作改进
刚才查找的时候,只简单实现了查找的功能,其他什么都没做。如果一个门派很多人,而一个人处于该门派的最下层,查找起来就很麻烦,就需要一个个地找他的上级。
改进:在查找的过程中,在这条路上的所有节点,直接由掌门人直接管理。
private Node findHead(Node node){ Node father = fatherMap.get(node); if(father != node){ father = findHead(father); } fatherMap.put(node, father); return father; }
-
合并操作改进
合并的时候,判断一下node1和node2各自所属门派,谁的门徒多,谁多谁做最终掌门人。就好比是两个人见面合并,谁的人数多,谁做大哥。
public void union(Node a, Node b){ if(a == null || b == null){ return; } Node aHead = findHead(a); Node bHead = findHead(b); if(aHead != bHead){ int aSetSize = sizeMap.get(aHead); int bSetSize = sizeMap.get(bHead); if(aSetSize <= bSetSize){//B门派人数多 fatherMap.put(aHead, bHead); sizeMap.put(bHead, aSetSize + bSetSize); }else{//A门派人数多 fatherMap.put(bHead, aHead); sizeMap.put(aHead, aSetSize + bSetSize); } } }
1.4 并查集Java实现
import java.util.HashMap;
import java.util.List;
public class Solution_UnionFindSet {
public static class Node{
//....节点类型根据需要自己定义
}
public static class UnionFindSet{
public HashMap<Node, Node> fatherMap;//map中value存的是key的上司
public HashMap<Node, Integer> sizeMap;
public UnionFindSet(List<Node> nodes){
makeSets(nodes);
}
private void makeSets(List<Node> nodes) {
fatherMap = new HashMap<>();
sizeMap = new HashMap<>();
for(Node node : nodes){
fatherMap.put(node, node);
sizeMap.put(node, 1);
}
}
//寻找node的根节点,在遍历过程中将node之前的节点都插在根节点下面
private Node findHead(Node node){
Node father = fatherMap.get(node);
if(father != node){
father = findHead(father);
}
fatherMap.put(node, father);
return father;
}
//判断两个节点是否在同一个并查集里。
public boolean isSameSet(Node a, Node b){
return findHead(a) == findHead(b);
}
//合并a所在的集合与b所在的集合
public void union(Node a, Node b){
if(a == null || b == null){
return;
}
Node aHead = findHead(a);
Node bHead = findHead(b);
if(aHead != bHead){
int aSetSize = sizeMap.get(aHead);
int bSetSize = sizeMap.get(bHead);
if(aSetSize <= bSetSize){
fatherMap.put(aHead, bHead);
sizeMap.put(bHead, aSetSize + bSetSize);
}else{
fatherMap.put(bHead, aHead);
sizeMap.put(aHead, aSetSize + bSetSize);
}
}
}
}
public static void main(String[] args) {
}
}
2. 岛屿数量
题目:给定一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 :
输入: 11110 11010 11000 00000 输出: 1
1. 常规解法
public class Solution_NumIslands {
public static int numIslands(char[][] grid) {
if(grid==null||grid.length==0||(grid.length==1&&grid[0].length==0)){
return 0;
}
int r = grid.length;//行
int w = grid[0].length;//列
int result = 0;
for(int i = 0; i < r; i ++){
for(int j = 0; j < w; j++){//按行遍历矩阵,如果遇到‘1’,则进入感染函数
if(grid[i][j] == '1'){
result++;
infect(grid, i, j, r, w);//向四周扩散
}
}
}
return result;
}
//如果当前元素值为'1',将其改为'2',向上下左右四个方向判断是否相连
public static void infect(char[][] grid, int i, int j, int r, int w){
if(i < 0 || j < 0 || i >= r || j >= w ||grid[i][j] != '1'){
return;
}
grid[i][j] = '2';
//向四周扩散
infect(grid, i, j - 1, r, w);//上
infect(grid, i, j + 1, r, w);//下
infect(grid, i - 1, j, r, w);//左
infect(grid, i + 1, j, r, w);//右
}
public static void main(String[] args) {
}
}
2. 并查集解法
上述解法只适用于矩阵比较小的情况,当矩阵比较大的时候,可能用并查集实现分治。
思路:将矩阵划分成若干个小矩阵,先分别求出小矩阵中岛屿的数量。然后判断相邻矩阵的边界。
如上图所示,
- 将矩阵划分成连个子矩阵,左边的矩阵岛屿数量为3,右边的矩阵岛屿数量为2。
- 给每个岛屿命名,每个岛屿名字不同。(相当于并查集中的根节点)
- 从交界处开始判断,发现A岛屿与B岛屿相邻,则将其归于一个岛屿{A,B},然后总岛屿数量减一。(相当于并查集中的合并操作)
- 继续向下判断,发现B岛屿与C岛屿相邻,则将其归为一个岛屿。则将C加入{A,B}中形成{A,B,C},岛屿总数量减一。
- 边界遍历完毕,最终岛屿数为3。