并查集介绍 & 代码实现 & 优化思路详解

什么是并查集

并:合并;

查:查询;

集:集合

其实就是只用来进行 并 和 查 操作的集合。并查集常用来解决连通性问题

通俗来讲:就是当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集。

并查集主要有两个功能:

  • 将两个元素添加到一个集合中。
  • 判断两个元素在不在同一个集合

并查集的实现思路分析

1、用森林,表示集合特性

将一个个集合看作一棵棵树。

  • 不同集合图示:
image-20231021103252666
  • 森林图示:image-20231021124855536

由上图对比可知,若干个互不相交的子集 相当于 森林中若干互不相关的子树

因此:将同一子集的各个元素组织为一棵树,那么不同的子树就构成了森林,那么不同的子集就构成了森林

这样的好处:在实现 并 与 查 功能时,变成了对树的操作,理解形象。

因此对查和并的理解变化为:

  • 从两个结点元素出发,一直向上找其根节点,若根节点相同,则属于同一个集合;反之属于不同集合。

  • 让一棵树作为另一棵树的子树。


2、存储结构使用:双亲表示法

因为双亲表示法可以直接找到结点的双亲,数组S[]中存储的是对应位置元素的双亲结点的下标

即:S[x] = t ===》结点x的双亲结点的下标为t。

image-20231021124926267

并且只需要一个一维数组(S[])就能表示集合关系

数组元素的下标代表元素名,根节点的下标代表子集名,根节点的双亲结点为负数


代码实现与优化

代码实现(优化前)

1、初始化并查集

# define SIZE 50
int UFSets[SIZE]; // 集合元素数组

// 1.初始化并查集 :初始化为 -1,因为根节点为-1
void init(int S[]){
    for (int i = 0; i < SIZE; ++i) {
        S[i] = -1;
    }
}

2、查(Find)

// 2. Find:查操作,一直向上,找到元素x所属集合
int Find(int S[],int x){
    // 即:循环寻找x的根,找到根节点就结束
    while (S[x] >= 0){
        x = S[x];
    }
    return x;
}

3、并(Union)

// 3. Union “并”操作,将两个集合合并为一个
// 思路:①首先看两者是否已经为同一集合
//      ②由于使用双亲表示法,因此直接将一根连接到另一根下面
void Union(int S[],int Root1,int Root2){
    if(Root1 == Root2) return; // 属于同一集合就直接返回
    S[Root1] = Root2; // 将根2连接到根1下边(可以理解为树的合并)
}

优化1:Union的优化

1、时间复杂度分析

image-20231021124950673

并:O(1)

查:O(n) <— 最坏情况

可以看到,查操作的时间复杂度与树高密切相关

而由于并操作的粗暴性,导致树高不受控,最终查操作的复杂度也是比较高的。

因此就有了如下的优化思路:

每次Union合并时,尽可能不让树长高。


2、具体的优化方法小树合并到大树

image-20231021125013373image-20231021125013373

  1. 根节点的绝对值来记录树中的结点数,据此来判断树的大小。

3、优化后代码:

// 优化后Union
void UnionPlus(int S[],int Root1,int Root2){
    // 1.属于同一集合就直接返回
    if(Root1 == Root2) return;

    // 2.比较两树的高
    int height1 = abs(Root1);
    int height2 = abs(Root2);
    if(height1 > height2){
        // 2.1 如果树1结点数更多、更高,就将树2并到树1
        S[Root1] += S[Root2];
        S[Root2] = Root1;
    }else{
        // 反之就将树1并到树2
        S[Root2] += S[Root1];
        S[Root1] = Root2;
    }
}

可以看到:

  • 1.优化后Union的复杂度不变,仍为O(1),而Find的复杂度变为O(logn)

  • 2.构造的树高不超过 ⌊logn⌋+1

    因为:n个结点的完全二叉树的高度为 ⌊ logn ⌋+1


优化2:Find的优化 — 压缩路径

优化1中,是通过调整并(Union)操作,来降低查(Find)操作的复杂度。

那么这次优化中,就通过调整 查(Find)操作,优化它本身。

1、回顾一下原始Find

image-20231021123130365

在这个操作中,x每一次都会找到自己的父结点,依次迭代,直到找到其根节点。

那么其实在每一次查x时,都要进行这样的全局迭代操作,代价还是挺大的。

那既然目标是找到其根(root),那么不如直接将x的根作为x的父结点,也就是将x直接挂载到根节点下

那么再考虑一件事:x找根的这条路上,经历过的一切结点,其根不都是x的根节点嘛


2、基于这两点考虑,就有了压缩路径的思路

image-20231021121132120

image-20231021115114262

一边Find,一边进行压缩路径。

上边图解可以看出,将路径上的结点直接挂载到根节点之下。这样在下次Find时,查找时间大大减少。

代码:

// 优化后Find
int FindPlus(int S[],int x){
    int root = x;
    // 1.循环找到x的根
    while(S[root] >= 0) root = S[root];

    // 2.压缩路径:将x寻根路径上所有的结点,都直接指向根节点(root)
    while(x!=root){
        int temp = S[x]; // temp指向x的父节点
        S[x] = root;  // 将x直接挂载到根节点之下
        x = temp; // 继续向上操作,将x的Find路径上的所有结点,全部直接指向根节点root
    }
    return root;
}

代码实现(优化后):

1、初始化并查集

# define SIZE 50
int UFSets[SIZE]; // 集合元素数组

// 1.初始化并查集 :初始化为 -1,因为根节点为-1
void init(int S[]){
    for (int i = 0; i < SIZE; ++i) {
        S[i] = -1;
    }
}

2、查(Find)

// 优化后Find
int FindPlus(int S[],int x){
    int root = x;
    // 1.循环找到x的根
    while(S[root] >= 0) root = S[root];

    // 2.压缩路径:将x寻根路径上所有的结点,都直接指向根节点(root)
    while(x!=root){
        int temp = S[x]; // temp指向x的父节点
        S[x] = root;  // 将x直接挂载到根节点之下
        x = temp; // 继续向上操作,将x的Find路径上的所有结点,全部直接指向根节点root
    }
    return root;
}

3、并(Union)

// 优化后Union
void UnionPlus(int S[],int Root1,int Root2){
    // 1.属于同一集合就直接返回
    if(Root1 == Root2) return;

    // 2.比较两树的高
    int height1 = abs(Root1);
    int height2 = abs(Root2);
    if(height1 > height2){
        // 2.1 如果树1结点数更多、更高,就将树2并到树1
        S[Root1] += S[Root2];
        S[Root2] = Root1;
    }else{
        // 反之就将树1并到树2
        S[Root2] += S[Root1];
        S[Root1] = Root2;
    }
}

并查集优化总结:

将n个独立元素Union为一个集合,要经过(n-1)次Union。

image-20231021121311286

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值