06_并查集

About 9 minstudyalgorithm

一、并查集

// 提供两个方法,在频繁的执行以下方法(执行次数超过样本数)的均摊时间复杂度是O(1)
boolean isSameSet(int[] a, int[] b);

void union(int[] a, int[] b);

思路

利用包装类+hashmap进行值与值之间的关联,如果有值合并在一起则形成类似树状的结构(树的深度使用路径压缩)。判断是否是一个集合时只需要判断两个值的祖先节点是否相同即可。合并节点也只需将节点相交即可(小节点指向大节点)

代码

class UnionSet<V>{

    // 值对应自己的包装类
    private HashMap<V, Node<V>> nodes;
    // 节点于父亲节点的对应关系
    private HashMap<Node<V>, Node<V>> parents;
    // 集合中的数量
    private HashMap<Node<V>, Integer> counts;

    public UnionSet(List<V> list) {
        nodes = new HashMap<>();
        parents = new HashMap<>();
        counts = new HashMap<>();

        if(list == null || list.isEmpty()) {
            return;
        }
        for (V v : list) {
            Node<V> node = new Node<>(v);
            nodes.put(v, node);
            parents.put(node, node);
            counts.put(node, 1);
        }
    }

    // 查找是否相同的集合
    public boolean findSameSet(V a, V b){
        // 如果节点的祖先节点相等则是相同的集合
        return findAncestry(nodes.get(a)) == findAncestry(nodes.get(b));
    }

    // 合并两个节点
    public void union(V a, V b) {
        Node<V> aNode = findAncestry(nodes.get(a));
        Node<V> bNode = findAncestry(nodes.get(b));
        if(aNode == bNode) {
            return;
        }

        // 获取长度
        Integer aLength = counts.get(aNode);
        Integer bLength = counts.get(bNode);
        // 大小重定向
        Node<V> bigNode = aLength > bLength ? aNode : bNode;
        Node<V> smallNode = bigNode == bNode ? aNode : bNode;
        // 小的指向大的
        parents.put(smallNode, bigNode);
        // 大节点增加数量和删除小节点
        counts.put(bigNode, aLength + bLength);
        counts.remove(smallNode);
    }

    // 查找祖先
    private Node<V> findAncestry(Node<V> a) {
        Stack<Node<V>> stack = new Stack<>();
        // 将长集合放入栈中
        while (a != parents.get(a)) {
            stack.push(a);
            a = parents.get(a);
        }

        // 将长集合的每个集合指向祖先节点,优化树的高度
        while (!stack.isEmpty()) {
            parents.put(stack.pop(), a);
        }
        return a;
    }

    private static class Node<V>{
        V value;
        public Node(V v) {
            this.value = v;
        }

    }
}

二、朋友圈(省份)

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。 链接:https://leetcode.cn/problems/number-of-provinces

思路

1、用并查集实现,如果i和j=1. 则两个值关联上

2、获取集合的数量就是矩阵中的数量

代码

public static int findCircleNum(int[][] M) {
        int n = M.length;
        UnionFind unionFind = new UnionFind(n);
        // 遍历数组
        for (int i = 0; i < n; i++) {
            for (int j = i+1; j < n; j++) {
                // 如果i位置和j位置相连则合并
                if(M[i][j] == 1) {
                    unionFind.union(i, j);
                }
            }
        }

        return unionFind.sets;
    }
    public static class UnionFind{
        /**
         * 父亲节点
         * [0,1]: 代表下标为0节点的父节点为0,下标为1节点的父节点为1
         * [1,0]: 代表下标为0节点的父节点为1,下标为1节点的父节点为0
         */
        private int[] parent;
    
        /**
         * 存放大小
         */
        private int[] size;
    
        // 辅助数组,用来代替栈
        private int[] help;
    
        // 集合数量
        private int sets;
    
        public UnionFind(int n) {
            parent = new int[n];
            size = new int[n];
            help = new int[n];
            sets = n;
            for (int i = 0; i < n; i++) {
                parent[i] = i;
                size[i] = 1;
            }
        }
    
        // 查找祖先
        private int find(int n) {
            int i = 0;
            // 遍历如果n的父值不是n则往上赋值, help存父值
            while (n != parent[n]) {
                help[i++] = parent[n];
                n = parent[n];
            }
            // 路径压缩,将help里面的值都指向n
            for (i--; i>=0; i--) {
                parent[help[i]] = n;
            }
            return n;
        }
    
        private void union(int i, int j) {
            int i1 = find(i);
            int i2 = find(j);
            // 两个父节点不是一个则合并
            if(i1 != i2) {
                // 数量小的集合合并到数量大的集合
                if(size[i1] > size[i2]) {
                    parent[i2] = i1;
                    size[i1] += size[i2];
                } else {
                    parent[i1] = i2;
                    size[i2] += size[i1];
                }
                sets--;
            }
        }
    }

