跟着我的推导慢慢来,首先并查集主要有3个主要动作:
-
1.初始化
假如有编号为1,2,3…n个元素,我们用一个数组fa[]来存储每个元素的父节点(注意是父节点),一开始,我们先将每个元素的父节点设为自己。
-
2.查询:找到元素i的祖先元素直接返回,未进行路径压缩
int find(int i) { // 为什么呢?因为祖先元素一般指向自己(即祖先元素的父节点为自己) if (fa[i] = i) return i; // 递归出口,当到达了祖先元素,就返回祖先。 else return find(fa[i]) // 不断往上查找祖先 }
此处比较重要的是要知道为什么要判断fa[i] = i,因为我们在找寻一个元素的祖先元素,重点就是祖先元素指向自己(即祖先元素的父元素为自己)。因此,需要这样判断。
-
3.合并
void union(int i, int j) { int i_fa = find(i); // 找到i的祖先元素 int j_fa = find(j); // 找到j的祖先元素 fa[i_fa] = j_fa; // i的祖先指向j的祖先 }
比如有元素:1,2,3,4,5
此时有三组合并操作(4,3),(3,2),(2,1)
此时就需union(4,3)
一开始,4的父节点(此时也是祖先节点)是4,即fa[4],3的父节点(此时也是祖先节点)是fa[3],则将fa[4] = 3,则此时4的父节点为3。
接着union(3,2)
此时3的祖先为3,即fa[3] ,此时2也同理,为fa[2],则将fa[3] = 2,则此时3的父节点为2
union(2,1)
…
则4 -> 3 -> 2 -> 1->1
如果此时(4,5)
重点之一:那么此时先找4的祖先元素,进入find(4),fa(4)为3,那么一开始3 == 4不成立(为什么成立就返回这个 i 呢,因为祖先元素指向自己,同理为什么要判断父节点是不是等于本身也是因为如此)。
-
则找4的父节点(3)的父节点是不是祖先节点,而3的父节点是2,不相等。
-
因此继续找2的父节点是不是祖先元素,2的父节点是1,不相等。
-
则继续找1的父节点是不是祖先元素,发现1的父元素为1,等于自身,因此此时,1就是4的祖先元素。
不过此时,让我们想一想,数据多了的时候,找一个元素的祖先元素岂不是得一直递归?会非常的慢。
因此,我们能不能再找4的祖先元素的时候,就把其查找路径上的3、2、1的祖先元素都找到,这种方式称为压缩路径。
int find(int i) { if (i == fa[i]) return i; else { fa[i] = find(fa[i]); //该步进行了路径压缩 return fa[i]; } }
这时,比如之前union(4,5),在find(4)的时候:
-
一开始判断 4 不是祖先元素,继续查找。
-
然后find(fa[i]),此时为find(fa[4]),即find(3),找3的父元素是不是祖先元素,并且把find(3)的结果(3的祖先元素,因为3是4的父元素,所以此时该结果也是4的祖先元素)赋值给fa[4],即将 4 直接 指向其祖先元素(当然此时我们假设已经执行完了find(3)中的递归,这个递归过程下面再分析)。
-
然后我们再看仔细分析find(3)返回结果的过程中,在find(3)的过程中,3的父元素是2,2不等于3,因此3不是祖先元素,因此继续找find(2),在find(2)的过程中,2的父元素是1,1不等于2,因此继续find(1)然后此时找到了1就是祖先元素,return 1,这时候,要开始回溯了。
- 那么之前find(2)的结果就是1,然后让2指向1(通过fa[i] = find(fa[i]) 这行代码,1 = 2,不过此处2本来就直接指向1),返回fa[i],此处为fa[2]也就是1,即返回1给find(3),那么再把3指向1,然后再同理,返回1给find(4),则此时再把4指向1,find(4)返回1,结束。
那么,在这个过程中,我们不仅找到了4的祖先元素,并且还把4以及其找祖先元素的路径上的元素全部指向1(因为一个元素及该元素到达其祖先元素的路径上所经过的所有元素的祖先元素是必定一样的,即一条路径上(这条路径能找到某个元素的祖先元素)的元素的祖先元素都是相同的,所以这个操作是没问题的)。
如下图所示:
-