天天看點

并查集的了解與實作總結

并查集的應用十分廣泛,包括一些算法,當應用上并查集的時候,也會更容易實作。下面總結下并查集的相關内容。

什麼是并查集?

個人的了解是:并查集就是對集合三種常用操作的再一次抽象。分别是集合的合并(Union)、元素的搜尋(Find)和對集合的分解。因為這3中操作非常常用并且又不囿于集合,是以就把這一組操作抽象成一個獨立的資料結構。

标準定義

在一些應用問題中,需要将n個不同的元素劃分成一組不相交的集合。開始時每個元素自成一個單元素集合,然後按照一定規律将歸于同一組元素的集合合并,在此過程中需要反複查詢某個元素歸屬于哪個集合的運算,适合于描述這類問題的抽象資料類型稱為并查集(union-find set)。

并查集的3種操作!

從上面的定義也可以看出來,并查集的三種操作是:

(1)Union(Root1, Root2):把子集合Root2并入集合Root1中。要求這兩個集合互不相交,否則不執行合并。

(2)Find(x):搜尋單元素x所在的集合,并傳回該集合的名字。

(3)UnionFindSets(s):構造函數,将并查集中s個元素初始化為s個隻有一個單元素的子集合。

并查集的一種實作方案

實作并查集的方式有多種,這裡主要總結用**樹結構(父指針表示法)**來實作并查集及其相關操作。

用這種實作方式,每個集合用一棵樹表示,樹的每一個節點代表集合的一個單元素。所有各個集合的全集合構成一個森林,并用樹與森林的父指針表示法來實作。其下标代表元素名。第I個數組元素代表包含集合元素I的樹節點。樹的根節點的下标代表集合名,根節點的父為-1,表示集合中元素個數。

下面看一個例子:

全集合是S = {0,1,2,3,4,5,6,7,8,9},初始化每個元素自成為一個單元素子集合。(書上原圖,感覺挺清晰的)

并查集的了解與實作總結

經過一段時間的計算,這些子集合并成3個集合,他們是全集合S的子集合:S1 = {0,6,7,8},S2= {1,4,9},S3 = {2,3,5}。則表示他們并查集的樹形結構如下圖:

并查集的了解與實作總結

上面數組中的元素值有兩種含義:

(1)負數表示目前節點是樹的根節點,負數的絕對值表示樹中節點的個數,也即集合中元素的個數。

(2)正數表示其所屬的樹的根節點,由樹形表示很容易了解,這也是樹的父指針表示的定義。

經過上面對相關資料的組織,再回頭來看并查集的3中核心操作是怎樣依托于樹來實作的:

(1)将root2并入到root1中,其實就可以直接把root2的數組元素(就是他的父節點)改成root1的名字(就是他所在的數組下标)。

下面的圖表示了合并兩個子集合的過程:

并查集的了解與實作總結

(2)查找x所屬于的根節點(或者說是x所屬于的集合),就可以一直找array[x],直到array[x]小于0,則證明找到了根(所在集合)。

下面的圖示意了查找一個節點所屬集合的過程:

并查集的了解與實作總結
(3)将整個集合初始化為單元素集合,其實就是建立樹的父指針數組的過程,把數組元素全初始化為-1,也就表示了每個元素都各占一個集合。

有了上面的理論,代碼也比較容易實作出來!下面給出了一個代碼的執行個體:

/*
*樹結構建構并查集,其中樹用父指針形式表示
*/
#include <iostream>

const int DefaultSize = 10;

class UFSets {										//集合中的各個子集合互不相交
public:
	UFSets(int sz = DefaultSize);					//構造函數       (并查集的基本操作)
	~UFSets() { delete[] parent; }					//析構函數
	UFSets& operator = (UFSets& R);					//重載函數:集合指派
	void Union(int Root1, int Root2);				//兩個子集合合并 (并查集的基本操作)
	int Find(int x);								//搜尋x所在集合	 (并查集的基本操作)
	void WeightedUnion(int Root1, int Root2);		//權重的合并算法 
private:
	int *parent;		//集合元素數組(父指針數組)
	int size;			//集合元素的數目
};

UFSets::UFSets(int sz) {
	//構造函數,sz是集合元素的個數,父指針數組的範圍0到sz-1
	size = sz;										//集合元素的個數
	parent = new int[size];							//開辟父指針數組
	for (int i = 0; i < size; i ++) {				//初始化父指針數組
		parent[i] = -1;								//每個自成單元素集合
	}
}

int UFSets::Find(int x) {
	//函數搜尋并傳回包含元素x的樹的根
	while (parent[x] >= 0) {
		x = parent[x];
	}
	return x;
}

void UFSets::Union(int Root1, int Root2) {
	//函數求兩個不相交集合的并,要求Root1與Root2是不同的,且表示了子集合的名字
	parent[Root1] += parent[Root2];					//更新Root1的元素個數
	parent[Root2] = Root1;							//令Root1作為Root2的父節點
}

void UFSets::WeightedUnion(int Root1, int Root2) {
	//使用節點個數探查方法求兩個UFSets集合的并
	int r1 = Find(Root1);							//找到root1集合的根
	int r2 = Find(Root2);							//找到root2集合的根
	if (r1 != r2) {									//兩個集合不屬于同一樹
		int temp = parent[r1] + parent[r2];			//計算總節點數
		if (parent[r2] < parent[r1]) {				//注意比較的是負數,越小元素越多,此處是r2元素多
			parent[r1] = r2;						//r1作為r2的孩子 
			parent[r2] = temp;						//更新r2的節點個數
		}
		else {
			parent[r2] = r1;						//...
			parent[r1] = temp;						//...
		}
	}
}



           

代碼的注釋比較詳盡,我就不在贅言。但是有一個注意點我已經寫在了下面!

目前并查集的改進!

的确,有一個極端的狀況使得上面的樹實作的并查集性能低下!問題原因在于,這裡沒有規定子集合并的順序,更确切的說是子集一直在向同一個方向依附:

下面的圖檔展示了當Union(0,1),Union(1,2),Union(2,3),Union(3,4)執行完後的樹的形狀。

并查集的了解與實作總結

在這種極端情況下他編變成了一個單連結清單(退化的樹),這樣的話,用Find函數查找完所有的節點所歸屬的集合将會開銷的時間複雜度為:O(n^2)。

怎樣來改變這種狀況,就是在合并兩個子集的時候,規定順序,代碼中是讓元素多的始終作為父節點,這樣就避免了這種麻煩。

另外還可以用性能更加的查找算法,例如折疊規則壓縮路徑。并查集的一個重要的應用是在圖論中生成最小生成樹的Kruskal算法,充分展現了并查集的優越性和思想,之後會寫相關的博文總結此算法!

參考書目:《資料結構 C++語言描述 第二版》 殷人昆著

并查集的了解與實作總結