【数据结构高阶】第十一篇——并查集(原理+实现+应用)

本文介绍了并查集这一数据结构,包括其概念、原理、实现方式和应用。通过示例展示了如何查找元素所属集合、合并集合以及统计集合个数。并以LeetCode中的省份数量问题为例,阐述了并查集在解决实际问题中的应用。并查集是一种高效处理不相交集合合并与查询的数据结构,常用于图论和算法中。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

⭐️ 今天要和大家介绍一个新的数据结构——并查集。听名字好像是把集合合并再查找元素,其实总体来说也是这样的,下面我们来和大家好好聊一聊这个玩意~
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/data-structure


🌏概念和原理

并查集: 在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。并查集其实就是一个森林,由多个集合合并而成。

举例说明:
假设有10个人,给他们编号,依次是0-9,他们之间互相不认识,也就是说每个人都是一个独立的集合,用并查集表示就是如下图所示(其中每个成员是下标,内容为负数,代表该集合元素个数是该负数的绝对值,为正数,代表该下标的双亲是该正数。例如:对应的内容就是-1就是该小集体中元素个数为1,也就是自己一个元素):
在这里插入图片描述
现在,编号为0,3,4这三个人结识称为朋友,构成了一个朋友圈,也就是三个人形成一个集合,合并过程如下图:
在这里插入图片描述

下面是集合的树形表示:
在这里插入图片描述
接下来,编号为2,5,7,8成为一个朋友圈,编号为1,6,9成为一个朋友圈,合并结果如下:
在这里插入图片描述
用树形结构表示如下:
在这里插入图片描述
最后再操作一次,如果3和6结识称为好朋友,那么这两个朋友圈就要合并。合并分为两个步骤:

  1. 分别找到自己所在集合的根
  2. 把两个根按照上面的方法合并

树形图形式演示:
在这里插入图片描述
数组形式演示:
在这里插入图片描述

总结:

  1. 数组的下标对应集合中元素的编号
  2. 数组中如果为负数,负号代表根,数字的绝对值代表该集合中元素个数
  3. 数组中如果为非负数,代表该元素双亲在数组中的下标

🌏实现

🌲整体框架

总体来说,实现起来还是比较简单的,底层我们选择用vector来实现,构造函数将vector的内容都初始化为-1,框架如下:

class UnionFindSet
{
public:
	UnionFindSet(size_t size)
		:_ufs(size, -1)
	{}
private:
	vector<int> _ufs;
};

🌲查找元素属于哪个集合

步骤:

  1. 当元素对应的下标对应的内容为正数时(也就是该元素不为根),我们需要更新该下标,也就是index=_ufs[index],让自己成为双亲
  2. 继续判断,如果对应内容为负数,说明该下标是根,直接返回

代码实现如下:

int FindRoot(int x)
{
	assert(x >= 0 && x < _ufs.size());

	int index = x;
	while (_ufs[index] >= 0)
	{
		// 更新双亲
		index = _ufs[index];
	}
	return index;
}

🌲合并两个集合

两个步骤:

  1. 先分别找到两个集合对应的根
  2. 然后合并两个根,让其中一个根归并到一个根的下面,更新新的双亲的内容和被并入的根的内容(把内容修改为新根的下标)
    具体演示:
    在这里插入图片描述
    代码实现:
void Union(int x1, int x2)
{
	assert(x1 >= 0 && x1 < _ufs.size());
	assert(x2 >= 0 && x2 < _ufs.size());

	int root1 = FindRoot(x1);
	int root2 = FindRoot(x2);

	// 双亲不同就合并,否则不做处理
	if (root1 != root2)
	{
		_ufs[root1] += _ufs[root2];// 把root2合并到root1
		_ufs[root2] = root1;// 改变祖先
	}
}

🌲统计集合个数

很简单,遍历一遍数组,统计内容为负数的下标的个数,即统计了集合个数
代码实现如下:

int Size()
{
	int count = 0;
	for (auto e : _ufs)
	{
		if (e < 0)
			++count;
	}

	return count;
}

🌏应用

这里以LeetCode中的一道题为例,链接——省份数量

题目描述:

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。

实例:
在这里插入图片描述
思路:
这里就是用到并查集的知识,我们可以通过遍历二维数组得到每两个城市的关系,因为isConnected[i][j]和isConnected[j][i]的内容其实是一样的,所以我们这里只需要遍历右上角的一部分内容即可确定所有的两个城市的关系。
在这里插入图片描述

如果两个城市有关系,那么就合并,没有就不合并,最后返回集合个数就是身份数。
解题代码如下:

class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        // if (isConnected.size() == 1) return 1;
        vector<int> ufs(isConnected.size(), -1);
        for (size_t i = 0; i < isConnected.size(); ++i)
        {
            for (size_t j = i + 1; j < isConnected.size(); ++j)
            {
                // 为1j就合并
                if (isConnected[i][j]) Union(ufs, i, j);
            }
        }

        int count = 0;
        for (auto& e : ufs)
        {
            if (e < 0) count++;
        }

        return count;
    }
    int FIndRoot(vector<int>& ufs, int x)
    {
        int index = x;
        while (ufs[index] >= 0)
        {
            index = ufs[index];
        }
        return index;
    }

    // 合并两个集合
    void Union(vector<int>& ufs, int x1, int x2)
    {
        int root1 = FIndRoot(ufs, x1);
        int root2 = FIndRoot(ufs, x2);

        // 祖先不同就合并
        if (root1 != root2)
        {
            ufs[root1] += ufs[root2];// 把root2合并到root1
            ufs[root2] = root1;// 改变祖先
        }
    }
};

