《算法系列》之并查集

简介

  并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受。即使在空间上勉强通过,运行的时间复杂度也极高,只能用并查集来描述。故并查集是一种树型的数据结构,可用于处理一些不相交集合(disjoint sets)的合并及查询问题。

理论基础

  并查集(Union-find Data Structure)是一种树型的数据结构。它的特点是由子结点找到父亲结点,用于处理一些不交集(Disjoint Sets)的合并及查询问题。并查集的思想是用一个数组表示了整片森林(parent),树的根节点唯一标识了一个集合,我们只要找到了某个元素的的树根节点,就能确定它在哪个集合里。 并查集跟树有些类似,只不过它和树是相反的。在树这个数据结构里面,每个节点会记录它的子节点。而在并查集里,每个节点只会记录它的父节点。

  • 并(Union):将两个子集合并成同一个集合。
  • 查(Find):确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
  • 集(Set):代表这是一个以字典为基础的数据结构,它的基本功能是合并集合中的元素,查找集合中的元素。

并查集的代码实现:

class UnionFind {
    private Map<Integer,Integer> father;
    
    public UnionFind() {
        father = new HashMap<Integer,Integer>();
    }
    
    public void add(int x) {
        if (!father.containsKey(x)) {
            father.put(x, null);
        }
    }
    
    public void merge(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        
        if (rootX != rootY){
            father.put(rootX,rootY);
        }
    }
    
    public int find(int x) {
        int root = x;
        
        while(father.get(root) != null){
            root = father.get(root);
        }
        
        while(x != root){
            int original_father = father.get(x);
            father.put(x,root);
            x = original_father;
        }
        
        return root;
    }
    
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }
} 

解题心得

  • 前300道题里没多少关于并查集的题,但我们仍需要了解该解题方法。
  • 很多题,并查集并不是最优解法,但了解并查集解法会给人眼前一亮的感觉。
  • 只要找到了某个元素的的树根节点,就能确定它在哪个集合里。
  • 并查集的典型应用是有关连通分量的问题。
  • 并查集解决单个问题(添加,合并,查找)的时间复杂度都是O(1)。

算法题目

128. 最长连续序列

在这里插入图片描述
题目解析:建立一个并查集UF对象,用map存储,并用head指向共同的头结点,只有当head为空的时候其中size才有意义。
代码如下:

/**
 * 并查集
 */
class Solution {
    public int longestConsecutive(int[] nums) {
        if(nums.length==0) return 0;
        int max=1;
        Map<Integer,UF> map = new HashMap<>();
        for(int i : nums){
            if(map.containsKey(i)) continue;
            if(!map.containsKey(i)){
                map.put(i,new UF(null,1));
            }
            if(map.containsKey(i+1)){
                UF head = map.get(i+1);
                int size = ++head.size;
                max=Math.max(size,max);
                UF item=new UF(null,size);
                head.head=item;
                map.put(i,item);
            }
            if(map.containsKey(i-1)){
                UF head = map.get(i-1);
                while(head.head!=null){
                    head=head.head;
                }
                head.size=map.get(i).size+head.size;
                max=Math.max(head.size,max);
                map.get(i).head=head;
            }
        }
        return max;

    }
}

class UF{
    public UF head;
    public int size;
    public UF (UF head , int size ){
        this.head=head;
        this.size=size;
    }
}
130. 被围绕的区域

在这里插入图片描述
题目解析:使用Union-Find法,先将四条边的’O’和dummy连接 --> 内部的’O’都与之四个邻居的’O’连接。最后不和dummy连接的’O’都是要修改为’X’的位置,修改即可。
代码如下:

/**
 * 并查集
 */
