数据结构---并查集实现
数据结构—并查集实现
一. 内容介绍
本次介绍的并查集是一种树的应用,用以处理一些没有重复元素的集合之间的合并和查询问题,即查询两个数据是否位于一个集合或者将两个数据合并到一个集合中的数据结构。并查集的实现主要有两种分别是快速查找(查询:O(1) 合并:O(n))和快速合并(查询:O(logn) 合并:O(logn))两类。本次会将两种方式都实现一遍,并且会介绍一些优化技巧提高快速合并型的查询速度。
二. 并查集原理
关于并查集,我们主要有如下操作:
- 建立一个并查集,其中包含n个单元素集合
- 合并集合:两个集合合并,要求二者没有相同的元素
- 找到某元素所在集合(通过得到该元素所在集合的标志,比如该集合的id),同时也可通过该操作判断两个元素是否在一个集合(匹配二者的集合标志是否相同)。
关于前文介绍并查集是一种树的应用,原因是并查集中的集合是通过树表示的,树的根表示该集合的标志,它的节点代表每个集合中的元素,例如下图:

左图代表一个以a元素为标志的集合,该集合元素有a,b,c,d。右图为一个以e为标志的集合包含e,f,g。合并集合就是让其中一个集合的标志元素指向另一个集合的一个元素。查询集合就是不断遍历该节点的父节点直到找到根节点(根节点的父节点默认为其本身)。
优化方式:
- 在每个集合的标志元素处存储一个变量来记录该集合的元素数量,然后在合并两个集合的时候判断哪个集合的元素数量多,将其中元素量少的集合的根节点直接指向元素量多的集合的根节点。
- 路径压缩:在每次查找时,令该集合路径上的每一个节点都直接指向根节点,不需要层层递归寻找根节点,大大节省查询所需时间。