🌐总结

并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。应用的地方也比较多,查找的时间复杂度是每个集合的深度次,效率也很不错。今天的内容就到这里了,喜欢的话,欢迎点赞支持和关注~
在这里插入图片描述

<think>好的,用户问:“那是不是先把每个数据结构看完再去看算法思想”。我需要先理解他的问题。他可能之前得到了一个刷题的建议,现在在确认是否需要先学完所有数据结构再学算法。 用户可能是刚开始学习算法和数据结构的新手,对学习路径不太确定。他可能担心同时学习两者会不会混淆,或者是否有必要先打好数据结构的基础再进入算法。 我需要分析数据结构和算法思想的关系。数据结构是基础,但很多算法需要结合数据结构来理解。比如,栈和队列在BFS、DFS中的应用。如果完全分开学,可能会影响对算法的理解。 同时,用户可能希望有一个高效的学习路径,避免重复或浪费时间。他需要明确的学习顺序,以便合理安排学习计划。 回答时要指出两者相辅相成,建议交替学习,而不是完全割裂。比如学完数组和链表后,可以学双指针、滑动窗口等算法。这样既能巩固数据结构,又能立即应用。 还需要给出具体的例子,比如学完哈希表后,马上做两数之和,这样理论与实践结合,效果更好。同时强调基础阶段需要结合,但到了高级阶段可能需要更深入的数据结构知识。 最后要总结建议,分阶段交替学习,推荐一些经典题目,帮助用户更好地规划学习路线。确保回答清晰,有逻辑,解决用户的疑虑。</think>不需要严格分割先后!**数据结构和算法思想本质上是相辅相成的关系**,应该采用**交替螺旋式学习法**。以下是我的深度建议: --- ### 一、为什么不能割裂学习? 1. **现实场景的不可分割性** - 算法需要依托数据结构实现(如DFS用栈,BFS用队列) - 数据结构的选择影响算法效率(哈希表$O(1)$查找 vs 数组$O(n)$查找) 2. **认知心理学依据** - 建构主义学习理论:通过解决具体问题构建知识网络 - 及时反馈原则:学完数组立刻实践双指针算法,记忆留存率提升40% --- ### 二、推荐学习顺序(动态交织) #### 第一阶段:基础联动(每个数据结构搭配核心算法) ```markdown 1. 数组/链表 → 双指针(快慢指针、对撞指针) - 例题:移除元素(27)、反转链表(206) 2. 栈/队列 → BFS/DFS基础 - 例题:有效的括号(20)、二叉树的层序遍历(102) 3. 哈希表 → 查找优化 - 例题:两数之和(1)、字母异位词分组(49) 4. 树结构 → 递归与回溯 - 例题:二叉树的最大深度(104)、路径总和(112) ``` #### 第二阶段:算法深化(重点突破算法范式) ```markdown 1. 动态规划 → 需要数组/矩阵存储状态 - 例题:打家劫舍(198)、不同路径(62) 2. 滑动窗口 → 依赖双指针+哈希表 - 例题:最小覆盖子串(76)、找到字符串中所有字母异位词(438) 3. 堆结构 → 优先队列算法 - 例题:前K个高频元素(347)、合并K个排序链表(23) ``` #### 第三阶段:高阶融合(复杂结构+复合算法) ```markdown 1. 图结构 → DFS/BFS/拓扑排序 - 例题:课程表(207)、岛屿数量(200) 2. 并查集 → 连通性问题 - 例题:朋友圈(547)、冗余连接(684) 3. 前缀树 → 字符串处理 - 例题:实现Trie(208)、添加与搜索单词(211) ``` --- ### 三、避坑指南:常见错误学习方式 1. **盲目追求全面** ❌ 试图完全掌握红黑树再学排序算法 ✅ 先理解BST基本操作,快速进入快速排序/归并排序实践 2. **忽略应用场景** ❌ 单独背诵堆的代码实现 ✅ 通过「数据流中的中位数」理解堆的实战价值 3. **过早接触难题** ❌ 刚学完数组就挑战接雨水(42) ✅ 按照「盛水容器(11)→ 接雨水」的难度梯度推进 --- ### 四、高效学习节奏建议 1. **每日学习单元**(2小时) - 30分钟学习新数据结构(如哈希表原理) - 60分钟实践相关算法(两数之和、字母异位词) - 30分钟对比不同实现方案(暴力法 vs 哈希法) 2. **周计划示例** ```markdown | 周一 | 周二 | 周三 | 周四 | 周五 | |------------|------------|------------|------------|------------| | 数组+双指针| 链表+递归 | 哈希表应用 | 栈+DFS基础 | 队列+BFS | | 三数之和 | 反转链表 | 字母异位词| 二叉树遍历 | 层序遍历 | ``` --- ### 五、经典学习组合推荐 1. **链表 + 双指针** - 环形链表(141)→ 找环入口(142)→ 相交链表(160) 2. **树结构 + 递归** - 最大深度(104)→ 对称二叉树(101)→ 路径总和III(437) 3. **动态规划 + 状态压缩** - 斐波那契数列 → 打家劫舍(198)→ 零钱兑换(322) --- **核心原则**:像搭乐高一样学习——每掌握一个数据结构模块,立即用算法积木拼出完整解决方案。初期可参考《剑指Offer》的章节编排,后期根据个人薄弱点动态调整。
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Jasper@.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值