三、岛数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/number-of-islands

递归方式

    /**
     * 递归方式寻找岛
     * 遍历二维数组,遇到一个岛屿就进行递归找上下左右的岛屿
     */
    public static int numIslands(char[][] board) {
        int isLands = 0;
        for (int i = 0; i < board.length; i++) {
            for(int j = 0; j < board[0].length; j++) {
                if(board[i][j] == '1') {
                    isLands++;
                    infect(board, i, j);
                }
            }
        }
        return isLands;
    }

    // 找到岛屿之后就将岛屿改为0, 并递归寻找该岛上下左右是否是岛屿
    private static void infect(char[][] board, int i, int j) {
        if(i < 0 || i == board.length || j < 0 || j == board[0].length || board[i][j] != '1'){
            return;
        }
        board[i][j] = '0';
        infect(board, i - 1, j);
        infect(board, i + 1, j);
        infect(board, i, j - 1);
        infect(board, i, j + 1);
    }

并查集方式

思路

1、将所有陆地初始化为list集合

2、判断每个值的左边和上边是否是陆地(每个值都判断左边和上边就不用判断左边和下边)

3、合并的集合数量就是岛的数量

    public static void main(String[] args) {
        char[][] list = new char[][]{"11110".toCharArray(),"11010".toCharArray(),"11000".toCharArray(),"00000".toCharArray()};
        System.out.print(numIslands1(list));
    }

    private static class Dot{}

    // 利用空对象判断,null表示水,对象表示陆地
    public static int numIslands(char[][] board) {
        int row = board.length;
        int col = board[0].length;
        Dot[][] dots = new Dot[row][col];
        // 将char二维数组以dot二维素组形式组装,路径就是dot对象,水为null。并将dot放到list里面
        List<Dot> dotList = new ArrayList<>();
        for (int i = 0; i < row; i++) {
            for(int j = 0; j < col; j++) {
                if(board[i][j] == '1') {
                    dots[i][j] = new Dot();
                    dotList.add(dots[i][j]);
                }
            }
        }

        // 先遍历列,判断是陆地则进行合并操作
        UnionHashMap<Dot> unionHashMap = new UnionHashMap<>(dotList);
        for (int i = 1; i < col; i++) {
            if(board[0][i-1] == '1' && board[0][i] == '1') {
                unionHashMap.union(dots[0][i-1], dots[0][i]);
            }
        }

        // 遍历行,判断是陆地则进行合并操作
        for (int i = 1; i < row; i++) {
            if(board[i - 1][0] == '1' && board[i][0] == '1') {
                unionHashMap.union(dots[i - 1][0], dots[i][0]);
            }
        }

        // 遍历中间的其他水和陆地,是陆地则进行合并操作
        for (int i = 1; i < row; i++) {
            for (int j = 1; j < col; j++) {
                if(board[i][j] == '1') {
                    // 判断上边是陆地则合并
                    if(board[i][j-1] == '1') {
                        unionHashMap.union(dots[i][j], dots[i][j-1]);
                    }
                    // 判断左边是陆地则合并
                    if(board[i-1][j] == '1') {
                        unionHashMap.union(dots[i][j], dots[i-1][j]);
                    }
                }
            }
        }
        // 合并后的集合就是陆地的数量
        return unionHashMap.getSize();
    }

    private static class UnionHashMap<V>{

        private HashMap<V, Node<V>> nodes;
        private HashMap<Node<V>, Node<V>> parents;
        private HashMap<Node<V>, Integer> sizes;

        public UnionHashMap(List<V> list) {
            nodes = new HashMap<>();
            parents = new HashMap<>();
            sizes = new HashMap<>();
            for (V v : list) {
                Node node = new Node(v);
                nodes.put(v, node);
                parents.put(node, node);
                sizes.put(node, 1);
            }
        }

        public int getSize() {
            return sizes.size();
        }

        // 合并两个元素
        public void union(V a, V b) {
            Node<V> aNode = findAncestor(nodes.get(a));
            Node<V> bNode = findAncestor(nodes.get(b));
            if(aNode != bNode) {
                int aLength = sizes.get(aNode);
                int bLength = sizes.get(bNode);
                Node maxNode = aLength > bLength ? aNode : bNode;
                Node minNode = maxNode == aNode ? bNode : aNode;
                parents.put(minNode, maxNode);
                sizes.put(maxNode, aLength + bLength);
                sizes.remove(minNode);
            }
        }

        // 获取祖先节点
        private Node<V> findAncestor(Node<V> node){
            Stack<Node<V>> stack = new Stack<>();
            while (node != parents.get(node)) {
                stack.push(node);
                node = parents.get(node);
            }

            // 路径压缩,将所有的
            while (!stack.isEmpty()) {
                Node<V> pop = stack.pop();
                parents.put(pop, node);
            }
            return node;
        }
    }
    private static class Node<T>{
        private T val;

        public Node(T val) {
            this.val = val;
        }
    }

