并查集

一、并查集

并查集是一种树型的数据结构,用于处理一些不交集的合并及查询问题

并查集主要包含以下几种基本操作:

  • init(s):建立一个新的并查集,其中包含s个单元素集合
  • union(x, y):把元素x和元素y所在的集合合并,要求x和y所在的集合不相交,如果相交则不合并
  • find(x):找到元素x所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了

find(D)和find(F)分别返回元素D和元素F所属集合的代表A和H:

在这里插入图片描述

union(D, F)将元素D和元素F所在的集合合并:

在这里插入图片描述

二、并查集的代码实现

public class UnionFind {
    int[] parents;

    public UnionFind(int totalNodes) {
        parents = new int[totalNodes];
        for (int i = 0; i < totalNodes; i++) {
            parents[i] = i;
        }
    }

    public void union(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        if (rootP != rootQ) {
            parents[rootQ] = rootP;
        }
    }

    public int find(int p) {
        if (p == parents[p])
            return p;
        return find(parents[p]);
    }

    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }
}

三、并查集优化

以上是并查集最基础的表示方法,如果创建的树严重不平衡就会退化成链表,可以用按秩合并路径压缩两种办法来进行优化

1、按秩合并

按秩合并是指总是将更小的树连接至更大的树上。因为影响执行效率的是树的秩(深度),更小的树添加到更深的树的根上将不会增加秩除非它们的秩相同。当两棵秩同为r的树联合时,它们的秩r+1

在这里插入图片描述

2、路径压缩

对于一个树来说,它的根节点下面可以依附着许多的节点,因此,可以尝试在find的过程中,从底向上,如果此时访问的节点不是根节点的话,那么可以把这个节点尽量的往上挪一挪,减少数的层数,这个过程就叫做路径压缩

1)、优化方法一

在这里插入图片描述

实现代码如下:

    //在路径上的每个元素都直接指向根元素
	public int find(int p) {
        if (p == parents[p])
            return p;
        parents[p] = find(parents[p]);
        return parents[p];
    }
2)、优化方法二

在这里插入图片描述

假设起始的并查集如上图所示,现在执行find(4),首先根据parents[4]可以得出,4并不是一个根节点,因此,可以在向上继续查询之前,把这个节点往上面挪一挪(路径压缩),首先现在4节点连接到其父亲3节点上,我们可以让4节点不在指向3节点作为父亲节点了,而是让其跳一下,让其指向2节点(即父亲节点的父亲节点)作为新的父亲节点(如果该元素的父亲节点正好是根节点,那么让其指向父亲节点的父亲节点并不会出错,因为根据根元素的父亲节点指向其自己的结构,此时父亲节点的父亲节点仍然是有效的,即还是根节点,不会发生越界问题)

在这里插入图片描述

这样,树的层数由原来的5层变成了现在的4层,即路径被压缩了一下

下面,把继续来对2节点进行find操作,这里没有再去访问3节点,相当于跳过了一步操作(因为3节点也不是根节点,并不是我们想要返回的结果。如果3节点是根节点的话,那么4节点就会指向3节点,接下来就会访问3节点,所以这样的跳过是可行的),对于2节点来说,2节点也不是我们所要找到的根节点,因此,同样也可以对其进行压缩操作,让2节点指向父亲节点的父亲节点0节点作为新的父亲节点,如下图所示:

在这里插入图片描述

此时,树的层数由4层被压缩到了3层,与此同时,还跳过了一个1节点,接下来,只需要对0节点在进行路径压缩操作就好了。因为0节点是我们要找的根节点,因此,我们不再需要执行路径压缩操作了,只需要把找到的结果即根节点给返回就好了

实现代码如下:

    public int find(int p) {
        //如果p元素指向的不是自己,说明p并不是集合的根元素,还需要一直向上查找和路径压缩
        while (parents[p] != p) {
            //p元素不再指向原来的父亲节点,而是直接指向父亲节点的父亲节点来做为自己新的一个父亲节点,这样的操作使得树的层数被压缩了
            parents[p] = parents[parents[p]];
            //p压缩完毕后且p不是根节点,p变成p新的父节点继续进行查找和压缩的操作
            p = parents[p];
        }
        return p;
    }

四、并查集相关题目

1、LeetCode200:岛屿数量

