用于不相交集合的数据操作——并查集

        假定有一组词汇,其中有一些词是同义词,可以把意思不同的词分别放到不同的集合中,构成一组不相交的集合,每个集合内部都是同义词。最开始我们不知到哪些词可以归并到相同的组中,因此开始的时候它们每个词为一组。然后我们再一一给出哪些词是同义词,据此将初始的组进行合并……直到最后同义词都被合并到各自应该归属的组里面。

        讲的简单点,假定这些单词是一个个的整数,他们构成了一组不相交的动态集合 S={S1,S2,,Sk} ,,每个集合可能包含一个或多个元素,并选出集合中的某个元素作为代表。每个集合中具体包含了哪些元素是不关心的,具体选择哪个元素作为代表一般也是不关心的。我们关心的是,对于给定的元素,可以很快的找到这个元素所在的集合(的代表),以及合并两个元素所在的集合,而且这些操作的时间复杂度都是常数级的。

        这就是并查集的问题,并查集的基本操作有三个:

  1. makeSet(s):建立一个新的并查集,其中包含 s 个单元素集合。
  2. unionSet(x, y):把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。
  3. find(x):找到元素 x 所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。

1.并查集的森林表示

        并查集的实现原理也比较简单,就是使用树来表示集合,树的每个节点就表示集合中的一个元素,树根对应的元素就是该集合的代表,如图 1 所示。

图 1 并查集的树表示

        图中有两棵树,分别对应两个集合,其中第一个集合为 {a,b,c,d} ,代表元素是 a ;第二个集合为 {e,f,g} ,代表元素是 e

        树的节点表示集合中的元素,指针表示指向父节点的指针,根节点的指针指向自己,表示其没有父节点。沿着每个节点的父节点不断向上查找,最终就可以找到该树的根节点,即该集合的代表元素。

2.构造并查集并初始化

        现在,假设使用一个足够大的的连续空间来存储树节点,那么 makeSet 要做的就是构造出如图 2 的森林,其中每个元素都是一个单元素集合,即父节点是其自身:

图 2 构造并查集初始化

3.并查集的find操作

        接下来,就是 find 操作了,如果每次都沿着父节点向上查找,那时间复杂度就是树的高度,完全不可能达到常数级。这里需要应用一种非常简单而有效的策略——路径压缩。路径压缩,就是在每次查找时,令查找路径上的每个节点都直接指向根节点,如图 3 所示。

图 3 路径压缩

4.并查集的合并操作

        最后是合并操作 unionSet,并查集的合并也非常简单,就是将一个集合的树根指向另一个集合的树根,如图 4 所示。

图 4 并查集的合并

        这里也可以应用一个简单的启发式策略——按秩合并。该方法使用秩来表示树高度的上界,在合并时,总是将具有较小秩的树根指向具有较大秩的树根。简单的说,就是总是将比较矮的树作为子树,添加到较高的树中。为了保存秩,需要为节点增加一个成员rank,并将其值初始化为 0。(树的秩:从树的根节点x到某一后代叶节点的最长简单路径上边的数目)


5.源代码


#include <ctime>
#include <cstdlib>
#include <iostream>
#include <iomanip>

using namespace std;

const int MAX = 20;	//节点总数
const int NUM = 50;	//操作次数

//节点定义
class Node{
	public:
		int num;		//节点编号,可以用来代表单词
		int son_num;		//该节点的孩子数量,代表本单词同义词家族单词数目
		int rank;		//节点所在树的秩
		Node *father;		//该节点家族的代表词汇
	public:
		Node() : num(0), son_num(1){
			father = NULL;
		}
		
};

//并查集类操作定义
class UF{
	public:
		Node *s;		//指向节点队列的指针,用于分配和释放内存
	public:
		UF();			
		~UF();
		Node* find(Node *r);	//查找本家族的代表词汇
		void unit(Node *x, Node *y);	//合并家族词汇
		void print();		//打印所有的节点信息
};

//构造函数,批量申请内存并初始化
UF::UF(){
	s = new Node[MAX]();
	for(Node *p = s ; p < s + MAX; p++){
		p -> num = p - s;
		p -> son_num = 1;
		p -> father = p;
		p -> rank = 0;
	}
}

//析构函数,释放空间
UF::~UF(){
	delete [] s;
	s = NULL;
}

//查找本家族代表单词
Node* UF::find(Node *r){
	if(r == r -> father){
		return r;
	} else {
		r -> father = find(r -> father);
		return r -> father;
	}
}

//合并两个家族
void UF::unit(Node *x, Node *y){
	Node *x_parent = find(x);
	Node *y_parent = find(y);
	if(x_parent != y_parent){
		if(x_parent -> rank > y_parent -> rank){
			y_parent -> father = x_parent;
			x_parent -> son_num += y_parent -> son_num;
		} else {
			x_parent -> father = y_parent;
			y_parent -> son_num += x_parent -> son_num;
			if(x_parent -> rank == y_parent -> rank){
				y_parent -> rank += 1;
			}
		}
	}
}

//打印所有节点的信息,以供参考(当MAX大于20时建议不要用该函数)
void UF::print(){
	cout << "序号:";
	for(Node* p = s; p < s + MAX; p++){
		cout << right << setw(3) << p -> num;
	}
	cout << endl;
	cout << "祖先:";
	for(Node* p = s; p < s + MAX; p++){
		cout << right << setw(3) << p -> father -> num;
	}
	cout << endl;
	cout << "大小:";
	for(Node* p = s; p < s + MAX; p++){
		cout << right << setw(3) << p -> son_num;
	}
	cout << endl;
	cout << "秩:  ";
	for(Node* p = s; p < s + MAX; p++){
		cout << right << setw(3) << p -> rank;
	}
	cout << endl;
}

//测试函数
int main(){
	UF uf;
	for(int i = 0; i < NUM; i++){
		int a = rand()%MAX;
		int b = rand()%MAX;
		uf.unit(uf.s + a, uf.s + b);
		cout << "第" << i << "次操作,a = " << a << " b = " << b << endl;
		uf.print();
		cout << endl;
	}
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值