并查集

并查集

1. 并查集原理与实现

原文参考链接:一个非常实用而且精妙的算法-并查集

1.1 原理

话说在江湖上有很多门派,这些门派相互争夺武林霸主。毕竟是江湖中人,两个人见面一言不合就开干。但是打归打,总是要判断一下是不是自己人,免得误伤。
在这里插入图片描述
于是乎,分了各种各样的门派,比如说张无忌和岳灵珊俩人要打架,就先看看是不是同一门派的,不是的话那就再开干。要是张无忌和岳灵珊觉得俩人合得来,那就合并门派。

而且规定了,每一个门派都有一个掌门人,比如武当派就是张三丰。华山派就是岳不群等等。

现在我们把目光转到并查集上。

(1)张无忌和岳灵珊打架之前,先判断是否是同一门派,这就涉及到了并查集的查找操作。

(2)张无忌和岳灵珊觉得俩人合得来,那就合并门派,这就涉及到了并查集的并操作。

(3)每一个门派都有一个掌门人,这涉及到了并查集的存储方式。掌门人代表了这个门派的根节点

现在我们从这个例子的思想开始认识一下并查集。

1.2 简单实现

我们实现一个并查集的时候首先要考虑的就是存储结构,一般情况下有两种:数组和链表。现在我们使用哈希表来实现一下。 使用哈希表的优势是:能快速的找到自己所属门派。

并查集主要涉及到两种操作,合并和查找。 假设有一个链表List< Node>,其中每个元素都是未入江湖的习武之人。makeSets(nodes)操作将这帮人推入江湖斗争之中。

  1. 类架构

    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个人。
            }
        }
    }
    
  2. 查找操作

    Find操作就是查找某个元素所在的集合,返回该集合的代表元素。通俗的理解就是根据张无忌找到其相应门派的掌门人张三丰。

    private Node findHead(Node node){
        Node father = fatherMap.get(node);
        if(father != node){
            return findHead(father);
        }
        return father;
    }
    

    这样做,每次查找掌门人的时候,都要一级一级的往上找,怎么改进呢?

  3. 合并操作

    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 并查集改进

  1. 查找操作改进

    刚才查找的时候,只简单实现了查找的功能,其他什么都没做。如果一个门派很多人,而一个人处于该门派的最下层,查找起来就很麻烦,就需要一个个地找他的上级。

    改进:在查找的过程中,在这条路上的所有节点,直接由掌门人直接管理。
    在这里插入图片描述

    private Node findHead(Node node){
        Node father = fatherMap.get(node);
        if(father != node){
            father = findHead(father);
        }
        fatherMap.put(node, father);
        return father;
    }
    
  2. 合并操作改进

    合并的时候,判断一下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. 并查集解法

上述解法只适用于矩阵比较小的情况,当矩阵比较大的时候,可能用并查集实现分治。

思路:将矩阵划分成若干个小矩阵,先分别求出小矩阵中岛屿的数量。然后判断相邻矩阵的边界。
在这里插入图片描述如上图所示,

  1. 将矩阵划分成连个子矩阵,左边的矩阵岛屿数量为3,右边的矩阵岛屿数量为2。
  2. 给每个岛屿命名,每个岛屿名字不同。(相当于并查集中的根节点)
  3. 从交界处开始判断,发现A岛屿与B岛屿相邻,则将其归于一个岛屿{A,B},然后总岛屿数量减一。(相当于并查集中的合并操作)
  4. 继续向下判断,发现B岛屿与C岛屿相邻,则将其归为一个岛屿。则将C加入{A,B}中形成{A,B,C},岛屿总数量减一。
  5. 边界遍历完毕,最终岛屿数为3。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值