并查集是一种特别的数据结构,在解决连通性问题屡试不爽。如下代码均为java语言的实现java
并查集的做用先整体说一下算法
public void join(int a, int b)
连通a节点和b节点public boolean isJoined(int a, int b)
判断任意两个节点是不是连通的来查询任意节点的根节点,自己你能够理解并查集是一颗树,但这颗是反向寻找,并非像一般的树,自顶向下dfs的,而是一直向上找寻根节点数据结构
private int findP(int x)
查询x元素的根节点将连通节点的父节点都设置为相同节点(索引),查询的时候 f(x)=parent[x],但更新比较麻烦,每次更新须要遍历全部元素O(n)的复杂度,n是总的元素个数性能
/** * 并查集 快速查询 */ public class UFQuickFind { int[] parent; public UFQuickFind(int size) { parent = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; } } private int findP(int x) {//查询操做,实际上是查询 根节点 if (x < 0 || x >= parent.length) return -1; //或者直接抛出异常 return parent[x]; //直接返回 parent就能够了,由于全部 链接 的节点parent值都相同 } public void join(int a, int b) { int ap = findP(a); int bp = findP(b); if (ap != bp) { for (int i = 0; i < parent.length; i++) { if (parent[i] == ap) parent[i] = bp; //修改为相同的 parent } } } public boolean isJoined(int a, int b) { //两个节点是不是 链接的 return findP(a) == findP(b); } }
合并的时候,a,b元素,直接让将a的父亲指向b的父亲。【通俗一点 a节点的父亲 认b节点的根节点作父亲了。a节点认祖归宗了】ui
/** * 快速合并 */ public class UFQuickUnion { int[] parent; public UFQuickUnion(int size) { parent = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; } } private int findP(int x) {//查询操做,实际上是查询 根节点 if (x < 0 || x >= parent.length) return -1; //或者直接抛出异常 while (parent[x] != x)//一直搜索到根节点 x = parent[x]; return x; } public void join(int a, int b) { int ap = findP(a); int bp = findP(b); if (ap != bp) { parent[ap] = bp;//让其中一个节点的根节点 指向 另一个节点的根节点 } } public boolean isJoined(int a, int b) { //两个节点是不是 链接的 return findP(a) == findP(b); } }
因为快速合并的过程,合并的过程是随机的,若是全部节点都合并到一块儿,那么最后这颗树可能变成一个链表【就是接龙,成为一条线】,经过节点数来改进,节点数少的接到节点数多的上面,这样确定不会成一个链表code
public class UFNumsUnion { int[] parent; int[] nums;//节点数 public UFNumsUnion(int size) { parent = new int[size]; nums = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; nums[i] = 1; } } private int findP(int x) {//查询操做,实际上是查询 根节点 if (x < 0 || x >= parent.length) return -1; //或者直接抛出异常 while (parent[x] != x)//一直搜索到根节点 x = parent[x]; return x; } public void join(int a, int b) { int ap = findP(a); int bp = findP(b); if (ap != bp) { //a节点多,就将 b节点往a节点上合并,这样的话能够减小树的高度 if (nums[ap] > nums[bp]) { parent[bp] = ap; nums[ap] += nums[bp]; //根节点的节点数 增长了 被合并节点的节点数 } else { parent[ap] = bp; nums[bp] += nums[ap]; } } } public boolean isJoined(int a, int b) { //两个节点是不是 链接的 return findP(a) == findP(b); } }
【基于每株节点数的合并】虽然已经挺好的了,但偶尔也会不太合理的状况以下所示索引
A: o / | \ \ o o o o 5个节点,两层 B: o / | o o 4个节点 3层 / o
上面这种状况按照基于节点数的合并,会获得一个4层的树(层的深度增长),而更合理的方式是让A接到B上,这样最后总体层数保持在3层。层数越小,那么findP
方法就会变快,而合并也由于调用findP
方法也会加快。get
基于层数的实现,永远让层数矮的接到层数高的树上io
public class UFRankUnion { int[] parent; int[] rank;//树的层数 int plant; //一共有多少株树 public UFRankUnion(int size) { plant = size; parent = new int[size]; rank = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; rank[i] = 1; } } private int findP(int x) {//查询操做,实际上是查询 根节点 if (x < 0 || x >= parent.length) return -1; //或者直接抛出异常 while (parent[x] != x)//一直搜索到根节点 x = parent[x]; return x; } public void join(int a, int b) { int ap = findP(a); int bp = findP(b); if (ap != bp) { plant--; //a的层数越高,就将层数少的合并到层数高的上面 if (rank[ap] > rank[bp]) parent[bp] = ap; else if (rank[ap] < rank[bp]) { parent[ap] = bp; } else { //相同状况的话,随便就能够,但整体层高会增长1,画一个相同层的树合并一下就知道 parent[ap] = bp; rank[bp]++; } } } public int getPlant() { return plant; } public boolean isJoined(int a, int b) { //两个节点是不是 链接的 return findP(a) == findP(b); } }
咱们设想一下并查集的理想状态,全部树都是深度为1的树,这种理想情况的findP的时间复杂度为O(1),大大提升了查询性能,以下图所示class
o o / | \ \ / | \ o o o o o o o 全部的树都是以这种方式去组合的
可是咱们知道,若是A和B合并,层高是否是又变成2层了,如何才能让层高再次恢复到1层呢。这里路径压缩的一种方式是在查询的时候,将查询的节点不断往上提升,直接接到根节点上
节点1 o / | \ 节点2 o o o / 节点3 o 节点1
查询节点3的时候,是否是让节点3接到 节点2的父节点上 parent[节点3]= parent[节点2]
public class UFRankUnionCompressPath { int[] parent; int[] rank;//树的层数 int plant; //一共有多少株树 public UFRankUnionCompressPath(int size) { plant = size; parent = new int[size]; rank = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; rank[i] = 1; } } private int findP(int x) {//查询操做,实际上是查询 根节点 if (x < 0 || x >= parent.length) return -1; //或者直接抛出异常 while (parent[x] != parent[parent[x]]) //若是parent[x]==parent[parent[x]] 说明这颗树到了第二层,或者第一层 //第一层 由于parent[x]=x因此有 parent[x]==parent[parent[x]] //第二层 由于第二层的parent是根节点 有: parent[x]= root //因此有 parent[parent[x]]= parent[root] 而自己 parent[root]=root { // x = parent[x]; // parent[x] = parent[parent[x]]; //将下层节点往顶层提高,最终 } return parent[x]; } public void join(int a, int b) { int ap = findP(a); int bp = findP(b); if (ap != bp) { plant--; //a的层数越高,就将层数少的合并到层数高的上面 if (rank[ap] > rank[bp]) parent[bp] = ap; else if (rank[ap] < rank[bp]) { parent[ap] = bp; } else { //相同状况的话,随便就能够 parent[ap] = bp; rank[bp]++; } } } public int getPlant() { return plant; } public boolean isJoined(int a, int b) { //两个节点是不是 链接的 return findP(a) == findP(b); } }
期待下一篇,并查集运用相关的算法题及题解