算法学习-并查集(持续更新中)

本文参考:

最容易理解的并查集详解
详解:并查集(Union-Find)
「代码随想录」684. 冗余连接:【并查集基础题目】详解!
并查集从入门到出门

并查集常常在做图相关的题目时冒出来,但是笔者经常去回避这样的解法,这次找到个机会讲相关的知识和题目进行汇总。

基础知识

并查集常常用来解决图的连通性相关的问题,主要实现了下面几个方法:

class UnionFind {
	private int count; //记录连通分量
    private int[] parent; //节点x的父亲节点是parent[x]
    
    /* 将 p 和 q 连接 */
    public void union(int p, int q);
    /* 判断 p 和 q 是否连通 */
    public boolean connected(int p, int q);
    /* 返回当前节点的根节点 */
    public int find(int x);
    /* 返回图中有多少个连通分量 */
    public int count();
}

其中最重要的是「并」和「查」:

  • union – 合并两个节点,把两个节点所在的连通分量合并成一个
  • find – 查找节点所属的连通分量(也就是根)

「集合」使用一个根节点来标识:

数组 parent[] 来表示每个节点的父亲节点,如果自己就是根节点,那么 parent[i] = i,即自己指向自己.

其中方法的具体实现如下:

class UnionFind{
    private int count; //记录连通分量
    private int[] parent; //节点x的父亲节点是parent[x]
    
    public UnionFind(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 a,int b){
        int parentA = find(a);
        int parentB = find(b);
        if(parentA == parentB) return;
        // 将两颗树合并为一颗
        parent[parentA] = parentB;
        // 连通分量-1
        count--;
    }
	
	// 并查集里寻根的过程,迭代做法,从父节点向上继续找
    public int find1(int x){
          //根节点的parent[x]==x
          while (parent[x]!=x){
              x=parent[x];
          }
          return x;
     }
	
	// 并查集里寻根的过程,递归做法
    public int find2(int u) {
        if (x==parent[x]) return x;
        else return find(parent[u])}
	
	// 判断p和q是否连通:如果两个节点是连通的,那么他们一定拥有相同的根节点
    public boolean connected(int a,int b){
        return find(a) == find(b);
    }
	
	// 返回当前连通分量的个数
    public int getCount(){
        return this.count;
    }
}

优化方法

平衡性优化

思路:当我们每次连接两个节点的时候,不希望出现头重脚轻的情况,而希望到达一种平衡的状态
在这里插入图片描述

使用额外的一个数组 size[] 记录每个连通分量中的节点数,每次均把节点数少的分量接到节点数多的分量上,如下图所示:

注意:只有每个连通分量的根节点的 size[] 才可以代表该连通分量中的节点数

private int count;
private int[] parent;
private int[] size;

// 构造函数
public UnionFind (int n) {
    this.count = n;
    parent = new int[n];
    size = new int[n];
    for (int i = 0; i < n; i++) {
        parent[i] = i;
        // 最初,每个连通分量均为 1
        size[i] = 1;
    }
}

public void union(int p, int q) {
    int rootP = find(p);
    int rootQ = find(q);
    if (rootP == rootQ) return;
    /******** 修改部分 ********/
    if (size[rootP] < size[rootQ]) {
        parent[rootP] = rootQ;
        size[rootQ] += size[rootP]
    } else {
        parent[rootQ] = rootP;
        size[rootP] += size[rootQ]
    }
    /********** end **********/
    count--;
}

路径压缩

分析上述实现的方法,find() 是决定并查集时间复杂度的重要因素。抛开 find() 因素,其他方法的时间复杂度均可视为 O ( 1 ) O(1) O(1)。所以如果要优化算法的时间复杂度,需要从 find() 入手。

对于有 n 个节点 1 个连通分量的并查集来说,最坏的时间复杂度为 O ( n ) O(n) O(n),最好的时间复杂度为 O ( 1 ) O(1) O(1)

  • 最坏情况:全部只有左子节点
  • 最好情况:n - 1 叉树,即根节点有 n - 1 个子节点

find()的过程中将树高进行压缩,即子节点尽可能一步指向根节点:

迭代做法,子节点指向父亲节点的父亲节点

public int find(int x) {
    while (parent[x] != x) {
        // 进行路径压缩
        parent[x] = parent[parent[x]];
        x = parent[x];
    }
    return x;

递归做法,由于find()递归返回的是一个根,所以能一次性将这条路径上的节点全部拉平。

private int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);
    }
    return parent[x];
}

相关题目

参考题目,并查集从入门到出门

684.冗余连接

并查集,原来无向无环的图加入了一条边以后存在了一个环,现在要删除一条边让它继续无环,当有多种删除方案时,选择 edges 中最后出现的边。

聚焦于点,从前向后遍历每一条边,边的两个节点如果不在同一个集合,就将这条边的两个节点加入同一个集合,当遍历到某条边却发现其两个节点已经在同一个集合里,再加入这条边就会成环。

在进行路径压缩以后,find()可以在 O ( 1 ) O(1) O(1)内找到根节点,因此总体时间复杂度为 O ( N ) O(N) O(N)

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
        UnionFind unionFind=new UnionFind(edges.length);
        for(int[]e:edges){
            if(unionFind.isConnect(e[0],e[1])){
                return e;
            }
            unionFind.union(e[0],e[1]);
        }
        
        return new int[0];
    }

    class UnionFind{
        private int count;
        private int[] parent;
        public UnionFind(int n){
            this.count=n;
            parent=new int[n+1];
            for(int i=1;i<=n;i++){
                parent[i]=i;
            }
        }

        public void union(int a, int b){
            int parentA=find(a);
            int parentB=find(b);
            if(parentA==parentB) return;
            parent[parentA]=parentB;
            this.count--;
        }

        // public int find(int x){
        //     if(parent[x]==x) return parent[x];
        //     else return find(parent[x]);
        // }

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

        public boolean isConnect(int a,int b){
            return find(a)==find(b);
        }

        public int getCount(int x){
            return this.count;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

互联网民工蒋大钊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值