class Solution {
    public void solve(char[][] board) {
        // 注意二维数组的坐标[i,j]换算成一个数:i*n+j
        int m = board.length;
        int n = board[0].length;
        UF uf = new UF(m*n+1);
        // 最后一个位置留给dummy用
        int dummy = m*n;
        for(int i=0;i<m;i++){
            if(board[i][0]=='O'){
                // 最左边
                uf.union(i*n,dummy);
            }
            if(board[i][n-1]=='O'){
                // 最右边
                uf.union(i*n+n-1,dummy);
            }
        }
        for(int i=0;i<n;i++){
            if(board[0][i]=='O'){
                // 最上边
                uf.union(i,dummy);
            }
            if(board[m-1][i]=='O'){
                // 最下边
                uf.union(n*(m-1)+i,dummy);
            }
        }
        int[] dx = new int[]{-1,1,0,0};
        int[] dy = new int[]{0,0,-1,1};
        // 检查内部的O
        for(int i=1;i<m-1;i++){
            for(int j=1;j<n-1;j++){
                if(board[i][j]=='O'){
                    for(int k=0;k<4;k++){
                        int newX = i+dx[k];
                        int newY = j+dy[k];
                        if(board[newX][newY]=='O'){
                            uf.union(newX*n+newY,i*n+j);
                        }
                    }
                }
            }
        }
        for(int i=1;i<m-1;i++){
            for(int j=1;j<n-1;j++){
                if(!uf.connected(i*n+j,dummy)){
                    board[i][j] = 'X';
                }
            }
        }
    }
    class UF {
        // 连通分量个数
        private int count;
        // 存储每个节点的父节点
        private int[] parent;

        // n 为图中节点的个数
        public UF(int n) {
            this.count = n;
            parent = new int[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;
            }
        }
        
        // 将节点 p 和节点 q 连通
        public void union(int p, int q) {
            int rootP = find(p);
            int rootQ = find(q);
            
            if (rootP == rootQ)
                return;
            
            parent[rootQ] = rootP;
            // 两个连通分量合并成一个连通分量
            count--;
        }

        // 判断节点 p 和节点 q 是否连通
        public boolean connected(int p, int q) {
            int rootP = find(p);
            int rootQ = find(q);
            return rootP == rootQ;
        }

        public int find(int x) {
            while(x!=parent[x]){
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            return x;
        }

        // 返回图中的连通分量个数
        public int count() {
            return count;
        }
    }
}
200. 岛屿数量

在这里插入图片描述
题目解析:我们可以用并查集代替深搜和广搜搜索,为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为1,则将其与相邻四个方向上的1在并查集中进行合并。最终岛屿的数量就是并查集中连通分量的数目。
代码如下:

/**
 * 并查集
 */
class Solution {
    class UnionFind {
        int count;
        int[] parent;
        int[] rank;

        public UnionFind(char[][] grid) {
            count = 0;
            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) {
                    if (grid[i][j] == '1') {
                        parent[i * n + j] = i * n + j;
                        ++count;
                    }
                    rank[i * n + j] = 0;
                }
            }
        }

        public int find(int i) {
            if (parent[i] != i) parent[i] = find(parent[i]);
            return parent[i];
        }

        public void union(int x, int y) {
            int rootx = find(x);
            int rooty = find(y);
            if (rootx != rooty) {
                if (rank[rootx] > rank[rooty]) {
                    parent[rooty] = rootx;
                } else if (rank[rootx] < rank[rooty]) {
                    parent[rootx] = rooty;
                } else {
                    parent[rooty] = rootx;
                    rank[rootx] += 1;
                }
                --count;
            }
        }

        public int getCount() {
            return count;
        }
    }

    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0) {
            return 0;
        }

        int nr = grid.length;
        int nc = grid[0].length;
        int num_islands = 0;
        UnionFind uf = new UnionFind(grid);
        for (int r = 0; r < nr; ++r) {
            for (int c = 0; c < nc; ++c) {
                if (grid[r][c] == '1') {
                    grid[r][c] = '0';
                    if (r - 1 >= 0 && grid[r-1][c] == '1') {
                        uf.union(r * nc + c, (r-1) * nc + c);
                    }
                    if (r + 1 < nr && grid[r+1][c] == '1') {
                        uf.union(r * nc + c, (r+1) * nc + c);
                    }
                    if (c - 1 >= 0 && grid[r][c-1] == '1') {
                        uf.union(r * nc + c, r * nc + c - 1);
                    }
                    if (c + 1 < nc && grid[r][c+1] == '1') {
                        uf.union(r * nc + c, r * nc + c + 1);
                    }
                }
            }
        }

        return uf.getCount();
    }
}
回到首页

刷 leetcode 500+ 题的一些感受

下一篇

《算法系列》之模拟

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小夏陌

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

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

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

打赏作者

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

抵扣说明:

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

余额充值