(刷题必备)并查集详解+Java模板代码

本文的主要内容如下:

  • 并查集的简单介绍
  • 并查集模板代码
  • 保证性能的两个启发式策略:路径压缩和按秩合并
  • 时间复杂度介绍
  • Ackermann(4,1)到底有多大

何谓并查集

并查集,在《算法导论》中的术语是“用于不相交集合的数据结构”。比较抽象,笔者觉得还不如“并查集”来的好理解。

先举个简单的例子来直观感受一下“并查集”解决的问题:

假设有编号分别为0,1,2,3,4,5的6个人,

告诉你0和4是一伙儿的,2和3同属一伙,3和5同属一伙,请问一共有几个团伙?

这个问题可以靠简单推理得出答案。但是如果人数比较多,团伙关系复杂的话,就需要借助算法工具来解决。

并查集就是解决这类集合(或者图)的连通性问题,一个很好用的工具。

接下来,用数组来表达这个例子

数组arr[0..5]记录这6个人,他们各自的“状态”,即他们各自属于哪个团伙。

一开始,令arr[i] = i (i=0..5),每个人都属于不同的团伙,也可以说是都各自独立。

接着,既然0和4一伙,那么让arr[4] = 0。即两个人一伙的话,可以规定编号大的要“听从”编号小的,4的上级是0。

同理,arr[3] = 2, arr[5] = 3

那么,如此一来数组就变成了:[0,1,2,2,0,3]

其中有3个联通分支(团伙):

以0为首的分支:0<–4 (0的上级是0,4的上级是0)

以1为首的分支:1 (1的上级是1)

以2为首的分支:2<–3<–5 (2的上级是2,3的上级是2,5的上级是3)

模板代码

// 开启了路径压缩和按秩合并的并查集
public class UnionFind {
    int[] parent;
    int[] size;
    // 当前连通分支数目
    int branchCount;

    public UnionFind(int n) {
        this.branchCount = n;
        this.parent = new int[n];
        this.size = new int[n];
        Arrays.fill(size, 1);
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
        }
    }

    public int find(int x) {
        // 路径压缩
        return parent[x] == x ? x : (parent[x] = find(parent[x]));
    }

    public boolean unite(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) {
            return false;
        }
        // 按秩合并
        if (size[x] < size[y]) {
            int temp = x;
            x = y;
            y = temp;
        }
        parent[y] = x;
        size[x] += size[y];
        --branchCount;
        return true;
    }

    public boolean connected(int x, int y) {
        return find(x) == find(y);
    }

    public int branchCount() {
        return branchCount;
    }
}

路径压缩和按秩合并

并查集本身理解起来不困难,关键是要理解保证性能的两个启发式策略:路径压缩和按秩合并。

1、路径压缩
路径压缩(union by rank)还是很好实现的。
finx(x)查找元素x所属分支根元素的过程中,将向上查找路径上的所有元素的parent直接指向最终找到的根。实现上,这是通过递归一层一层调用,最终一层一层返回并进行赋值来做到的。
那么,下一次查找这条路径上的任意一个元素,可以直接一步定位到根,即缩短了查找路径。

2、按秩合并
按秩合并(path compression)实现稍微麻烦一点,需要一个额外的size[]数组,size[i]记录以i为根的分支内节点的数量。
有了size[]数组,在合并的时候,将节点数量少的分支,向节点数量多的分支进行合并。

时间复杂度

使用路径压缩和按秩合并的情况下,作用于n个元素上的m个不相交集合,
建立起整个联通关系的时间复杂度为O(m·α(n)),其中α(n)<=4

//α(n)的值(摘自《算法导论》)
0 (n=0,1,2)
1 (n=3)
2 (n=4,5,6,7)
3 (8<= n <=2047)
4 (2048<= n <= X),其中X是阿克曼函数值Ackermann(4,1),是一个远远比10^80大的数。

所以,对于不是大到特别特别离谱的n来说,α(n)<=4是没毛病的。

后记

如果想要知道Ackermann(4,1)到底是多大的一个数,可以查看:很大、大得离谱的数


  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Set和Map都是Java中常用的集合类型,它们的区别在于Set是一组唯一的无序元素的集合,而Map是一组键值对的集合。 下面是对Set和Map的详细解释和代码案例: 1. Set Set是一组唯一的无序元素的集合。它实现了Collection接口,因此它继承了Collection接口中的所有方法。Set中的元素不能重复,而且没有顺序。 常用的Set实现类有HashSet和TreeSet。HashSet使用哈希表来存储元素,而TreeSet使用树来存储元素,并保证元素按照升序排列。 下面是一个HashSet的代码示例: ```java import java.util.HashSet; import java.util.Set; public class HashSetExample { public static void main(String[] args) { Set<String> set = new HashSet<>(); set.add("apple"); set.add("banana"); set.add("orange"); set.add("apple"); System.out.println(set); // 输出结果为 [banana, orange, apple] } } ``` 2. Map Map是一组键值对的集合。它提供了一种将键映射到值的方式,可以通过键来访问对应的值。Map中的键不能重复,而值可以重复。 常用的Map实现类有HashMap和TreeMap。HashMap使用哈希表来存储键值对,而TreeMap使用树来存储键值对,并保证键按照升序排列。 下面是一个HashMap的代码示例: ```java import java.util.HashMap; import java.util.Map; public class HashMapExample { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); map.put("apple", 1); map.put("banana", 2); map.put("orange", 3); map.put("apple", 4); System.out.println(map); // 输出结果为 {banana=2, orange=3, apple=4} } } ``` 以上就是对Set和Map的详细解释和代码案例,希望对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值