内容简介:先来想想「亲戚」这个词的定义:「指和自己有血亲和姻亲的人」。你和你女友家属本身并非是亲戚关系,一旦结婚后,两家人便成为了一家人,你的家人包括你在内和你女友及其家人自动成为了亲戚,这就是一个典型的并查集应用。并查集是一种树形的数据结构,用于处理一些不相交集合的合并及查询,上面例子中「结婚」其实就是并查集的合并操作下面我们来演示下并查集的常规操作,我们默认创建6个元素,这6个元素我们可以看成是互不相交的6个集合
先来想想「亲戚」这个词的定义:「指和自己有血亲和姻亲的人」。你和你女友家属本身并非是亲戚关系,一旦结婚后,两家人便成为了一家人,你的家人包括你在内和你女友及其家人自动成为了亲戚,这就是一个典型的并查集应用。并查集是一种树形的数据结构,用于处理一些不相交集合的合并及查询,上面例子中「结婚」其实就是并查集的合并操作
下面我们来演示下并查集的常规操作,我们默认创建6个元素,这6个元素我们可以看成是互不相交的6个集合
进行几次简单合并操作,我们把元素0,2,4合并为集合set0,1,3合并为set1,5单独看成一个set2
查询操作
实现方法
初始化(make_set)
我们可以把并查集看成是由很多颗树组成的森林,每棵树中相连的结点都代表属于同一集合,树中parent指向自己的根结点被视为该集合的代表。初始的时候,我们用一个parent数组存储所有结点的父结点下标,由于默认情况下每个集合互不相交,所以我们令每个结点的parent都指向自己,这样就生成了N棵以自己为根的树组成的森林。
parent数组的初始化结构如下图所示:
初始化并查集的代码:
void make_set (){ for (int i=0;i<N;i++){ parent[i] = i;// 如上图i的parent指向自己 } } 复制代码
合并(Union)
Union(a,b)会将a所在的集合与b所在的集合相结合。在数据结构的实现上,只需要将b的根结点指向a的根结点,或a的根结点指向b的根结点即可,本文中默认使用前者。假设我们现在要将0,2,4合并为一个集合,1,3 合并为一个集合,5单独视为一个集合,那么运算的过程的可能如下:
接着,如果想要继续Union(5,3),我们可以先获得结点3所处树的根结点1,让1指向5即可。但是这样树的高度要比5指向1的树要高,随着并查集规模的增大,树会多出很多不必要的高度,这将导致并查集的查询更耗时。
为了让合并后树的整体高度相对更矮,在每次合并时,我们让高度较矮的树并入高度较高的树,这种优化会在之后的代码中体现出来。
最后,如果我们想要Union(2,3),由于2,3各自所处的树高度相同,所以按默认方式将「3」的根结点「1」指向「2」的根结点「0」即可。
在实现Union函数之前,我们先增加一个rank[N]数组记录高度,默认的时候rank数组全部设置为0,rank中数值随着并查集的合并而改变。下面给出Union的代码:
void union_set(int a,int b) { if (a==b) return; // 相同 int root_a = find_root(a);//找到a的根结点 int root_b = find_root(b);//找到b的根结点 if (root_a == root_b) return; //根结点相同 int higher_root = rank[root_a]>rank[root_b] ?root_a:root_b;// 选出较高的树 int lower_root = rank[root_a]<rank[root_b]?root_a:root_b;// 选出较低的树 if( higher_root == lower_root ) { // 两颗树高度相等的情况 parent[root_b] = root_a; //root_b.parent 指向 root_a (默认操作) ++rank[root_a];// 高度+1 }else { parent[lower_root] = higher_root; // 较矮的树指向较高的树,不会改变整体高度 } } 复制代码
查询(Find)
查询某个元素所在的集合非常简单,由于parent数组记录了每一个元素的父结点,我们只需要递归回溯即可。
执行 find_root(5)
后沿着红线向上回溯找到0,执行 find_root(2)
后沿着红线向上回溯也找到了0,说明5和2同属一个集合,而执行 find_root(7)
后沿着红线回溯找到了6,故7和元素5,2不属于同一个集合。下面给出实现代码:
int find_root(int node) { if (parent[node] == node) return node; return find_root(parent[node]); } 复制代码
路径压缩(Path Compression)
在查询某个元素的所在集合的时候,上面的 find_root(int node)
函数会返回元素所在的树的根结点------这个集合的代表,在这个过程中,我们可以将当前待查找的元素直接指向这个根结点,降低树的高度,从而使得查询速度得到提升。以上图为例子,执行 find_root(3)
, find_root(5)
后树形结构会变成如下结构:
代码实现上的改动非常小:
int find_root(int node) { if (parent[node] == node) return node; parent[node] = find_root(parent[node]); // 指向根结点 return parent[node]; } 复制代码
完整的代码+前面的例子
并查集的代码和逻辑都非常精简,在我看来是非常优雅的数据结构。
#include<iostream> #include <stdio.h> #define N 10000+10 int parent[N]; int rank[N]; void make_set (){ for (int i=0;i<N;i++){ parent[i] = i; rank[i] = 0; } } int find_root(int node) { if (parent[node] == node) return node; parent[node] = find_root(parent[node]); return parent[node]; } void union_set(int a,int b) { if (a==b) return; // 相同 int root_a = find_root(a);//找到a的根结点 int root_b = find_root(b);//找到b的根结点 if (root_a == root_b) return; //根结点相同 int higher_root = rank[root_a]>rank[root_b] ?root_a:root_b;// 选出较高的树 int lower_root = rank[root_a]<rank[root_b]?root_a:root_b;// 选出较低的树 if( higher_root == lower_root ) { // 两颗树高度相等的情况 parent[root_b] = root_a; //root_b.parent 指向 root_a (默认操作) ++rank[root_a];// 高度+1 }else { parent[lower_root] = higher_root; // 较矮的树指向较高的树,不会改变整体高度 } } int main(){ make_set(); union_set(0, 2); union_set(2, 4); union_set(1, 3); union_set(5, 3); union_set(2, 3); find_root(3); find_root(5); for(int i=0;i<6;i++) { printf("%d ",parent[i]);// 输出所有指向 } } 复制代码
以上所述就是小编给大家介绍的《优雅的数据结构–并查集》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 数据结构 – 用于构建文件系统的数据结构?
- 荐 用Python解决数据结构与算法问题(三):线性数据结构之栈
- 数据结构和算法面试题系列-C指针、数组和结构体
- 请问二叉树等数据结构的物理存储结构是怎样的?
- 数据结构——单链表
- 常用数据结构
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Grails权威指南
瑞切 / 张若飞 / 电子工业 / 2007-11 / 49.80元
《Grails权威指南》译自由Grails项目负责人Graeme Keith Rocher编写的《The Definitive Guide to Grails》,着重介绍了如何在Grails框架下使用Groovy语言进行敏捷的Web开发。本书详细讲解Grails开发的全部过程,包括项目构架、控制器与视图、与关系数据库之间的ORM映射,以及与Ajax和Java平台的无缝集成。同时该书也揭示了Grai......一起来看看 《Grails权威指南》 这本书的介绍吧!