数据结构 —— 并查集

今天我们来了解一下并查集:

并查集

并查集(Disjoint Set Union, DSU)是一种高效的数据结构,用于处理集合的合并(Union)和查询(Find)操作。它的设计目的是快速判断两个元素是否属于同一个集合,并能够将多个集合合并成一个集合。并查集广泛应用于解决一些涉及分类、归属判断的问题,如图的连通性分析、社交网络的朋友圈划分、计算机网络中的域划分等场景。

并查集的核心思想围绕两个基本操作:

  1. 查找(Find):确定一个元素所属的集合。这一操作通常伴随着“路径压缩”优化,即在查找的过程中,将沿途经过的所有节点直接连接到它们的根节点,从而加速后续的查找操作。

  2. 合并(Union):将两个不同的集合合并成一个集合。为了保持数据结构的高效性,合并时往往采用按秩(每个集合中树的最大深度)或按大小(集合中元素的数量)的启发式合并策略,以避免树的高度过高,保证操作的近似对数时间复杂度。

并查集的典型应用场景包括:

  • 连通分量计数:在无向图中快速找出有多少个连通分量。
  • 判断连通性:给定两个顶点,判断它们是否在同一个连通分量内。
  • 最小生成树算法(如Kruskal算法)中的边的集合判断。
  • Tarjan算法中的强连通分量识别。
  • 动态集合划分问题,如组织管理、文件系统等。

并查集的魅力在于其简单直观的实现方式和高效的运行效率,尤其是在大数据量下的优秀表现。

并查集的操作其实就是合并两颗树
在这里插入图片描述
在这里插入图片描述同时我们这里可以直接运用数组数组里面存放指向父亲的父节点
在这里插入图片描述
现在我们来实现一个并查集:

并查集实现

我们把架子搭好:

#pragma once
#include<iostream>
#include<vector>

//并查集
class MyUnion
{
public:
    //初始化并查集
    MyUnion(size_t size)
      :_ufs(size,-1)
    {

    }

    //合并
    void Union(size_t number1,size_t number2)
    {

    }

    //寻根
    int FindRoot(size_t number)
    {

    }

    //判断是否在同一集合
    bool InSet(size_t number1,size_t number2)
    {
        
    }

    //共有几个集合
    size_t SetSize()
    {

    }



private:
    std::vector<int> _ufs;
};

我们这里最重要的函数就是要实现寻根,寻根函数是其他函数的基础。

比如我有这样的一个集合:
在这里插入图片描述现在我想把3合并到0那里:

肉眼看其实很简单,就是把3里面的数合并到0处,然后修改3里面的值,改为0(指向0)
在这里插入图片描述

这个时候0这个集合有两个数0,3。我们可以推出几个结论:

  1. 对应下标里面的数是负数,则这个数为
  2. 对应负数的绝对值表示这个集合里面的元素个数

这样寻根函数就很好写了,给定一个值,访问对应下标里面的数,若为负数则为根,否则迭代,直到为负数

    //寻根
    int FindRoot(size_t number)
    {
        int root = number;

        while(_ufs[root] >= 0)
        {
            root = _ufs[root];
        }

        return root;
    }

有了寻根函数,合并集合的操作也变的简单:

    //合并
    void Union(size_t number1,size_t number2)
    {
        int root1 = FindRoot(number1);
        int root2 = FindRoot(number2);

        if(root1 == root2)
            return;
				
		//这里让大树合并到小树上,树的高度更低,寻根次数会变低
        if(root1 < root2)
            std::swap(root1,root2);
            
		//父节点个数加上子集合中的个数
        _ufs[root1]+=_ufs[root2];
       //子节点指向父节点
        _ufs[root2] = root1;
    }

给出完整代码:

#pragma once
#include<iostream>
#include<vector>

// 并查集类定义
class MyUnion
{
public:
    // 构造函数,初始化并查集大小,每个元素初始时属于独立的集合,使用负数表示集合的大小(或作为父节点标记)
    MyUnion(size_t size)
      :_ufs(size, -1) // _ufs[i] 的值为负数表示 i 是一个集合的根,绝对值表示该集合的元素数量
    {
    }

    // 合并操作,将属于number1和number2的集合合并
    void Union(size_t number1, size_t number2)
    {
        // 分别找到number1和number2的根节点
        int root1 = FindRoot(number1);
        int root2 = FindRoot(number2);

        // 如果根相同,说明已经在同一集合内,无需合并
        if(root1 == root2)
            return;

        // 将较小深度的集合合并到较大深度的集合下,以保持树的平衡(此处通过比较根的值实现)
        if(root1 < root2)
            std::swap(root1, root2);

        // 合并:调整根节点的值以表示集合大小的变化
        _ufs[root1] += _ufs[root2]; 
        _ufs[root2] = root1; // 设置root2的父节点为root1
    }