三. 代码实现
A.快速查询型
1. 初始化定义和创建/释放并查集
typedef int Element;typedef struct {Element *data; // 存放具体数据,利用索引来建立数据元素的关系int *groupID; // 每个元素的组编号,利用索引值找到组ID信息int n; // 并查集中元素的个数
} QuickFindSet;QuickFindSet* createQuickFindSet(int n)//创建包含n个元素的集合
{QuickFindSet* setQF = malloc(sizeof(QuickFindSet));//申请空间if (setQF == NULL)//判断是否申请成功{printf("setQF malloc failed!\n");return NULL;}setQF->data = malloc(sizeof(Element)*n);//初始化预留n个元素的空间setQF->groupID = malloc(sizeof(int)*n);//初始化预留n个元素其标志的空间setQF->n = n;return setQF;
}void releaseQuickFindSet(QuickFindSet* setQF)
{if (setQF){if (setQF->data)free(setQF->data);if (setQF->groupID)free(setQF->groupID);free(setQF);}
}void initQuickFindSet(const QuickFindSet* setQF, const Element* data, int n)//数据初始化
{for (int i = 0;i < n;i++){setQF->data[i] = data[i];setQF->groupID[i] = i;}
}
2. 查找元素标志和判断两元素是否为同一集合
static int findIndex(const QuickFindSet *setQF, Element e)
{for (int i = 0;i < setQF->n;i++){if (setQF->data[i] == e)return i;//从0开始遍历查找setQF是否有该元素}return -1;//未找到
}
3. 判断两元素是否为同一集合
int isSameQF(QuickFindSet* setQF, Element a, Element b)
{int aIndex = findIndex(setQF,a);int bIndex = findIndex(setQF,b);if (aIndex == -1 || bIndex == -1)return 0;//setQF中不存在a或breturn setQF->groupID[aIndex] == setQF->groupID[bIndex];//将判断结果返回
}
4. 合并集合
void unionQF(QuickFindSet* setQF, Element a, Element b)
{int aIndex = findIndex(setQF,a);int bIndex = findIndex(setQF,b);int gID = setQF->groupID[bIndex];for (int i = 0; i < setQF->n; ++i){if (setQF->groupID[i] == gID)setQF->groupID[i] = setQF->groupID[aIndex];}
}
B.快速合并型
1. 初始化定义和创建/释放并查集
typedef int Element;typedef struct {Element *data;int *parent;int *size;int n;
} QuickUnionSet;/* 由于是链式栈,所以需要定义节点 */
typedef struct _set_node {int index;struct _set_node *next;
} SetNode;QuickUnionSet* createQuickUnionSet(int n) //同快查型并查集创建操作
{QuickUnionSet* setQU = malloc(sizeof(QuickUnionSet));if (setQU == NULL) {return NULL;}setQU->data = malloc(sizeof(Element) * n);setQU->parent = malloc(sizeof(int) * n);setQU->size = malloc(sizeof(int) * n);setQU->n = n;return setQU;
}void releaseQuickUnionSet(QuickUnionSet* setQU) //同快查型并查集释放操作
{if (setQU) {if (setQU->data)free(setQU->data);if (setQU->parent)free(setQU->parent);if (setQU->size)free(setQU->size);}
}void initQuickUnionSet(QuickUnionSet* setQU, const Element* data, int n)
{for (int i = 0; i < n; ++i) {setQU->data[i] = data[i];setQU->parent[i] = i;setQU->size[i] = 1;}
}
2. 查找元素标志
static int findIndex(const QuickUnionSet* setQU, Element e)
{for (int i = 0; i < setQU->n; ++i) {if (setQU->data[i] == e) {return i;}}return -1;
}
3. 查找标志
static int findRootIndex(const QuickUnionSet* setQU, Element e)
{int curIndex = findIndex(setQU,e);//得到e的索引if (curIndex == -1){return -1;}while (setQU->parent[curIndex] != curIndex)//循环遍历得到e的标识索引·1{curIndex = setQU->parent[curIndex];}return curIndex;
}
4. 判断是否为同一集合
int isSameQU(const QuickUnionSet* setQU, Element a, Element b)
{int aRoot = findRootIndex(setQU,a);int bRoot = findRootIndex(setQU,b);if (aRoot == -1 || bRoot == -1){return 0;}return aRoot == bRoot;
}
5. 合并集合
/* 将元素a和元素b进行合并* 1. 找到a和b的根节点,原本是b的父节点指向a的父节点* 2. 引入根节点的size有效规则,谁的元素多,让另外一个接入元素多的组*/void unionQU(QuickUnionSet* setQU, Element a, Element b)
{int aRoot = findRootIndex(setQU,a);//得到a和b的标志索引int bRoot = findRootIndex(setQU,b);if (aRoot == -1 || bRoot == -1){return;}if (aRoot != bRoot)//确定a与b不在同一集合{int aSize = setQU->size[aRoot];//确定a与b的集合元素数量int bSize = setQU->size[bRoot];if (aSize >= bSize){setQU->parent[bRoot] = aRoot;//b的根节点换成asetQU->size[aRoot] += bSize;//a的元素数量增加}else{setQU->parent[aRoot] = bRoot;//a的根节点换成bsetQU->size[bRoot] += aSize;//b的元素数量增加}}
}
6. 路径压缩*
//通过链式栈来存每一个查找过程中遇到的节点索引,再一一修改它们的父节点直接指向根
static SetNode *push(SetNode *stack, int index)//压栈实现
{SetNode* node = malloc(sizeof(SetNode));//申请一个栈节点空间node->index = index;//存下index索引node->next = stack;//把stack往下压,由node作为新栈顶return node;//将新栈顶返回
}static SetNode *pop(SetNode *stack, int *index)//出栈实现
{SetNode* temp = stack;//存下准备出栈的数据*index = stack->index;//将stack的索引返回给indexstack = stack->next;//出栈free(temp);//释放空间return stack;//将新栈顶返回
}static int findRootIndex(const QuickUnionSet* setQU, Element e)
{int curIndex = findIndex(setQU,e);//得到e的当前索引if (curIndex == -1){return -1;}SetNode* path = NULL;//定义一个新栈,初始为空while (setQU->parent[curIndex] != curIndex)//循环条件:curIndex所指元素不为根节点(根节点的父节点为自己){path = push(path,curIndex);//将途径节点的索引值都进栈curIndex = setQU->parent[curIndex];//curIndex向上遍历更新}while (path)//栈不为空{int pos;//定义pos接收返回的栈中节点的索引值path = pop(path,&pos);//完成出栈并得到possetQU->parent[pos] = curIndex;//将pos所指元素的父节点直接改为根节点}return curIndex;
}
四. 内容总结
本次内容主要就是通过前面学到的树来完成一种常见的数据操作应用,也是一种比较常见的数据结构,介绍的两种实现里第一种相较来说简单些,第二种更泛用,但是整体难度都不大。如果大家碰到什么问题可以在评论区提出来。
