C语言经典算法之并查集

目录

前言

A.建议

B.简介

一 代码实现

二 时空复杂度

A.查找操作(Find):

B.合并操作(Union):

三 优缺点

A.优点:

B.缺点:

C.总结:

四 现实中的应用


前言

A.建议

1.学习算法最重要的是理解算法的每一步,而不是记住算法。

2.建议读者学习算法的时候,自己手动一步一步地运行算法。

tips:文中的(如果有)对数,则均以2为底数

B.简介

并查集(Disjoint Set Union,简称并查集,也叫不相交集合)是一种用来管理分组的数据结构。它主要支持两种操作:查找(Find)和合并(Union)。并查集通常用于处理元素的分组问题,比如连通性的问题,网络连接状态的问题等。

一 代码实现

#include <stdio.h>
#include <stdlib.h>

// 定义并查集结构体
typedef struct {
    int *parent;  // 父节点数组
    int *rank;    // 秩(用于优化合并操作)
    int size;     // 并查集的大小
} DisjointSet;

// 初始化并查集
void initDisjointSet(DisjointSet *set, int size) {
    set->size = size;
    set->parent = (int *)malloc(sizeof(int) * size);
    set->rank = (int *)malloc(sizeof(int) * size);

    // 初始化每个元素的父节点为自身,秩为0
    for (int i = 0; i < size; i++) {
        set->parent[i] = i;
        set->rank[i] = 0;
    }
}

// 查找操作(找到元素所在的集合的代表元素)
int find(DisjointSet *set, int x) {
    if (set->parent[x] != x) {
        // 路径压缩:将节点的父节点直接设为根节点,减小查找路径长度
        set->parent[x] = find(set, set->parent[x]);
    }
    return set->parent[x];
}

// 合并操作
void unionSets(DisjointSet *set, int x, int y) {
    int rootX = find(set, x);
    int rootY = find(set, y);

    // 按秩合并:将秩较小的集合合并到秩较大的集合上
    if (rootX != rootY) {
        if (set->rank[rootX] < set->rank[rootY]) {
            set->parent[rootX] = rootY;
        } else if (set->rank[rootX] > set->rank[rootY]) {
            set->parent[rootY] = rootX;
        } else {
            set->parent[rootY] = rootX;
            set->rank[rootX]++;
        }
    }
}

// 释放并查集占用的内存
void freeDisjointSet(DisjointSet *set) {
    free(set->parent);
    free(set->rank);
}

int main() {
    // 示例用法
    int size = 10; // 假设有10个元素
    DisjointSet set;
    initDisjointSet(&set, size);

    // 进行一些合并操作
    unionSets(&set, 0, 1);
    unionSets(&set, 2, 3);
    unionSets(&set, 4, 5);
    unionSets(&set, 6, 7);
    unionSets(&set, 1, 2);
    unionSets(&set, 5, 6);
    unionSets(&set, 3, 7);

    // 查找操作
    printf("Element 0 is in set with representative: %d\n", find(&set, 0));
    printf("Element 4 is in set with representative: %d\n", find(&set, 4));

    // 释放内存
    freeDisjointSet(&set);

    return 0;
}

这是一个简单的并查集的实现,你可以根据实际需要进行扩展和优化。并查集的主要思想是通过合并集合的操作,将元素划分到不同的集合中,同时通过查找操作找到元素所在的集合。这种数据结构在解决一些图论和连通性的问题中非常有用。

二 时空复杂度

并查集的主要操作是查找(Find)合并(Union)。下面是对这两个操作的时空复杂度分析:

A.查找操作(Find):

时间复杂度: Amortized O(α(n)),其中 α(n) 是 Ackermann 函数的反函数,增长非常缓慢,可以近似看作是常数时间。在实际应用中,查找操作几乎可以看作是常数时间。


空间复杂度: O(n),其中 n 是并查集的大小。这是由于需要存储每个元素的父节点。


B.合并操作(Union):

时间复杂度: Amortized O(α(n)),其中 α(n) 是 Ackermann 函数的反函数,增长非常缓慢,可以近似看作是常数时间。合并操作的复杂度主要由查找操作决定。


空间复杂度: O(n),同样是由于需要存储每个元素的父节点。

三 优缺点

A.优点:

高效的合并操作: 并查集的合并操作具有较好的平均时间复杂度。通过使用路径压缩和秩优化等技术,可以保持较低的合并代价。

高效的查找操作: 并查集的查找操作(Find)在实际应用中通常是近似常数时间,尤其是在使用路径压缩的情况下。