优化思路

上面是利用空对象和空+hashmap实现的,可以进行优化,用数组的方式代替hashmap。

将二维数组转换为一维数组(i,j 对应 i*列数+j)

public static int numIslands(char[][] board) {
        int row = board.length;
        int col = board[0].length;
        UnionArray unionArray = new UnionArray(board);
        // 遍历列board[0][i]
        for (int i = 1; i < col; i++) {
            if(board[0][i-1] == '1' && board[0][i] == '1') {
                unionArray.union(0, i, 0, i-1);
            }
        }

        // 遍历行board[i][0]
        for (int i = 1; i < row; i++) {
            if(board[i -1][0] == '1' && board[i][0] == '1') {
                unionArray.union(i-1, 0, i, 0);
            }
        }
        for (int i = 1; i < row; i++) {
            for (int j = 1; j < col; j++) {
                if(board[i][j] == '1') {
                    if(board[i][j-1] == '1') {
                        unionArray.union(i, j, i, j-1);
                    }
                    if(board[i-1][j] == '1') {
                        unionArray.union(i, j, i-1, j);
                    }
                }
            }
        }
        return unionArray.getSize();
    }


        // 数组的方式实现并查集
    private static class UnionArray{

        // 节点的父节点
        private int[] parent;
        // 节点的数量
        private int[] sizes;
        // 辅助数组用于代替栈作路径压缩
        private int[] help;
        // 列数
        private int col;
        // 岛数量
        private int size;

        public UnionArray(char[][] board) {
            this.col = board[0].length;
            int row = board.length;
            int length = col * row;
            parent = new int[length];
            sizes = new int[length];
            this.size = 0;
            help = new int[length];
            for (int i = 0; i < row; i++) {
                for (int j = 0; j < col; j++) {
                    if(board[i][j] == '1') {
                        int index = index(i, j);
                        this.parent[index] = index;
                        this.sizes[index] = 1;
                        this.size++;
                    }
                }
            }
        }

        public void union(int ra, int ca, int rb, int cb) {
            int aIndex = index(ra, ca);
            int bIndex = index(rb, cb);
            // 获取祖先的下标
            int aAncestor = findAncestor(aIndex);
            int bAncestor = findAncestor(bIndex);
            if(aAncestor != bAncestor) {
                // 将短的集合指向长集合,并合并数量
                if(this.sizes[aAncestor] > this.sizes[bAncestor]) {
                    this.parent[bAncestor] = aAncestor;
                    this.sizes[aAncestor] += sizes[bAncestor];
                } else {
                    this.parent[aAncestor] = bAncestor;
                    this.sizes[bAncestor] += sizes[aAncestor];
                }
                this.size--;
            }
        }

        private int findAncestor(int val) {
            int i = 0;
            while (val != parent[val]) {
                help[i++] = val;
                val = parent[val];
            }

            // 路径压缩,将help里面的下标都指向祖先
            for (i--; i >= 0; i--) {
                parent[help[i]] = val;
            }
            return val;
        }

        // 获取数组的下标,行数从0开始*列总数+当前行位置
        private int index(int r, int c) {
            return r * col + c;
        }

        public int getSize() {
            return this.size;
        }
    }

