并查集能解决什么问题:
1.快速判断两个元素是否在同一个集合中。
2.合并两个元素所在集合,注意,输入的数据是两个元素,并不是两个集合,运算结果是将两个元素所在的集合合并。
3.并查集只适用于运算前已经将所有数据样本给它,不适用于动态添加数据,比如,运算到一半再用流来添加元素,并查集是不行的。
并查集结构及运算过程
1.并查集中,每个元素都有一个父元素,图中2和3号元素的父元素就是1号。
2.父元素指向本身的元素代表一个集合的终止。也就是说,2的父元素是1,1的父元素是它本身,那么,就说明元素2在集合1中。
3.初始化时,每一集合都只有自己本身一个元素,父节点全都指向自己本身。
4.判断两个元素是否在同一个集合中,就是两个元素各自依次遍历父元素,看看最终是否指向同一个终点元素,就是代表元素,代表一个集合。2和3向上的最终结果都是1,说明2和3在同一个集合1中。5向上是4,说明5在集合4中。
5.合并两个元素的集合,就是两个元素依次向上遍历,遍历到代表元素。然后判断两个集合哪个大?将小的集合链接在大集合的代表元素下面。比如,合并2和5所在的集合,2的所在集合是1,5所在集合是4,然后合并1和4,如下图。
6.并查集优化:如果链子太差的话,判断一个数是否在集合中,是不是需要向上遍历父节点很多次才能找到终点啊?这样效率就降低了。因此,并查集的优化,每次节点,当查询一次他在某个集合中时,就会把他以及他的所有父节点直接连接到代表节点上面,如下图,我们查询5所在集合时,就会把5以及父类中所有直接连接到代表节点上,5的子节点不动,如下图。
代码实现
public static class UnionFindSet{
public HashMap<Node,Node> fatherMap;//key是孩子,value是父节点,父节点可重复
public HashMap<Node,Integer> sizeMap;//某一个节点所在的集合共有多少节点,可以只保留代表节点。
public UnionFindSet(List<Node> nodes){
fatherMap=new HashMap<Node,Node>();
sizeMap=new HashMap<Node,Integer>();
makeSets(nodes);
}
private void makeSets(List<Node> nodes){
for(Node node : nodes){
fatherMap.put(node,node);//初始化时每一个集合都只有自己一个节点
sizeMap.put(node,1);
}
}
//找集合的头节点,并且将路径上的所有都直接连接在头节点上,递归实现
//为什么要用递归实现?可以这样考虑,如果不用递归,我们需要保存路径中每个节点,然后等到找到代表节点时,将他们全部连接上。什么结构能保存运算中间的数据呢?递归可以,递归的本质是系统帮我们压栈,同时保存所有中间数据。
private Node findHead(Node node){
Node father = fatherMap.get(node);
if(father!=node){
father = findHead(father);
}
fatherMap.put(node,father);
return father;
}
//判断是否在同一个集合
public boolean isSame(Node a,Node b){
return findHead(a)==findHead(b);
}
//合并集合
public void union(Node a,Node b){
if(a==null||b==null){
return;
}
Node aHead=findHead(a);
Node bHead=findHead(b);
if(aHead!=bHead){
int aSetSize=sizeMap.get(aHead);
int bSetSize=sizeMap.get(bHead);
if(aSetSize<=bSetSize){
fatherMap.put(aHead,bHead);
sizeMap.put(bHead,aSetSize+bSetSize);
}else{
fatherMap.put(bHead,aHead);
sizeMap.put(aHead,aSetSize+bSetSize);
}
}
}
}
并查集效率
并查集中放入的元素数量为N:
当查询次数+合并次数,达到O(N)及以上时,整个并查集中每一个查询以及合并的时间复杂度平均就为O(1)级别的。