简单直观: 并查集的实现相对简单,易于理解和实现。这使得它成为解决一些连通性问题的常用选择。

空间效率: 并查集在空间上的占用主要是存储每个元素的父节点和秩等信息,相对来说是比较节省空间的。

B.缺点:

不适用于所有问题: 并查集主要用于处理集合的合并和查找操作,但并不适用于所有类型的问题。对于其他类型的问题,可能有更适合的数据结构。

无法回溯修改历史操作: 一旦执行了合并操作,就很难回溯修改历史的操作。这使得并查集在一些需要撤销或修改历史状态的场景中不够灵活。

可能存在树不平衡的问题: 尽管使用了路径压缩和秩优化等策略,但在实际操作中,仍可能导致并查集中树的不平衡,使得部分操作的代价较高。

C.总结:

总体来说,并查集在处理特定类型的问题时非常高效,但在某些场景下可能不如其他数据结构适用。在选择数据结构时,需要根据具体问题的特点和要求进行权衡。

四 现实中的应用

图论中的连通性问题: 并查集常被用于解决图的连通性问题,如判断图中是否存在环,或者确定两个节点是否连通。在网络设计、社交网络分析等领域,这种连通性问题经常需要解决。

图像分割: 在计算机视觉中,图像分割是一个重要的问题。并查集可以用于将图像中的像素分组,形成一些区域,使得同一区域内的像素属于同一集合。

操作系统中的文件系统管理: 文件系统中的文件和目录之间存在关系,而这些关系可以使用并查集来管理。当文件或目录被移动或删除时,使用并查集可以有效地更新这些关系。

网络连接状态的管理: 在网络通信中,可以使用并查集来管理设备之间的连接状态。这对于网络设计和维护具有实际意义,例如在通信网络中检测环路。

最小生成树算法: 在图论中,最小生成树算法(如Kruskal算法)通常使用并查集来判断边的两个端点是否在同一连通分量中,以避免形成环路。

社交网络中的关系管理: 在社交网络中,人与人之间的关系可以使用并查集来管理。当两个人建立关系(如好友关系)时,可以将它们合并到同一个集合中。

数据库查询优化: 在数据库中,关联查询可以通过并查集来优化。通过维护表之间的关系,可以更高效地执行一些查询操作。

  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
可持久化并查集是指在并查集的基础上,支持回退到任意历史版本。这个结构可以用来处理一些需要撤销或者回退操作的问题。以下是一个基本的可持久化并查集的实现。 ```python class Node: def __init__(self, parent=None, rank=0): self.parent = parent self.rank = rank class PersistentUnionFind: def __init__(self, size): self.n = size self.roots = [None] * (2 * size) self.ranks = [None] * (2 * size) def make_set(self, v): self.roots[v] = Node(v) self.ranks[v] = 0 def find(self, node, version): if node.parent is None: return node if node.parent != node: node.parent = self.find(node.parent, version) return node.parent def union(self, x, y, version): x_root = self.find(self.roots[x], version) y_root = self.find(self.roots[y], version) if x_root == y_root: return False if self.ranks[x_root] < self.ranks[y_root]: x_root, y_root = y_root, x_root new_root = Node(x_root, self.ranks[x_root] + (self.ranks[x_root] == self.ranks[y_root])) self.roots[x] = self.roots[y] = new_root self.ranks[x_root] = self.ranks[y_root] = new_root.rank return True def get_version(self): return len(self.roots) // self.n - 1 def get_root(self, v, version): return self.find(self.roots[v], version).parent.val ``` 这个代码中,我们使用了一个 `Node` 类来表示每个节点,其中 `parent` 表示节点的父亲,`rank` 表示节点的秩。我们需要用一个 `roots` 数组来保存所有版本的根节点,以及一个 `ranks` 数组来保存所有节点的秩。`make_set` 函数用来初始化一个新节点,这个节点的父亲指向自己,秩为 0。`find` 函数用来找到节点所在的集合的根节点。如果节点的父亲不是根节点,那么我们就递归地寻找它的父亲。在递归返回之前,我们将所有遍历过的节点的父亲都更新为根节点,这样可以加速下次查找。`union` 函数用来将两个节点所在的集合合并。首先找到两个节点所在集合的根节点,如果根节点相同,那么这两个节点已经在同一个集合中,不需要再次合并。否则,我们将秩较小的根节点挂在秩较大的根节点下面,同时更新秩。`get_version` 函数用来获取当前版本号,而 `get_root` 函数则用来获取节点在指定版本中所在的集合的根节点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

JJJ69

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

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

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

打赏作者

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

抵扣说明:

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

余额充值