四、获取每一步岛数量

思路

先将二维数组初始化后,在并查集中新增一个链接方法,用来将给定的合并到并查集数组中(新加的值和前后左右的值进行对比),每次链接时返回岛数量。收集岛数量返回即可。

代码

// 给定二维数组和指定的岛数量,获取每多一个值的岛数量
    public static List<Integer> numIslands21(int m, int n, int[][] positions) {
        // 先初始化数组
        UnionArray unionArray = new UnionArray(m, n);
        List<Integer> result = new ArrayList<>();
        for (int[] position : positions) {
            // 每个点进行链接合并后获取岛数量
            result.add(unionArray.connect(position[0], position[1]));
        }
        return result;
    }
    
    private static class UnionArray{

        private int[] parent;
        // 通过sizes[i]是否等于0标记是否初始化过
        private int[] sizes;
        private int[] help;
        private int size;
        private int row;
        private int col;
        
        public UnionArray(int r, int c) {
            col = c;
            row = r;
            int length = r * c;
            parent = new int[length];
            sizes = new int[length];
            help = new int[length];
            size = 0;
        }

        public int connect(int r, int c) {
            // 判断当前位置已经合并过则直接返回数量
            int index = getIndex(r, c);
            if(sizes[index] == 0) {
                // 给当前值赋值
                parent[index] = index;
                sizes[index] = 1;
                size++;
                // 当前值 的前后左右进行合并
                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 size;
        }

        // 并查集的合并操作
        private void union(int ar, int ac, int br, int bc) {
            if(ar < 0 || ar == row || ac < 0 || ac == col || br < 0 || br == row || bc < 0 || bc == col) {
                return;
            }
            int ai = getIndex(ar, ac);
            int bi = getIndex(br, bc);
            if(sizes[ai] == 0 || sizes[bi] == 0) {
                return;
            }
            int af = findAncestor(ai);
            int bf = findAncestor(bi);
            if(af != bf) {
                int aLength = sizes[af];
                int bLength = sizes[bf];
                if(aLength > bLength) {
                    parent[bf] = af;
                    sizes[af] += sizes[bf];
                } else {
                    parent[af] = bf;
                    sizes[bf] += sizes[af];
                }
                size--;
            }
        }

        // 获取下标
        private int getIndex(int r, int c) {
            return r * col + c;
        }

        // 获取祖先节点
        private int findAncestor(int val) {
            int i = 0;
            while (val != parent[val]) {
                help[i++] = val;
                val = parent[val];
            }
            for(i--; i>=0; i--) {
                parent[help[i]] = val;
            }
            return val;
        }
    }

每次都先初始化数组,如果矩阵特别大,但是给定的值很少。就会浪费空间,可以用hashmap + String.valueOf(row + "_" + col)实现。

Last update:
Contributors: gaoqisen