并查集(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;
    }
};

十、总结

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

优点:

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

简洁:代码实现相对简单

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

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

相关推荐
草莓工作室3 小时前
数据结构7:栈和队列
c语言·数据结构
小欣加油4 小时前
leetcode 143 重排链表
数据结构·c++·算法·leetcode·链表
Camel卡蒙4 小时前
数据结构——字典树Trie(介绍、Java实现)
java·数据结构
爱吃生蚝的于勒4 小时前
【Linux】深入理解进程(一)
java·linux·运维·服务器·数据结构·c++·蓝桥杯
猫梦www5 小时前
力扣21:合并两个有序链表
数据结构·算法·leetcode·链表·golang·力扣
草莓工作室5 小时前
数据结构8:栈
c语言·数据结构
Han.miracle5 小时前
数据结构——排序的学习(一)
java·数据结构·学习·算法·排序算法
晚枫~6 小时前
图论基础:探索节点与关系的复杂网络
网络·数据结构·图论
liu****6 小时前
20.哈希
开发语言·数据结构·c++·算法·哈希算法
一匹电信狗7 小时前
【LeetCode_160】相交链表
c语言·开发语言·数据结构·c++·算法·leetcode·stl