并查集(Union-Find)数据结构详解

一、为什么需要并查集?

社交网络问题

  • 小明和小红是朋友,小红和小丽是朋友,那么小明和小丽是否在同一个朋友圈里?
  • 如何快速判断两个人是否能通过朋友关系间接认识?

团队划分

  • 班级里要分组做项目,已知一些同学必须在同一组,如何确定最终能分成几个组?

这些问题的共同特点是:

  • 需要维护元素之间的关系(连通/属于同一组)
  • 需要快速查询两个元素是否有关系
  • 需要动态合并两个组/集合
  • 需要统计总共有多少个独立的组

并查集这种数据结构在图论(例如 Kruskal 求最小生成树)、连通分量统计、网络连通性检测等场景非常常用。

二、什么是并查集?

并查集(Union-Find Set),也称为不相交集合数据结构(Disjoint Set Data Structure),是一种树型的数据结构,用于处理一些不交集的合并及查询问题。它的两个核心操作:

  • Find(查找):确定某个元素属于哪个子集
  • Union(合并):将两个子集合并成一个集合

核心思想:用树形结构来表示集合,每个集合用一棵树来表示,树的根节点就是该集合的代表元素。所有属于同一个集合的元素最终都能追溯到同一个根节点。

三、基本实现

3.1 朴素实现

cpp 复制代码
class UnionFindSet
{
public:

	UnionFindSet(size_t n)
		:_ufs(n, -1)
	{ }

	int FindRoot(int x) //查找根
	{
		int parent = x;
		while (_ufs[parent] >= 0)
		{
			parent = _ufs[parent];
		}
        return parent;
	}

	void Union(int x, int y) //合并节点
	{
		int root_x = FindRoot(x);
		int root_y = FindRoot(y);
		if (root_x == root_y)
			return;

		_ufs[root_x] += _ufs[root_y];
		_ufs[root_y] = root_x;
	}

	bool InSet(int x, int y) //俩个节点是否在同一个集合
	{
		return FindRoot(x) == FindRoot(y);
	}

	size_t SetSize()
	{
		int count = 0;
		for (auto x : _ufs)
		{
			if (x < 0) count++;
		}

		return count;
	}

private:
	vector<int> _ufs;
};

3.2 朴素实现的问题

朴素实现存在一个严重问题:树可能退化成链表

假设我们依次合并 (1,2), (2,3), (3,4), (4,5):1 → 2 → 3 → 4 → 5

这种情况下,查找元素1的根节点需要O(n)的时间复杂度,效率很低。

四、优化策略

4.1 路径压缩(Path Compression)

核心思想:在查找根节点的过程中,将路径上的所有节点都直接连接到根节点。

cpp 复制代码
int FindRoot(int x) 
{
    if (parent[x] != x) 
    {
        parent[x] = FindRoot(parent[x]);  // 路径压缩
    }
    return parent[x];
}

4.2 按秩合并(Union by Rank)

按秩合并的思想:总是将较小的树连接到较大的树上,保持树的平衡。

cpp 复制代码
class UnionFind {
private:
    vector<int> parent, rank;  // rank[i]表示以i为根的树的高度
    
public:
    UnionFind(int n) : parent(n), rank(n, 0) 
    {
        for (int i = 0; i < n; i++) 
        {
            parent[i] = i;
        }
    }
    
    void unite(int x, int y) 
    {
        int rootX = find(x);
        int rootY = find(y);
        
        if (rootX != rootY) 
        {
            if (rank[rootX] < rank[rootY]) 
            {
                parent[rootX] = rootY;
            } 
            else if (rank[rootX] > rank[rootY]) 
            {
                parent[rootY] = rootX;
            } 
            else 
            {
                parent[rootY] = rootX;
                rank[rootX]++;
            }
        }
    }
};

五、完整的优化实现

cpp 复制代码
class OptimizedUnionFind {
private:
    vector<int> parent, rank;
    int components;  // 连通分量个数
    
public:
    OptimizedUnionFind(int n) : parent(n), rank(n, 0), components(n) {
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }
    
    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);  // 路径压缩
        }
        return parent[x];
    }
    
    bool unite(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        
        if (rootX == rootY) return false;  // 已经连通
        
        // 按秩合并
        if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY;
        } else if (rank[rootX] > rank[rootY]) {
            parent[rootY] = rootX;
        } else {
            parent[rootY] = rootX;
            rank[rootX]++;
        }
        
        components--;  // 连通分量减少
        return true;
    }
    
    bool connected(int x, int y) {
        return find(x) == find(y);
    }
    
    int getComponents() {
        return components;
    }
};

十、总结

并查集是一个看似简单但功能强大的数据结构:

优点:

高效:经过优化后,操作时间复杂度接近常数

简洁:代码实现相对简单

通用:适用于多种连通性问题

通过合理的优化,并查集能够在近似常数时间内完成查找和合并操作,是解决连通性问题的首选数据结构。

相关推荐
西望云天4 小时前
基础组合计数(三道例题)
数据结构·算法·icpc
hn小菜鸡7 小时前
LeetCode 2540.最小公共值
数据结构·算法·leetcode
Z_z在努力8 小时前
【数据结构】List 详解
数据结构·list
神龙斗士24010 小时前
Java 数组的定义与使用
java·开发语言·数据结构·算法
Z_z在努力11 小时前
【数据结构】队列(Queue)全面详解
java·开发语言·数据结构
岑梓铭11 小时前
《考研408数据结构》第二章《线性表(顺序表、链表)》复习笔记
数据结构·笔记·考研
日落辞朝阳13 小时前
数据结构——顺序表
数据结构
Z_z在努力14 小时前
【数据结构】哈希表(Hash Table)详解
数据结构·哈希算法·散列表
MMjeaty15 小时前
数据结构——栈和队列
数据结构·算法