数据结构-树(集合)

发布于:2022-12-21 ⋅ 阅读:(343) ⋅ 点赞:(0)

并查集

1.并查集问题中集合存储如何实现?

可以用树结构表示集合,每个节点代表一个集合元素

采用数组形式存储 

负数表示根结点;非负数表示双亲结点的下标。

 数组中每个元素的类型描述为:

typedef struct {
    ElementType Data;
    int Parent;//相当于指针,表示这个节点的父节点在集合中的位置
} SetType;

 2 .集合运算

 (1)查找某个元素所在的集合(用根结点表示)

int Find(SetType S[], ElementType X)
{   //在数组S中查找值为X的元素所属的集合
    //Maxsize是全局变量,为数组S的最大长度
    int i;
    for (i=0 ; i < Maxsize && S[i].Data != X; i++);  //当S[i].Data = X时,循环结束,i为X的位置下标
    if(i >= Maxsize)  return -1;  //未找到X,返回-1 则i为根节点的下标
    for (; S[i].Parent >= 0; i = S[i].Parent);  //如果i < Maxsize,寻找父结点,直到S[i].Parent < 0为止(根结点的Parent为-1)
    return i;  //找到X所属集合,返回树根结点在数组S中的下标
}

(2)集合的并运算
①分别找到X1和X2两个元素所在集合树的根结点;
② 如果它们不同根,则将其中一个根结点的父结点指针设置成另一个根结点的数组下标。

void Union(SetType S[ ], ElementType X1, ElementType X2)
{
    int Root1, Root2;
    Root1 = Find(S, X1);
    Root2 = ind(S, X2);
    if (Root1 != Root2)  //当x1和x2不属于同一子集时,才需要合并
       S[Root2].Parent = Root1;  
}

 为了改善合并以后的查找性能,可以采用小的集合合并到相对大的集合中。(修改Union函数)

void Union(SetType S, SetName Root1, SetName Root2)
{   //这里默认Root1和Root2是不同集合的根结点
    //保证小集合并入大集合
    if ( S[Root2] < S[Root1] ) {  //如果集合2比较大
        S[Root2] += S[Root1];     //集合1并入集合2
        S[Root1] = Root2;
    }
    else {                         //如果集合1比较大
        S[Root1] += S[Root2];     //集合2并入集合1
        S[Root2] = Root1;
    }
}

3. 并查集的实现

数组存储形式实现并查集的代码如下所示。

#include<iostream>
using namespace std;
#define MaxSize 1000
typedef int ElementType;
typedef struct {
	ElementType Data; // 存值
	int Parent;  // 指向父结点 
}SetType;

// 查找 
int Find(SetType S[], ElementType X) 
{
	int i;
	for (i = 0; i < MaxSize && S[i].Data != X; i++);  // 找到数组中该值对应的下标 
	if (MaxSize <= i) // 如果没有找到,返回 -1 
		return -1;
	for (; S[i].Parent >= 0; i = S[i].Parent); 	// 找到该结点的根结点 
	return i; // 返回根结点在数组S中的下标 
}

// 并 
void Union(SetType S[], ElementType X1, ElementType X2) 
{
	int root1 = Find(S, X1);  // 找到 X1 的根结点下标 
	int root2 = Find(S, X2);  // 找到 X2 的根结点下标 
	// 如果根结点的下标不同,说明不是一个集合
	if (root1 != root2) 
	{
		S[root1].Parent = root2;   // 把X1挂到X2的集合 
	}
}

int main() 
{
	SetType S[MaxSize];
	// 初始化数组,父结点全部指向-1 
	for (int i = 0; i < MaxSize; i++) 
	{
		S[i].Data = i + 1;
		S[i].Parent = -1;
	}
	cout << Find(S, 5) << endl;
	Union(S, 3, 5);
	cout << Find(S, 4) << endl;
	cout << Find(S, 3) << endl;
	Union(S, 1, 3);
	Union(S, 2, 4);
	Union(S, 8, 6);
	cout << Find(S, 6) << endl;
	cout << Find(S, 8) << endl;
	system("pause");
	return 0;
}

代码运行结果如下图所示。

4.集合的优化

为什么要优化?

