一种特殊的树形结构——并查集

并查集

并查集作为一种特殊的数据结构,主要用于解决连接问题,如网络中节点间的连接状态等,对于一组数据来说,并查集主要支持两个操作:1、Union(p,q)——将p,q两个元素合并在一同一个组中。2、find§——查找p元素在哪个组中。并查集还经常用来回答一个问题:isConnected(p,q)——p,q两个元素是否在同一个组中,即p,q两个元素是否相连接。接下来,主要介绍并查集的两种实现,QuickFind和QuickUnion。

并查集的实现

[QuickFind]

该种实现方式,可以保证并查集的find操作的时间复杂度为O(1)

[数据表示]

使用数组进行存储,例如

0123456789
id0101010101

如图所示,id相同的元素连接在一起,即奇数连接在一起,偶数连接在一起
按照这种表示方法,我们实现QuickFind的并查集

[代码实现]

class UnionFind{
	private:
		int* id;
		int count;
	public:
		UnionFind(int n){
			id = new int[n];
			count = n;
			for(int i=0;i<n;i++)
				id[i] = i;//初始状态下,每个元素自己跟自己一个组
		}		
		
		~UnionFind(){
			delete[] id;
		}
		
		int find(int p){//查找元素p所对应的id
			return id[p];
		}
		
		bool isConnected(int p, int q){//判断两元素是否相连
			return id[p] == id[q];
		}
		void unionElements(int p, int q){//将p,q两元素合并
			int pId = find(p);
			int qId = find(q);
			if(pId == qId)
				return;//如果两个元素已经相连接,那么直接return
			else{
				for(int i=0;i<count;i++)//遍历所有id为pId的元素,
										//使他们都与qId相连接
					if(pId == id[i])
						id[i] = qId;
			}
		}
			
};

[优缺点]

QuickFind的实现方式,就如名字所形容的
find操作的时间复杂度为O(1),但是
union操作的时间复杂度为O(n)

[QuickUnion]

将每一个元素看作是一个节点,并查集中每个节点有一个指针,指向他的父节点,如3->2,表示2,3相连,根节点指向自己

[数据表示]

使用数组进行存储,例如

0123456789
parent1118305188

其中,parent[i]表示i这个元素所指向的父亲是谁,如图所示。
在这里插入图片描述
按照这种表示方法,我们实现QuickUnion的并查集

[代码实现]

class QuickUnion{
private:
	int* parent;
	int count;
public:
	QuickUnion(int n){
		count = n;
		parent = new int[count];
		for(int i=0;i<count;i++)
			parent[i] = i;//初始化时让他们自己指向自己,表示谁和谁也不是一个组
	}
		
	~QuickUnion(){
		delete[] parent;
	}
	
	int find(int p){
		assert(p>=0 && p<count);
		while(p != parent[p]){
			//当p=parent[p]时,表示到达根节点
			p = parent[p];
		}
		return p;
	}
	
	void unionElements(int p, int q){
		assert(p>=0 && p<count);
		assert(q>=0 && q<count);
		int pRoot = find(p);
		int qRoot = find(q);
		if(pRoot = qRoot)
			return;
		parent[pRoot] = qRoot;//把p的根连接到q的根上
	}
	
	bool isConnected(int p, int q){
		assert(p>=0 && p<count);
		assert(q>=0 && q<count);
		int pRoot = find(p);
		int qRoot = find(q);
		return pRoot == qRoot;
	}
};

[优缺点]

QuickUnion的实现方式,find的时间复杂度虽然上升,但是union的复杂度下降,综合考虑,还是QuickUnion实现方式更为理想

并查集的优化

首先,并查集的实现方式为QuickUnion

[union优化-基于size的优化]

[优化思想]

p,q两部分进行union操作。将少的一部分的根节点指向多的一部分的根节点,防止树过深。
比如说,union(4,9),按照我们之前union的实现方式,结果如下,这时候,我们树的层数变深,会导致find操作时间变长
在这里插入图片描述
解决办法:我们再union操作时,不是固定的将p连接到q,而是通过比较p,q两个树的元素的多少,将元素少根节点连到元素多的根节点上。如图所示
在这里插入图片描述

[代码实现]

//伪代码,在QuickUnion基础上进行修改
//增加数组存每个组的元素个数 
int* sz;
//构造函数,初始化sz
sz = new int[count];
for(int i=0;i<count;i++){
	sz = 1;//初始时,每个组的元素个数为1
}
//析构函数,释放内存
delete[] sz;
//union操作,优化
if(sz[pRoot]>sz[qRoot]){
	parent[qRoot] = pRoot;
	sz[pRoot] += sz[qRoot];//维护sz数组
}
else{
	parent[pRoot] = qRoot;
	sz[qRoot] += sz[pRoot];//维护sz数组
}

[union优化-基于rank的优化]

[优化思想]

通过对上面的基于size的优化,我们很容易想到,更好的一种方法,就是比较p,q的层数,将层数少的连接到层数多的上面,防止树的层数过深,话不多说,直接上代码实现

[代码实现]

//伪代码,在QuickUnion基础上进行修改
//增加数组存每个组的层数 
int* rank;
//构造函数,初始化rank
rank = new int[count];
for(int i=0;i<count;i++){
	rank = 1;//初始时,每个组的层数为1
}
//析构函数,释放内存
delete[] rank;
//union操作,优化
if(rank[pRoot]>rank[qRoot]){
	parent[qRoot] = pRoot;//将q连接到p
	//注意!此时rank[p]数组的层数并没有改变
}
else if(rank[pRoot]<rank[qRoot]){
	parent[pRoot] = qRoot;//将p连接到q
	//注意!此时rank[p]数组的层数并没有改变
}
else{
	parent[pRoot] = qRoot;//两者层数相等,谁连接到谁都一样
	rank[qRoot]++;//此时注意维护rank[qRoot]
}

[find优化-路径压缩]

[优化思想]

进行find操作。我们每次不一次向上移一个节点,可以向上移动两个,或者多个,因为并查集树的特殊性(根节点指向自己)不会导致越界的情况。
比如说,find(4),按照我们之前find的实现方式,我们在向上查找的同时也遍历了一遍这个树。由于并查集树的特殊性,我们每次向上移动多个节点,并且改变待查找元素的根节点,每执行一遍find(4),操作就进行一次路径压缩。如图所示。
在这里插入图片描述

[代码实现]

//伪代码,在QuickUnion基础上进行修改
//find操作,路径压缩
while( p != parent[p] )
	parent[p] = parent[parent[p]];//每次向上移动两个节点,并更新根节点
	p = parent[p];
}

[进一步的路径压缩]

通过上面的例子,我们很容易想到,如果我们能优化成,下面的样子,那岂不是更好
在这里插入图片描述

[代码实现]

我们通过递归来实现这种路径压缩

//函数定义:find(p)函数返回p的根节点
//递归调用,一定要明确函数的定义
int find(int p){
	if(p != parent[p])
		parent[p] = find(parent[p]);
	return parent[p];
	}
}

想说的话

至此并查集的内容已经介绍完毕了,并查集这种数据结构应用很广泛,在后面的图论中也会通过并查集这种数据结构判断联通分量等。下期更新图这种数据结构,大家加油~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值