并查集(union-find sets)
并查集所基于的数据结构是树形的数据结构,
整个树被抽象成为集合(Set),
而 树形结构中,节点被抽象为Elem,
树的根作为树中节点的祖先,
在树中,节点除了根节点以外都有父节点。
但是祖先节点只有一个。
即在集合Set中所有的元素Elem都有父节点,
但是它们的祖先节点有且只有一个:可以根据这一条件来判断两个元素
是否位于同一个集合中,即判断它们的祖先节点是否是相同的即可。
如果集合中只有一个节点那么,该集合的祖先节点为它本身。
基于该树形数据结构的主要三种原子操作有:
1.Make_Set(Elem x)
在刚开始接收元素的时候,每个元素之间都是相互孤立的集合。
这个操作用于把每一个元素初始化为一个仅包含一个元素的集合。
在初始化后每一个元素的父节点是它本身。
每个元素的祖先节点也是它本身。这里需要注意的是,父节点不一定是
祖先节点的。
也就是说
i-(root)>j->k
k的父节点是j,k的祖先节点是i
j的父节点是i,j的祖先节点仍然是i
2.Find_Set(Elem x)这个操作时查找一个元素所在的集合。
这个原子操作的本质思想就是找到元素x的祖先节点,
祖先节点对于一个集合来所是唯一的,
就像是对于一个树来所它的根可以
唯一确定一个树一样。
所以x元素的祖先节点所确定的集合一定是x所在的集合。
3.Union(Elem x, Elem y) 这个操作使用来合并元素x 和元素 y所在的集合的。
合并两个集合的思想是,
各自调用Find_Set(x) 和Find_Set(y)这两个操作,
找到两个元素所在集合的祖先,
然后根据树形结构的树的高度作为判断条件来
使得一个集合的祖先作为另一个集合的祖先。
这样就实现了合并两个元素所在的集合这一个操作。
即(x所在集合的祖先节点|变为子节点)->(y所在集合的祖先节点|仍旧是父节点&祖先节点)
或是(y所在集合的祖先节点|变为子节点)->(x所在集合的祖先节点|仍旧是父节点&祖先节点)
并查集优化操作
1.递归查找祖先可以使得代码简洁易懂,但是如果集合对应的树形结构比较特殊的时候(例如每层仅有一个节点)
会消耗很多资源,效率也会下降,会使得最坏情况的时间空间复杂度大大加大。
所以可以在递归寻找祖先的时候,把最后一个节点的父节点设为父节点的父节点。
以此类推下去,会将树中所有节点都指向祖先节点,
这时候的树高为1(不算根节点的情况下):即所有的节点的父节点都是祖先节点了。
这就是所谓的路径压缩方法。
如果说并查集就不得不说并查集中的秩,
秩往往用来表示子树的深度或者是同一个集合里面的元素的个数。
2.而在合并集合的时候,通常是将元素少的集合合并到元素多的集合中,
这样合并后树的高度会相对较小。
下面是对于并查集比较经典的代码实现,
注释是LZ根据自己的理解增添上去的。
1 int father[MAX]; 2 //这个数组是用来存放某一个元素的父节点元素值的 3 //即,若设节点i的父节点是节点j的话,则由 father[i]=j; 4 5 int rank[MAX]; 6 7 //这个数组是用来记录秩的,在本实现并查集的代码中 8 //秩是用于记录子集中元素个数的 9 //即 若元素节点i有k个子节点,那么就有 rank[i] = k; 10 11 12 //这个是初始化集合操作,在刚开始录入数据的时候, 13 //每个元素节点都是一个独立的集合,它们有0个子节点(因为自己无法是自身的子节点, 14 //但是自己却可以是自己的祖先节点 和 可以是自身的父亲节点) 15 16 //但是具体的实现方法和初始化设定还需要根据具体情况进行推敲 17 18 void Make_Set(int x) 19 { 20 21 father[x] = x;//将自己设定为自己的父节点(祖先) 22 rank[x] = 0; //集合中x的子集个数为0,故将其赋值为0 23 } 24 25 26 27 28 //下面的代码在实现递归查找x元素所在集合的同时, 29 //又对代码进行优化处理,在回溯的时候,又对路径进行了相应的压缩 30 //其实就是,为了降低树的高度,将集合中的的x节点的父节点设成该集合的祖先节点(树的根) 31 //不仅仅如此,x父节点的父节点....一直到祖先节点,它们的父节点都是集合的祖先节点 32 //(其中祖先节点的父节点就是本身,所以 father[祖先]=祖先,循环会因为这个条件而停下来) 33 34 int Find_Set(int x) 35 { 36 if(x != father[x]) //如果x的父节点不是它本身的话,x所在的集合中就一定存在其余元素节点 37 { 38 father[x] = Find_Set(father[x]); 39 //继续寻找x父节点的父节点,一直会找到祖先节点 40 //一次类推的话,最后从x到x父节点...祖先节点,的父节点都会是祖先节点 41 } 42 //循环会判断到father[x]=x这个地方停止,此时的x就是自身的父节点,即x就是祖先节点 43 44 return father[x]; 45 //递归到x=father[x]停止,所以由此可知return 的father[x]即是 46 //在一开始传入操作的形参x所在集合的祖先节点 47 } 48 49 50 51 52 3.下面的这个操作是要合并传入形参x和y所在的集合 53 54 void Union(int x , int y) 55 { 56 x = Find_Set(x); //首先找到x所在集合的祖先节点 57 y = Find_Set(y); //然后找到y所在集合的祖先节点 58 59 60 61 if(x==y) return; 62 //如果x、y值相等的话,说明 一开始传入形参的x y元素拥有同一个祖先,即x,y在相同的集合中 63 64 65 66 //如果不相同的话,说明x、y在不同的集合中,可以合并 67 //根据Union的描述,首先比较x和y(ps:此时的x、y分别对应的是传入参数的相应的祖先节点 68 //所以rank[x]的值是 传入参数1 所在集合的元素总数 69 // rank[y]的值对应的是 传入参数2 所在集合的元素总数) 70 //将元素少的集合的祖先节点作为 元素多的集合的祖先节点的 子节点 71 //即,if对应的是将y所在的集合的祖先节点作为 x集合祖先节点的子节点 72 73 if(rank[x] > rank[y]) 74 { 75 father [y] = x; 76 } 77 78 79 80 81 //其余的情况,小于或等于都是y所在集合包含x所在的集合 82 83 else 84 { 85 if(rank[x] == rank[y]) 86 { 87 rank[y]++; 88 } 89 90 father[x]=y; 91 92 } 93 }