问题中每查找一次都要遍历n个节点,若想把所有元素都遍历一遍则时间复杂度为O(n^2)。效率低,故需要进行优化。

 优化方案

任何有限集合的N个元素都能映射为0~N-1.故可以大小为N的数组的下标来表示集合元素的位置,数组中存放的值则为其父节点的位置。

 

 代码实现

1.并查集的表示形式

#define MaxSize 100
typedef int ElementType;
typedef int SetName;
typedef ElementType SetType[MaxSize];

2.查找和合并操作

SetName Find(SetType S, ElementType X) {//查找某个元素的根节点
	for (; S[X] >= 0; X = S[X]) {
		//默认集合元素全部初始化为-1
		return X;
	}
}
void Union(SetType S, SetName Root1, SetName Root2) {
	//这里root1和root2是不同节点的根节点
	S[Root2] = S[Root1];
}

例题

输入连接的计算机的数量n,分别编号为1,2,3···n,然后输入要进行的操作。

C表示Check两台计算机是否链接,若链接则输出“Yes”否则输出“No”

I表示Inter即连接两台计算机。

S表示操作结束,退出。

最后判断有几个连接网。(即有计算机相互连通的net有几个,若某个计算机没有和别的计算机联通,则按照它与自己连通来看,也算作一个net)

输入样例:                                                                              

                

输出样例:

 

 程序框架搭建:

int main() {
	SetType S;
	int n;
	char in;
	scanf("%d", &n);
	Initialization(S, &n);//建立一个容量为n的集合
	do {
		scanf("%c", &in);
		switch (in) {
		case 'I':Input_connection(S); break;//建立连接
		case 'C':Check_connection(S); break;//检查是否连接
		case 'S':Check_network(S, n); break;//检查有几个net
		}
	} while (in != 'S');
	return 0;
}

 方法实现

SetName Find(SetType S, ElementType X) {//查找某个元素的根节点
	for (; S[X] >= 0; X = S[X]) {
		//默认集合元素全部初始化为-1
		return X;
	}
}


void Union(SetType S, SetName Root1, SetName Root2) {
	//这里root1和root2是不同节点的根节点
	S[Root2] = S[Root1];
}


void Input_connection(SetType S) {
	ElementType u, v;
	SetName Root1, Root2;
	scanf("%d %d\n", &u, &v);
	Root1 = Find(S, u - 1);
	Root2 = Find(S, v - 1);
	if (Root1 != Root2) {
		Union(S, Root1, Root2);
	}
}


void Check_connection(SetType S) {
	ElementType u, v;
	SetName Root1, Root2;
	scanf("%d %d\n", &u, &v);
	Root1 = Find(S, v - 1);//计算机的编号为1,2....n映射到集合上为0,1....n-1.故传入的数据应为u-1
	Root2 = Find(S, v - 10);
	if (Root1 == Root2) {
		printf("Yes\n");
	}
	else {
		printf("No\n");
	}
}


void Check_network(SetType S, int n) {
	int i, counter = 0;
	for (i = 0; i < n; i++) {
		if (S[i] < 0)counter++;
	}
	if (counter == 1) {
		printf("The network is connected.\n");
	}
	else {
		printf("There are %d components.\n", counter);
	}
}

在PTA上的运行结果:

运行超时原因分析:

 

把高的树插到矮的树上会导致树的高度不断变高,随着集合的不断合并,树高不断增大,因而查找的次数也不断增多,导致超时。

 解决方案:按秩归并

方法1:

void Union(SetType S, SetName Root1, SetName Root2) {
	if (S[Root2] < S[Root1]) //S[Root]=-树高
		S[Root1] = Root2;
	else {
		if (S[Root1] == S[Root2]) S[Root1]--;//树高加1
		S[Root2] = Root1;
	}
}

方法2:

void Union(SetType S, SetName Root1, SetName Root2) {
	if (S[Root2] < S[Root1]) {
		S[Root2] += S[Root1];
		S[Root1] = Root2;
	}
	else {
		S[Root1] += S[Root2];
		S[Root2] = Root1;
	}
}

路径压缩

图示:

 

SetName Find(SetType S,ElementType X){
    if(S[X]<0)
        return X;
     else
        return S[X] = Find(S,S[X]);
}


网站公告

今日签到

点亮在社区的每一天
去签到