图片来自慕课网,仅仅为了记录学习。
基本概念
/**
*
* 并查集,用来解决连通问题的,两个节点之间是否是连通的。
* 此处的节点是抽象的概念:比如用户和用户之间,港口和港口之间。
* 用来看他们是否是连通的。典型的就是看你社交中任何人之间的关系是否认识.
* 并查集问题和路径问题的区别:并查集比路径能做的操作少,它只能回答两个节点是否连通
* 路径还可以找到类似最短的连通点等等.但正因为并查集专注于连接问题,所以判断是否连接
* 修改连接状态时比较高效.
*
* Union-Find 如其名字我们主要提供
* union(p,q); //合并p、q两点使他们两个连通.
* find(p); //找到节点q的连通性,(处在什么状态合谁联通)
* 通过find的api,我们可以找到两个节点是否会连通的,即api
* isConnected(p,q);
* @author zhaoyuan.
*
*/
1、第一种实现:quick-find
union的时候每一个相等的都要改,如:1所以和2索引连通,那么1、3、5、7、9索引 对应的id必须都变为0.
//第一种基本的实现quick-find.
public class UnionFind {
//此处用一个id数组来表示每个节点的连通性。
//当节点连接到一起的时候那么它们有相同的id号
private int[] mIds;
//表示描述的节点的规模,总共有多少个
private int mCount;
//构造中实例化用来保存连通状态的数组,并初始化连通状态
//传入的并查集要表示多少个元素
public UnionFind(int capacity){
mCount = capacity;
mIds = new int[mCount];
//初始为每个点都不连通,此处i不同就表示不连通,想要连通是就把i设置为同一个即可
//同时也隐含着mCount各节点元素,每个节点元素的对应索引0...n,连通性,此处默认赋值都不连通
for (int i = 0; i < mCount; i++) {
mIds[i] = i+5;//注意id代表的含义不要和索引混了
}
}
//寻找p索引对应的连通性的状态,可以看到查找某个元素的连通状态码
//是非常的快的,直接在数组中索引即可时间复杂度O(1)
public int find(int p){
if( p<0 || p>=mCount){
//...做一些异常处理
}
//直接返回当前索引所对应的元素的连通性,
//此处设计的是每个连通性默认是索引号.
return mIds[p];
}
//此处设计是用的数组存储元素,传入的是数组内元素的索引,注意这个数组不是指mIds.
public boolean isConnected(int p,int q){
//返回p和q在ids数组中对应的连通状态码是否一致。
return find(p) == find(q);
}
//联合的整体思路:
// 要么把p索引在mIds中的状态变成q的,
// 要么把q索引在mIds中的状态变成p的
//mIds中的状态代表了连通性,id号相等就代表连通。
//此时就遍历mIds数组,然后把p/q索引对应的id进行相关赋值
public void union(int p,int q){
//先拿到p和q的id
int pId = find(p);
int qId = find(q);
//如果已经相等那么直接返回
if( pId == qId ){
return;
}
//注意如下为什么不直接mIds[p] = qId,不要被初始状态迷惑
//此处的设计思想quick-find查找快,但想要改变连通性的时候
//需要把所有的节点中的和pId相等的状态码,全部变成qId的状态码
//只有这样才能算是完全的连通了,你不能只改一个啊!!
//这种设计模式下的union的时间复杂度是O(n).
for(int i=0;i<mCount;i++){
if(mIds[i] == pId){
mIds[i] = qId;
}
}
}
}
这种实现最终整体的时间复杂度还是O(n)的级别,并且每次p、q连通的时,要改变所有连通id等于p的元素,把它们全都赋值成q对应的ids。
第二种实现:quick-union
初始的时候每个人的parent中的数指向自己的索引:
然后执行union (4 3):就是把索引4对应的元素的parent指向索引3.
再比如:union(4,9)
找到4所在的属性集合的根节点,然后连入到9所在的树形结构的根节点,但此时9正好是根节点。
/**
* 快速合并的并查集实现的思路:(还是用数组)
* 每个元素层都看成是一个节点,该节点有一个引用指向它的父节点。
* 如果一个元素指向父节点那么它就和父节点连通,当这个元素本身就是根的时候,
* 那么父节点就指向本身。
* 由于当前只需一个用来存储父节点的空间,所以依然可以用数组来实现,此处用一个
* int[] parent数组,里面存放的是元素要连通的父节点的索引.初始状态都连接自己
* 注意和quick-find区别,它表示连通的时可以 4--->3--->8 表示3 4 8都连通
* 单用quick-find的时候,就需要 元素索引4、3 、8对应的id都为一样的id号。
*
* @author zhaoyuan
*
*/
public class QuickUnion {
private int[] mParents;
private int mCount;
public QuickUnion(int capacity){
mCount = capacity;
mParents = new int[mCount];
//初始化时每个索引对应的mParents都为自己的索引+5,表示谁也不连接
for (int i = 0; i < mCount; i++) {
mParents[i] = i;//初始状态为每个节点自己的索引
}
}
//查找索引p在parent中对应的连通状态码,当它是在一个树的结构中时,
//需要找到它一直往上直到根节点的对应码,因为我们联合的时候都是按照
//根节点进行联合的
public int find(int p){
if( p<0 || p>=mCount){
//...做一些异常处理
}
//最根部的肯定是等于当前索引的.
while(p!= mParents[p]){
//依次往上,把指向的父索引值赋值给当前的p循环查找.
p = mParents[p];
}
return p;
}
//是否连通
public boolean isConnected(int p,int q){
return find(p)==find(q);
}
//联合p所以和q索引对应的状态.此处的设计:
// 1)、把p所在的树的根节点指向q所在的树的根节点
// 2)、把q所在的树的根节点指向p所在的树的根节点
//但从此角度考虑的话两种实现其实是一样的
public void union(int p,int q){
//还是先找到pId和qId。
int pRoot= find(p);
int qRoot = find(q);
//如果相等的时候,证明已经联合
if(pRoot == qRoot){
return;
}
//第一版我们什么也不考虑直接把p所在的树的根节点pRoot指向q所在的树的根。
//所以注意不是mParents[p] = qRoot,应该是p索引找到的根
//这个根肯定这会也是指向自己的元素的索引,直接mParents[pRoot]
//把mParents中pRoot索引对应的值变成qRoot,也就是指向qRoot.
mParents[pRoot] = qRoot;
}
}
这种实现测试发现联合、判断是否连接公共花费的时间不比quick-find少,因此我们进一步优化,让树的层次更
为平缓,我们在前面联合的时候直接把pRoot的根指向了qRoot的根,这样假如p这个树的节点比较多的时候,把
它指
向q所在的树的时候,这个树的深度会增加!因此我们可以以此为优化,把深度小的树的根节点指向深度
大的树的根节点,这样让整体更为扁平化。
就比如前面的union(4,9)
我们可以把元素比较少的如:
9的节点连接到元素比较多的4节点所在的根8上,
这样树形图的整体深度就不会增加。
第三种实现
public class QuickUnionBetter {
private int[] mParents;
//新加一个数组用来记录每一个节点,以它为根的元素的个数。
//mSize[i]表示以i为根的树结构中的元素个数。
private int[] mSize;
private int mCount;
public QuickUnionBetter(int capacity){
mCount = capacity;
mParents = new int[mCount];
mSize = new int[mCount];
for (int i = 0; i < mCount; i++) {
mParents[i] = i;
//默认每个都是1:独立的时候含有一个元素.
mSize[i] = 1;
}
}
//以下find和isConnected都用不到mSize.
public int find(int p){
if( p<0 || p>=mCount){
//...做一些异常处理
}
while(p!=mParents[p]){
p = mParents[p];
}
return p;
}
public boolean isConnected(int p,int q){
return find(p)==find(q);
}
//联合的时候就需要用到mSize了.看看那个节点为根的树形集合中元素多,
//然后把少的那个节点对应的根,指向多的那个节点对应的根。
public void union(int p,int q){
//前两步不变
int pRoot= find(p);
int qRoot = find(q);
if(pRoot == qRoot){
return;
}
int pSize = mSize[pRoot];//初始事都是根,为1
int qSize = mSize[qRoot];
//如果pRoot为根的树形集合含有的元素比qRoot的多
if(pSize > qSize){
//注意是少的索引的父节点指向多的
mParents[qRoot] = pRoot;
//注意此时mSize的改变,由于qRoot归并到了pRoot当中那么
//需要加上相应数量的size,注意qRoot对应的size并没有改变
mSize[pRoot] = pSize+qSize;
}/*else if(pSize < qSize){//同理
mParents[pRoot] = qRoot;
mSize[qRoot] = pSize+qSize;
}else{//如果两个相等那么就无所谓了,谁先合并到谁都可以.
mParents[qRoot] = pRoot;
mSize[pRoot] = pSize+qSize;
}*/
//然后就可以把等于的合入到大于或者小于的里面.
else{//此处把小于和等于合到一块
mParents[pRoot] = qRoot;
mSize[qRoot] = pSize+qSize;
}
}
}
但是还有可能出问题,因为以某个节点为根的树的集合元素多并不一定代表深度就大,我们还可以按照深度来进行优化。把深度小的合并到深度大的节点中,这种优化叫做基于rank的。
特殊情况如下:
按照size的优化方式进行合并的时候,最终树的深度又增加一层
按照我们新的rank的优化方式进行优化把层数比较少的连入层数比较多的!
第四种实现:
public class QuickUnionBest {
private int[] mParents;
//mRank[i]表示以i为根节点的集合所表示的树的层数
private int[] mRank;
private int mCount;
public QuickUnionBest(int capacity){
mCount = capacity;
mParents = new int[mCount];
mRank = new int[mCount];
for (int i = 0; i < mCount; i++) {
mParents[i] = i;
//默认每个都是1:表示深度为1层
mRank[i] = 1;
}
}
//以下find和isConnected都用不到mRank.
public int find(int p){
if( p<0 || p>=mCount){
//...做一些异常处理
}
while(p!=mParents[p]){
p = mParents[p];
}
return p;
}
public boolean isConnected(int p,int q){
return find(p)==find(q);
}
//找到p、q节点所在的树形集合的根节点,它的深度。然后把深度小的根节点合入到深度大的根节点当中
public void union(int p,int q){
//前两步不变
int pRoot= find(p);
int qRoot = find(q);
if(pRoot == qRoot){
return;
}
int pRank = mRank[pRoot];//初始事都是深度为1
int qRank= mRank[qRoot];
//如果p的深度比q的深度大.
if(pRank > qRank){
//注意是小的指向大的,也就是为小的重新读之
mParents[qRoot] = pRoot;
//此时把并不需要维护pRank,因为qRank是比pRank小的
//也就是q更浅,它不会增加p的深度,只会增加去p的宽度
}else if(pRank < qRank){
mParents[pRoot] = qRoot;
//同样的道理不需要维护qRank,p只会增加它的宽度
}else{
//当两个深度相同的时候,谁指向谁都可以,但是注意此时的深度维护
//被指向的那个的深度需要加1.
//此时让qRoot指向pRoot吧.
mParents[qRoot] = pRoot;
mRank[pRoot]++;
}
}
}
以上这种实现其实和第三种差不多,有时候甚至比第三种还要费一点点时间,但整体来说理论上可以防止出现
意外的概率。
最后一种优化路径压缩
特殊情况时:
第一种压缩方法:
第二种压缩方法:
只是修改了find方法而已.
//为防止极端的情况,可以再在find的时候经行路径压缩有两种压缩方法:
// 1)、一层层的跳着压缩(隔一层走一下):
// 就是当前节点的父节点parent[i]指向它父节点的parent,此时当前节点的父节点的
// parent不用担心不存在,因为不存在时parent会指向自己!这也是我们退出循环的条件
// 2)、压缩到深度只有两层
// 利用递归实现,让最终只有树形集合中除了根意外其它节点都在第二层.
// 理论上看第二种广度更大,应该时间更少,实测很多时候甚至比第一种多一点点。个人感觉可能是
// 由于一是用了递归,而这种方式在quick-union这种情况下优化费的步骤过于多.
public int find(int p){
if(p < 0||p > mParents.length){
//异常处理
}
while(p!=mParents[p]){
//首先是拿到父节点的父节点指向,然后赋值给当前节点的父节点。
//也就是parent[p]:当前节点的父节点;
//mParents[mParents[p]]:当前节点的父节点的父节点指向。
mParents[p] = mParents[mParents[p]];
//然后是当前节点跳一下,直接指向新得到的mParents[p].
p = mParents[p];
//继续循环
}
/*//第二种:通过如下,递归调用本函数find,
//mParents[p]为当前节点的父节点的索引,循环传入(递归)直到根节点
//p == mParents[p] 返回当前节点的索引,然后就层层返回。
if(p!=mParents[p]){
//其实也不该考虑一层层的就,考虑这一层,当前节点的索引和父节点不同,
//也就是说当前不是根节点,那么传入父节点的索引,递归调用把上个节点的父节点
//传给当前节点的父节点。
mParents[p] = find( mParents[p] );
}
//因为最终走到这的是根节点,根节点的parent是自己.
return mParents[p];*/
return p;
}
附上java测试代码:
package com.zy.tt;
public class Helper {
public void testUF1( int n ){
UnionFoundBest uf = new UnionFoundBest(n);
long start = getTime();
for( int i = 0 ; i < n ; i ++ ){
int a = (int) (Math.random()*n);
int b = (int) (Math.random()*n);
uf.union(a,b);
}
for(int i = 0 ; i < n ; i ++ ){
int a = (int) (Math.random()*n);
int b = (int) (Math.random()*n);
uf.isConnected(a,b);
}
long end = getTime();
showTimeDiff("quick-union no opt", start, end);
}
public void testUF2( int n ){
UniodFoundFinal uf = new UniodFoundFinal(n);
long start = getTime();
for( int i = 0 ; i < n ; i ++ ){
int a = (int) (Math.random()*n);
int b = (int) (Math.random()*n);
uf.union(a,b);
}
for(int i = 0 ; i < n ; i ++ ){
int a = (int) (Math.random()*n);
int b = (int) (Math.random()*n);
uf.isConnected(a,b);
}
long end = getTime();
showTimeDiff("quick-union optmization", start, end);
//uf.show();
}
/**
* 以s为单位获取当前时间
* @return 当前时间的秒数。
*/
public long getTime(){
long time = System.currentTimeMillis();
return time;
}
/**
* 显示时间差
* @param name 排序算法的名字
* @param start 开始的时间
* @param end 结束的时间
*/
public void showTimeDiff(String name ,long start,long end){
long diff = end - start;
System.out.println("name: "+name+" 花费了 = "+diff+"ms");
}
}