并查集java实现笔记

并查集的实现有三个主要的方法:

  1. 初始化
  2. 查找该节点的根节点
  3. 合并
public interface DjsetsFunction {
	public void init();
	public int findRoot(int index); //return index of finding
	public void union(String i, String j);
}

初始化

我采用的是链表的方式,其实数组等其他的结构都是可以的,考虑到数组需要在添加完所有数据之后才能初始化,所以我采用了链表,可以new对象一边添加数据。

初始化之前,我们需要有几个链表来存储一些相关的东西:

  1. int size:存储当前的节点的个数
  2. ArrayList parents:存储第i个节点的根节点;初始设置时,每个节点的根节点就是它自己
  3. ArrayList rank:存储第i个节点作为根节点时子树的深度;初始设置时,每个节点的rank是1层
public Djsets(String name) {
	parents.add(size++);
	names.add(name);
	rank.add(1);
}

如果没有rank存储子树的深度的话,在合并的时候我们会将两个参数传入合并函数,此时要不就是左边节点变成右边的父节点,要不就是右边节点变成左边节点的父节点,这个过程的代码是写死的,不能发生变化。此时,如果出现了1-2(1节点与2节点合并),2-3,3-4…的情况,就会变成一个链表,而且递归寻找根节点的时候,需要全部遍历一次链表。这种情况下的时间复杂度是O(n)。为了降低时间复杂度,可以采用树的结构,就引入了rank这个深度的概念。在每次合并的时候,深度小的树合并到深度大的树里,这样树的深度会是两者的较大者,且发生变化。

查找根节点

第一种方式:

public int findRoot(int index) {
	// TODO Auto-generated method stub
	return parents.get(index);
}

这是最直接的通过链表里存放的数据返回当前节点的根节点的方式。但是这样不一定保证在别的地方调用一次findRoot函数就可以找到根节点,所以采用另一种实现的方式:

public int findRoot(int index) {
	// TODO Auto-generated method stub
	while(parents.get(index) != index) {
		parents.set(index, parents.get(parents.get(index)));
		index = parents.get(index);
	}
	return index;
}

通过把父节点改为祖父节点的方式,循环调用,在根节点的位置时,根节点没有父节点,只能指向自己,从而退出循环,返回真正的根节点。

合并

合并是我认为的并查集里最重要的部分。

首先当然是传入两个要合并的节点,然后找到这两个节点的根节点,要是属于同一个根节点,那就说明这两个节点已经合并过了,在“图”中有可能出现了回环的情况。要是两个节点的根节点不相同,则表示这两个节点所在的树不是同一棵,可以进行合并。

我们之前有讲到rank的概念,这里就要对每个节点的rank进行考虑。这个是我参考其他的讲解写的代码。

	public void union(String i, String j) {
		// TODO Auto-generated method stub
		int iRoot = findRoot((names.indexOf(i)));
		int jRoot = findRoot((names.indexOf(j)));
        
		if(iRoot == jRoot) {
			System.out.println(i + " " + j + " have combined");
		}else {
			if(rank.get(names.indexOf(i)) > rank.get(names.indexOf(j))) {
				parents.set(names.indexOf(j), iRoot);
			}else if(rank.get(names.indexOf(i)) < rank.get(names.indexOf(j))) {
				parents.set(names.indexOf(i), jRoot);
			}else {
				parents.set(names.indexOf(i), jRoot);
				rank.set(names.indexOf(j), rank.get(names.indexOf(j))+1);
			}
		}
	}

以下是连接和合并的顺序:

		String[][] NO1 = {
				{"A","B"},
				{"B","C"},
				{"B","D"},
				{"C","E"},
				{"D","F"},
				{"E","F"}
		};

但是在运行的时候会存在一些问题:

 我们绘制一幅这样的图:
 A - B
   / |
 C - D
 |   |
 E - F
 但是执行之后,情况变成了:
 A-B D-F C-E
 
 其实在运行 A B C D 的时候,运行的结果是这样的:
    B
   /|\
  A C D
 但是当C E合并的时候,因为此时 C 为根节点的子树的深度是 1, E 也同理,那么就会执行
 	parents.set(names.indexOf(i), jRoot);// i == "C", j == "E"
 这时,C 的根节点就会变成了 E,但实际上我们希望的是在C的基础上继续添加E
 D-F 同理
 为了避免这种情况,我们需要找到在树中的 C 与 未被合并的 E 之前的区别,那就是他们的根节点不同,所以我们需要加上根节点的判断
public void union(String i, String j) {
		// TODO Auto-generated method stub
		int iRoot = findRoot((names.indexOf(i)));
		int jRoot = findRoot((names.indexOf(j)));
		if(iRoot == jRoot) {
			System.out.println(i + " " + j + " have combined");
		}else {
			
			if(rank.get(names.indexOf(i)) > rank.get(names.indexOf(j))) {
				parents.set(names.indexOf(j), iRoot);
			}else if(rank.get(names.indexOf(i)) < rank.get(names.indexOf(j))) {
				parents.set(names.indexOf(i), jRoot);
			}else {
				if(parents.get(names.indexOf(i)) != names.indexOf(i)
						&& parents.get(names.indexOf(j)) == names.indexOf(j)) {
					parents.set(names.indexOf(j), iRoot);
					rank.set(names.indexOf(i), rank.get(names.indexOf(i))+1);
				}else {
					parents.set(names.indexOf(i), jRoot);
					rank.set(names.indexOf(j), rank.get(names.indexOf(j))+1);
				}	
			}
		}
	}

我们还可以在最后加一段代码判断建立好的结构里面,两点是不是联通的:

	public boolean isConnected(String i, String j) {
		int l = names.indexOf(i);
		int r = names.indexOf(j);
		if(this.findRoot(l) == this.findRoot(r)) {
			return true;
		}else {
			return false;
		}
	}

最后加入测试代码:

public class DjsetsTest {
	public static void main(String[] args) {
		Djsets dj1 = new Djsets("A");
		Djsets dj2 = new Djsets("B");
		Djsets dj3 = new Djsets("C");
		Djsets dj4 = new Djsets("D");
		Djsets dj5 = new Djsets("E");
		Djsets dj6 = new Djsets("F");
		Djsets dj = new Djsets();
		
		System.out.println("names: " + dj.names);
		System.out.println("init root: " + dj.parents);
		System.out.println("init rank: " + dj.rank);
		System.out.println();
		
		String[][] NO1 = {
				{"A","B"},
				{"B","C"},
				{"B","D"},
				{"C","E"},
				{"D","F"},
				{"E","F"}
		};
		
		for(int i=0; i<6; i++ ) {
			String l = NO1[i][0];
			String r = NO1[i][1];
			dj.union(l, r);
		}
		
		System.out.println("A C are connected: " + dj.isConnected("A", "C"));
	}
}

返回结果如下:

names: [A, B, C, D, E, F]
init root: [0, 1, 2, 3, 4, 5]
init rank: [1, 1, 1, 1, 1, 1]

E F have combined
A C are connected: true

(最后感谢b站up:正月点灯笼 & 中二病也要学算法 的投稿,对于我理解并查集提供了很多帮助)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值