目录
一. 什么是并查集
并查集是一种树形结构,用来处理一些不相交的集合的合并问题。
假如说,我们有上面几种只有单个元素的集合,并查集所能执行的操作就是 合并两个集合以及查看两个集合是否在同一个集合中,且两种操作的时间复杂度都为O(1)
比如说,a和b是否在同一个集合中,以及合并a和b到同一个集合中
二. 并查集详解
我们要如何实现合并两个集合以及查看两个集合是否在同一个集合中,且时间复杂度都为O(1)呢?
这里就引出并查集,假设我们有四个单集合如下图,我们假设每一个单集合都有一个指针指向自己,如下图:
假设说,我们将单集合a与单集合b进行合并,a的指针就可以指向b
在我们判断是否是同一个集合中的时候,我们只需要判断每个元素其指针指向的最上方的元素是否是同一个,就可以判断其是否在同一个集合中,有点类似于树的公共祖先。
比如说,此时a的最上方指向就是b,b的指向还是自身,是相同的最上方元素,故我们说a和b是同一个集合
判断c和a是否是同一个集合时,我们发现a的最上方元素指的是b,c的最上方元素指的是c,并不相同,故我们说二者并不在同一个集合中。
并查集的合并操作也是比较简单的,其主要过程就是让长度小的集合的最上端元素的指针指向长度长的集合的最上端元素。
比如说,我们合并上面的{a,b} 集合 和 {c} 集合,此时 {a,b}集合长度为2,而c集合长度为1
此时c集合的长度短,应让c集合的最上端元素即c本身的指针指向{a,b}集合的最上端元素,如下图:
此时我们再判断a和c是否在同一个集合中时就可以发现,a的最上端元素是b,c的最上端元素也是b,故a和c是同一个集合中的。
三. 并查集的代码实现
首先我们知道并查集的操作 ,查询是否同一个集合时,需要判断二者是最上端元素是否是同一个,故我们需要一个函数能够得到其最上端的元素,即 getHead方法
同时,我们封装集合时,使用一个fatherMap 的HashMap进行封装,其key值为当前节点,value值为其 父节点 ; 再使用一个 sizeMap的HashMap进行封装,其key值为每个集合的最上端节点,value 值为该集合中的元素个数。(便于在union时进行大小比较)
同时考虑到在HashMap中key的值只能有一个,但是并查集中有可能会有重复元素,所以我们单独封装一个类,这个类中的属性只有值value
elementHashMap类的作用是记录 Element类中的 value和其对应的类
初始化过程如下:
构造方法就是传入一个List集合,然后我们将里面的所有元素都作为单集合进行处理 (<V>为泛型,因为用户可能输入不同的数据类型)
public class UnionFind {
public static class Element<V>{
public V value;
public Element(V value){
this.value=value;
}
}
public static class UnionFindSet<V>{
public HashMap<V,Element<V>> elementHashMap;
//key 某个元素 value 该元素的父
public HashMap<Element<V>,Element<V>> fatherMap;
// key 某个集合的代表元素, value 该集合的大小
public HashMap<Element<V>,Integer> sizeMap;
public UnionFindSet(List<V> list){
for(V value:list){
Element<V> element=new Element<>(value);
elementHashMap.put(value,element);
fatherMap.put(element,element);
sizeMap.put(element,1);
}
}
}
getHead操作:
我们发现在fatherMap这张表中,只有最上端的节点,其key值才会与其value值相等
public Element<V> getHead(Element<V> e){
while(e!=fatherMap.get(e)){
stack.push(e);
e=fatherMap.get(e);
}
//此时e就是头顶节点
return e;
}
isSameSet操作:
这个操作我们需要判断两个元素是否在同一个集合中,首先我们要判断这两个元素是否都在集合中,然后再判断二者的最上端元素是否相同,代码如下:
public boolean isSameSet(V a,V b){
if(elementHashMap.containsKey(a) && elementHashMap.containsKey(b)){
return getHead(elementHashMap.get(a))==getHead(elementHashMap.get(b));
}
return false;
}
union操作:
这个操作就需要进行两个集合的合并了
我们还是先判断两个元素是否都是集合中的元素,然后再判断二者的最上端元素是否相同,如果相同则不需要进行合并,不同时则进行合并,代码如下:
public void union(V a,V b){
if(elementHashMap.containsKey(a) && elementHashMap.containsKey(b)) {
Element<V> headA = elementHashMap.get(a);
Element<V> headB=elementHashMap.get(b);
if(headA!=headB) {
Element<V> big = sizeMap.get(headA) > sizeMap.get(headB) ? headA : headB;
Element<V> small = big == headA ? headB : headA;
fatherMap.put(small, big);
sizeMap.put(big, sizeMap.get(big) + sizeMap.get(small));
sizeMap.remove(small);
}
}
}
四. 并查集的改进
在getHead代码中,我们发现去找最上端元素时,往往需要遍历每一个元素过去,但是我们需要的仅仅只是其最上端的元素,所以我们应该在遍历一遍以后,同时把这个集合中的所有元素的指针指向最上端元素,改进代码如下:
public Element<V> getHead(Element<V> e){
Stack<Element<V>> stack=new Stack<>();
while(e!=fatherMap.get(e)){
stack.push(e);
e=fatherMap.get(e);
}
//此时e就是头顶节点
while(!stack.isEmpty()){
fatherMap.put(stack.pop(),e);
}
return e;
}
总体代码如下:
package com.atguigu;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
public class UnionFind {
//样本进来会包一层,叫做元素
public static class Element<V>{
public V value;
public Element(V value){
this.value=value;
}
}
public static class UnionFindSet<V>{
public HashMap<V,Element<V>> elementHashMap;
//key 某个元素 value 该元素的父
public HashMap<Element<V>,Element<V>> fatherMap;
// key 某个集合的代表元素, value 该集合的大小
public HashMap<Element<V>,Integer> sizeMap;
public UnionFindSet(List<V> list){
for(V value:list){
Element<V> element=new Element<>(value);
elementHashMap.put(value,element);
fatherMap.put(element,element);
sizeMap.put(element,1);
}
}
public void union(V a,V b){
if(elementHashMap.containsKey(a) && elementHashMap.containsKey(b)) {
Element<V> headA = elementHashMap.get(a);
Element<V> headB=elementHashMap.get(b);
if(headA!=headB) {
Element<V> big = sizeMap.get(headA) > sizeMap.get(headB) ? headA : headB;
Element<V> small = big == headA ? headB : headA;
fatherMap.put(small, big);
sizeMap.put(big, sizeMap.get(big) + sizeMap.get(small));
sizeMap.remove(small);
}
}
}
public boolean isSameSet(V a,V b){
if(elementHashMap.containsKey(a) && elementHashMap.containsKey(b)){
return getHead(elementHashMap.get(a))==getHead(elementHashMap.get(b));
}
return false;
}
public Element<V> getHead(Element<V> e){
Stack<Element<V>> stack=new Stack<>();
while(e!=fatherMap.get(e)){
stack.push(e);
e=fatherMap.get(e);
}
//此时e就是头顶节点
while(!stack.isEmpty()){
fatherMap.put(stack.pop(),e);
}
return e;
}
}
}