    // 寻找给定元素的根节点,使用路径压缩优化
    int FindRoot(size_t number)
    {
        int root = number;

        // 路径压缩:直接将沿途所有节点指向根节点
        while(_ufs[root] >= 0)
        {
            root = _ufs[root];
        }

        return root;
    }

    // 判断两个元素是否属于同一集合
    bool InSet(size_t number1, size_t number2)
    {
        // 通过比较两元素的根节点是否相同来判断
        return FindRoot(number1) == FindRoot(number2);
    }

    // 返回并查集中集合的数量
    size_t SetSize()
    {
        size_t size = 0;
        // 遍历数组,负数表示集合的根,因此计数负数的个数即为集合的数量
        for(auto e : _ufs)
        {
            if(e < 0)
                size++;
        }

        return size;
    }

    // 打印并查集当前状态
    void Print()
    {
        for(auto e : _ufs)
        {
            std::cout << e << " ";
        }
        std::cout << std::endl;
    }

private:
    // 存储并查集数据的向量,_ufs[i]的值如果是负数表示i是集合的根,绝对值表示集合的大小;非负数表示其父节点的索引
    std::vector<int> _ufs;
};

我们来试试:

#include"MyUnion.h"

int main()
{
    MyUnion myUnion(23);

    myUnion.Union(12,2);
    myUnion.Union(12,7);
    myUnion.Union(12,0);
    myUnion.Union(8,4);

    myUnion.Print();

    return 0;
}

在这里插入图片描述

省份数量

力扣上面也有一道关于并查集的题:
在这里插入图片描述

https://leetcode.cn/problems/number-of-provinces/description/

这道题用并查集就很合适:

//并查集
class MyUnion
{
public:
    //初始化并查集
    MyUnion(int size)
      :_ufs(size,-1)
    {

    }

    //合并
    void Merege(int number1,int number2)
    {
        int root1 = FindRoot(number1);
        int root2 = FindRoot(number2);

        if(root1 == root2)
            return;

        _ufs[root1]+=_ufs[root2];
        _ufs[root2] = root1;
    }

    //寻根
    int FindRoot(int number)
    {
        int root = number;

        while(_ufs[root] >= 0)
        {
            root = _ufs[root];
        }

        return root;
    }

    //判断是否在同一集合
    bool InSet(int number1,int number2)
    {
        return FindRoot(number1) == FindRoot(number2);
    }

    //共有几个根
    size_t SetSize()
    {
        size_t size = 0;
        for(auto e : _ufs)
        {
            if(e < 0)
                size++;
        }

        return size;
    }

private:
    vector<int> _ufs;
};

class Solution 
{
public:
    int findCircleNum(vector<vector<int>>& isConnected) 
    {
        MyUnion myUnion(isConnected.size());

        for(int i = 0; i < isConnected.size(); i++)
        {
            for(int j = 0; j < isConnected[i].size(); j++)
            {
                if(isConnected[i][j]==1)
                {
                    myUnion.Merege(i,j);
                }
            }
        }

        return myUnion.SetSize();
    }
};

在这里插入图片描述

并查集的优化

我们这里主要介绍路径压缩:
目的:减少查询路径上的节点数量,使得后续的查询更快。

实现:在执行FindRoot操作时,不仅找到指定节点的根节点,还将沿途的所有节点直接连接到根节点。这样,之后对于这些节点的任何查询都将直接到达根,而无需再次遍历路径。

    // 寻根操作,采用路径压缩优化
    int FindRoot(size_t number)
    {
        int root = number;

        // 不断迭代直至找到根节点,根节点的.ufs值为负
        while(_ufs[root] >= 0)
        {
            root = _ufs[root]; // 循环直至到达集合的根
        }

        // 路径压缩:更新从number到根节点这条路径上的所有节点的父节点直接指向根节点
        // 这样下次查询这些节点时可以直接跳到根,减少查询时间
        while(number != root)
        {
            // 将当前节点的父节点设置为根节点
            int father = _ufs[number]; // 临时存储当前节点的父节点
            _ufs[number] = root;       // 将当前节点的父节点指向上一步找到的根节点
            number = father;           // 移动到下一个节点继续进行路径压缩
        }

        return root; // 最终返回根节点的索引
    }

在这里插入图片描述

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值