给定一个由1(陆地)和0(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。可以假设网格的四个边均被水包围

示例1:

输入:
11110
11010
11000
00000
输出: 1

示例2:

输入:
11000
11000
00100
00011
输出: 3

题解:

    int rows;
    int cols;

    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0) return 0;
        rows = grid.length;
        cols = grid[0].length;
        UnionFind uf = new UnionFind(rows * cols + 1);
        int dummyNode = rows * cols;
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                if (grid[i][j] == '0') uf.union(node(i, j), dummyNode);
                else {
                    if (i + 1 < rows && grid[i + 1][j] == '1') uf.union(node(i + 1, j), node(i, j));
                    if (j + 1 < cols && grid[i][j + 1] == '1') uf.union(node(i, j + 1), node(i, j));
                }
            }
        }
        return uf.getCount() - 1;
    }

    public int node(int i, int j) {
        return cols * i + j;
    }

    class UnionFind {
        private int[] parents;
        private int count;

        public UnionFind(int totalNodes) {
            parents = new int[totalNodes];
            count = totalNodes;
            for (int i = 0; i < totalNodes; i++) {
                parents[i] = i;
            }
        }

        public void union(int p, int q) {
            int rootP = find(p);
            int rootQ = find(q);
            if (rootP == rootQ) return;
            parents[rootQ] = rootP;
            count--;
        }

        public int find(int p) {
            if (p == parents[p])
                return p;
            parents[p] = find(parents[p]);
            return parents[p];
        }

        public int getCount() {
            return count;
        }
    }

2、LeetCode547:朋友圈

班上有N名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知A是B的朋友,B是C的朋友,那么我们可以认为A也是C的朋友。所谓的朋友圈,是指所有朋友的集合

给定一个N * N的矩阵M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第i个和j个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数

示例1:

输入: 
[[1,1,0],
 [1,1,0],
 [0,0,1]]
输出: 2 

说明:已知学生0和学生1互为朋友,他们在一个朋友圈。第2个学生自己在一个朋友圈。所以返回2

示例2:

输入: 
[[1,1,0],
 [1,1,1],
 [0,1,1]]
输出: 1

说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1

注意:

N 在[1,200]的范围内
对于所有学生,有M[i][i] = 1
如果有M[i][j] = 1,则有M[j][i] = 1

题解:

    public int findCircleNum(int[][] M) {
        int[] parent = new int[M.length];
        Arrays.fill(parent, -1);
        for (int i = 0; i < M.length; i++) {
            for (int j = 0; j < M.length; j++) {
                if (M[i][j] == 1 && i != j) {
                    union(parent, i, j);
                }
            }
        }
        int count = 0;
        for (int i = 0; i < parent.length; i++) {
            if (parent[i] == -1)
                count++;
        }
        return count;
    }

    public int find(int parent[], int i) {
        if (parent[i] == -1)
            return i;
        return find(parent, parent[i]);
    }

    public void union(int parent[], int p, int q) {
        int rootP = find(parent, p);
        int rootQ = find(parent, q);
        if (rootP != rootQ)
            parent[rootP] = rootQ;
    }

3、LeetCode130:被围绕的区域

给定一个二维的矩阵,包含XO(字母O)

找到所有被X围绕的区域,并将这些区域里所有的OX填充

示例:

X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X

解释:

被围绕的区间不会存在于边界上,换句话说,任何边界上的O都不会被填充为X。任何不在边界上,或不与边界上的O相连的O最终都会被填充为X。如果两个元素在水平或垂直方向相邻,则称它们是相连的

题解:

    int rows;
    int cols;

    public void solve(char[][] board) {
        if (board == null || board.length == 0) return;
        rows = board.length;
        cols = board[0].length;
        //多申请一个空间
        UnionFind uf = new UnionFind(rows * cols + 1);
        //所有边界的O节点都和dummy节点合并
        int dummyNode = rows * cols;
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                if (board[i][j] != 'O') continue;
                //当前节点在边界就和dummy合并
                if (i == 0 || j == 0 || i == rows - 1 || j == cols - 1) uf.union(node(i, j), dummyNode);
                else {
                    //将上下左右的O节点和当前节点合并
                    if (board[i - 1][j] == 'O') uf.union(node(i, j), node(i - 1, j));
                    if (board[i + 1][j] == 'O') uf.union(node(i, j), node(i + 1, j));
                    if (board[i][j - 1] == 'O') uf.union(node(i, j), node(i, j - 1));
                    if (board[i][j + 1] == 'O') uf.union(node(i, j), node(i, j + 1));
                }
            }
        }

        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                //判断是否和dummy节点是一类
                if (uf.isConnected(dummyNode, node(i, j))) board[i][j] = 'O';
                else board[i][j] = 'X';
            }
        }
    }

    public int node(int i, int j) {
        return cols * i + j;
    }

    class UnionFind {
        int[] parents;

        public UnionFind(int totalNodes) {
            parents = new int[totalNodes];
            for (int i = 0; i < totalNodes; i++) {
                parents[i] = i;
            }
        }

        public void union(int p, int q) {
            int rootP = find(p);
            int rootQ = find(q);
            if (rootP != rootQ) {
                parents[rootQ] = rootP;
            }
        }

        public int find(int p) {
            if (p == parents[p])
                return p;
            parents[p] = find(parents[p]);
            return parents[p];
        }

        public boolean isConnected(int p, int q) {
            return find(p) == find(q);
        }
    }

参考:

https://www.cnblogs.com/MrSaver/p/9607552.html

https://blog.csdn.net/qq_19782019/article/details/78919990

常用数据结构的时间、空间复杂度:
在这里插入图片描述
https://www.bigocheatsheet.com/

  • 10
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值