数据结构与算法-并查集

201 阅读3分钟

1,需求分析

  1. 假设有n个村庄,有些村庄之间有连接的路,有些村庄之间并没有连接的路
  2. 设计一个数据结构,能够快速的执行2个操作

1,查询2个村庄之间是否有连接的路

2,连接2个村庄

并查集 非常适合解决这类的连接问题

2,并查集

  1. 并查集也叫作不相交的集合
  2. 并查集有2个核心操作

查找:查找元素所在的集合

合并:将两个元素所在的集合合并为一个集合

有2种常见的实现思路

1,快速查找

查找(find)的时间复杂度为:O(1)

合并(union)的时间复杂度:O(n)

2,快速合并

查找的时间复杂度:O(logn)

合并的时间复杂度:O(logn)

2.1,Quick Find - 快速查找方案实现

1,假设并查集处理的数据都是整型,可以用整型数组来存储

实现思路:

1,方法union(v1,v2): 让v1所在集合的所有元素都指向v2的根节点

实现代码:

//查找,父节点就是根节点
public int find(int v) {
    //检查是否超过范围
    rangeCheck(v);
    return parents[v];
}

//将v1所在集合的所有元素,都嫁接到v2的父节点上
public void union(int v1,int v2){
    int p1 = find(v1);
    int p2 = find(v2);
    if(p1 == p2) return;
    
    for (int i = 0; i < parents.length; i ++){
        if (parents[i] == p1){
            parents[i] = p2;
        }
    }
}
//检查是否在同一个集合
public boolean isSame(int v1,int v2){
    return find(v1) == find(v2);
}
//
protected void rangeCheck(int v) {    if (v < 0 || v >= parents.length) {       throw new IllegalArgumentException("v is out of bounds");    }
}

2.2,快速合并的实现

1,快速合并的方法union(v1,v2): 让v1的根节点指向v2的根节点

实现代码:

//查找
public int find(int v) {
    rangeCheck(v);
    while (v != parents[v]) {
        v = parents[v];
    }
}
//合并
public void union(int v1,int v2) {
    int p1 = find(v1);
    int p2 = find(v2);
    if (p1 == p2) return;
    
    parents[p1] = p2;
}

3,快速查找的并查集-优化

  1. 在union的过程中,可能会出现树不平衡的情况,甚至退化成链表
  2. 有两种常见的优化方案

基于size的优化:元素少的树  嫁接到 元素多的树
基于rank的优化:矮的树  嫁接到 高的树

3.1,基于size的优化

只是在合并的时候,将元素少的树,嫁接到元素多的树

代码实现:

//初始化size,默认存储元素的个数为1
sizes = new int[capacity];
for (int i = 0; i < sizes.length; i++){
    sizes[i] = 1;
}
//合并
public void union(int v1,int v2){
    int p1 = find(v1);
    int p2 = find(v2);
    if (p1 == p2) return;

    if (sizes[p1] < sizes[p2]) {
        //将p1根节点嫁接到p2
        parents[p1] = p2;
        //p2的size要加上p1的size大小
        sizes[p2] += sizes[p1];
    }
}

3.2,基于rank的优化

ranks = new int[capacity];
for (int i = 0; i < ranks.length; i++){
    ranks[i] = 1;
}

//
public void union(int v1, int v2){
    int p1 = find(v1);
    int p2 = find(v2);
    if (p1 == p2) return;
    
    if (ranks[p1] < ranks[p2]){
        parents[p1] = p2;
    } else if (ranks[p1] > ranks[p2]){
        parents[p2] = p1;
    } else {//只有当相等的时候,rank才会加1
        parents[p1] = p2;
        ranks[p2]++;
    }
}

4,路径压缩

  1. 虽然有了基于rank的优化,树会相对平衡一点
  2. 但是随着union次数的增多,树的高度依然会越来越高,导致find操作变慢,尤其是底层节点

路径压缩:在find时使路径上的所有节点都指向根节点,从而降低树的高度

代码实现

public int find(int v){
    if (parents[v] != v){
        //找到路径上的所有节点,都指向根节点
        parents[v] = find(parents[v]);
    }
    return parents[v];
}

还有2种更优的做法,不但能降低树高,实现成本也比路径压缩底
路径分裂 和 路径减半 

4.1,路径分裂

路径分裂:使路径上每隔一个节点就指向其祖父节点

//在查找时修改节点的指向
public int find(int v) {
    while (v != parents[v]) {
        int parent = parents[v];
        parents[v] = parents[parent];
        v = parent;
    }
    return v;
}

4.2,路径减半

路径减半:使路径上每隔一个节点就指向其祖父节点

代码实现:

//
public int find(int v){
    while (v != parents[v]){
        parents[v] = parents[parents[v]];
        v = parents[v];
    }
    return v;
}