并查集简介

1,077 阅读5分钟

1.概述与具体用途

并查集是一种树型的数据结构,一个并查集的结构就是一个森林,用于处理一些不交集(Disjoint Sets)的合并及查询问题。 具体用途有:

  • 求连通子图
  • 求最小生成树的Kruskal算法
  • 求最近公共祖先(Least Common Ancestors, LCA)

一个子集合中需要选出一个元素代表整个集合,这个元素成为该集合的代表元。集合中的所有元素组织成以代表元为根节点的树形结构,代表元的父节点为代表元自己。该结构区别于一般的树结构关键在于,一般树形结构都是父节点存储指向孩子节点的指针,而该结构孩子节点存储指向父节点的指针。

2.结构与基本操作

并查集的操作包含:判断子集是否相交,合并这些不相交的子集,以及查询元素所属子集的操作等。并查集的基本操作有:

    public class UnionFind {
        private int[] sets;
        public UnionFind(int s){} // 构造函数,建立一个新的并查集,其中包含 s 个单元素集合。
        void union(int x, int y){} // 把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。
        int find(int x){} // 找到元素 x 所在的集合的**代表元**,
        int count();// 统计子树的数量
        boolean connected(int x, int y){} // 判断两个元素是否位于同一个集合,通过find()函数查找它们的代表元,比较一下是否相等,即可判断两个元素是否属于同一集合。
    }

3.具体实现

我们先通过数组的形式来实现一个并查集结构,具体代码如下:

public class UnionFind {
    
    private int[] elements; // 存储并查集

    public UnionFind(int n) { 
        elements = new int[n];
        for (int i = 0; i < n; i++) {
            elements[i] = -1; // 根节点的值默认设置为-1
        }
    }

    public int find(int x) {
        while(elements[x] != -1) {
            x = elements[x]; // 找到数的根
        }
        return x;
    }

    public void union(int x, int y) {
        int rootx = find(x); // x的根
        int rooty = find(y); // y的根
        if(rootx != rooty) {
            elements[rootx] = rooty; // 合并x的集合到y所属集合中
        }
    }

    public int count() { // 统计有多少子集合
        int count = 0;
        for(int i = 0; i < elements.length; i++) {
            if(elements[i] == -1) {
                count++;
            }
        }
        return count;
     }
}

以上的结构存在一个问题,如果元素之间的关系为: 1 -> 2(父节点),2 -> 3(父节点), 3 -> 4(父节点)...,那么我如果想知道1元素所属的集合,就需要一路向上寻找他的最终父节点,也就是整个集合的代表元,这个查找如果在数据规模为n的情况下,时间复杂度为O(n),那么我们有没有什么方法优化查找的过程的呢?答案是有的,接下来我们介绍两种优化方法,一种作用在union()操作中,称为基于树高度的合并优化,另一种作用在find()操作中,叫做路径压缩

4.基于树高度的合并优化

此种优化方式主要作用在合并的过程中,首先,需要准备换一个存储树高度的辅助数组,在合并union()操作的时候,根据辅助数组判断要合并的两个树集合的高度,将矮的树合并到高的上。

public class UnionFind {
    
    private int[] elements; // 存储并查集
    private int[] heights;// 存储树的高度

    public UnionFind(int n) { 
        elements = new int[n];
        heights = new int[n];
        for (int i = 0; i < n; i++) {
            elements[i] = -1; // 根节点的值默认设置为-1
            heights[i] = 1;// 初始高度1
        }
    }

    public int find(int x) {
        while(elements[x] != -1) {
            x = elements[x]; // 找到数的根
        }
        return x;
    }

    public void union(int x, int y) {
        int rootx = find(x); // x的根
        int rooty = find(y); // y的根
        if(rootx != rooty) { // 矮树向高树合并
            if(heights[rootx] > heights[rooty]) {
                elements[rooty] = rootx;
            } else if(heights[rootx] < heights[rooty]) {
                elements[rootx] = rooty;
            } else {
                elements[rootx] = rooty; // 如果高度相同,随便合并
                heights[rooty]++; // 树的高度需要加1
            }
        }
    }

    public int count() { // 统计有多少子集合
        int count = 0;
        for(int i = 0; i < elements.length; i++) {
            if(elements[i] == -1) {
                count++;
            }
        }
        return count;
     }
}

5.路径压缩

另一种优化方式,作用在查找过程中,当我们从下至上找到一个集合的根的时候,会走过一条路径,我们将走过的这条路径上所有节点的父节点都修改为这个集合的代表元,这样下次查找或获取集合的代表元的时候,我们只需查找一次即可。大大的提升了查询的效率。这个操作就叫做路径压缩

public class UnionFind {
    
    private int[] elements; // 存储并查集

    public UnionFind(int n) { 
        elements = new int[n];
        for (int i = 0; i < n; i++) {
            elements[i] = -1; // 根节点的值默认设置为-1
        }
    }

    public int find(int x) {
        int originX = x; // 保存原始x值
        while(elements[x] != -1) {
            x = elements[x]; // 找到数的根
        }
        while(originX != x) { // 把这一路的节点全部直接连到根上
            int tempX = originX;
            originX = elements[originX];
            elements[tempX] = x;
        }
        return x;
    }

    public void union(int x, int y) {
        int rootx = find(x); // x的根
        int rooty = find(y); // y的根
        if(rootx != rooty) {
            elements[rootx] = rooty; // 合并x的集合到y所属集合中
        }
    }

    public int count() { // 统计有多少子集合
        int count = 0;
        for(int i = 0; i < elements.length; i++) {
            if(elements[i] == -1) {
                count++;
            }
        }
        return count;
     }
}

参考链接:
【面试现场】如何编程解决朋友圈个数问题?
并查集(Union-Find)算法介绍