本来打算写多线程ThreadLocal的源码的,为什么突然转来写一篇关于并查集的日志,因为最近在改自己以前写的代码时,发现很多地方可以改写得更好,对于我来说,平时除了开发自己喜欢的软件外,最大的乐趣就是优化以前写过的代码,这次就来总结下当初写的并查集的两种优化算法:按秩归并和路径压缩。
例子是模拟网络连通检查,是这样一个问题,假设网络中分布着各个结点,这些结点可以是主机或是路由器,它们之间有的可以互相通信,有的还没有建立连接,无法通信。我们要写出一个程序,输入两个结点信息,判断它们是否能够通信,如果可以,则输出提示,否则让它们之间建立连接。因为是网络上的结点,它们有传递特性,即如果结点A和结点B连接,结点B和结点C连接,那么结点A和结点C也就建立了连接。如果结点A和结点B连接后,这对结点合并成一个连通分量,当结点B和结点C连接后,结点ABC三个也合并成了一个连通分量。
这些结点的存储结构用树来表示,那么这个网络就是一片森林。为什么要用树来存储这些结点,因为我们来看看互连网络:
一个路由器可连接多个交换机,一个交换机能连接多台主机,所以用树来作为存储结构能很形象地表示出网络。树结点保存着与该结点相连的另外一个结点的名称,用这种关系来表示它们的连接状态,这样我们就可以从一个结点得到与其相连的下一个结点,由此我们可以一直寻找某一个连通分量中所含有的结点,直到最后找到根结点,即结点中标识符等于自己的结点,我们可以利用连通分量的根结点来判断两个结点是否相连,如果两个结点已连接,那么它们的根结点一定是相同的。接下来,根据问题的解决过程来设计API:
- 第一步查找两个结点所在的连通分量,如果两个结点处在同一个连通分量中,那么它们就是已连接的:初始时假设这个网络中所有结点都是互不相连的,各自都是一个连通分量,当两个结点的父结点相同时,就认为它们是相连的网络,处在同一个连通分量中:
//查找网络n所在的连通分量标识符(用其根结点标识这个网络)
public int Search(int networkAddress) {
while(networkAddress != networkID[networkAddress]) {
networkAddress = networkID[networkAddress]; //往上寻找它的根结点
}
return networkID[networkAddress];
}
Search方法就是用来寻找一个结点的根结点,初始时所有结点都未相连,所以结点标识符都是等于自己,当两个结点例如A和B连接后,B结点的标识符就等于A了,所以可以从networkAddress[ B ] = A得到B与A连接,再从networkAddress[ A ] = A得到结点A是根结点,A就是这个连通分量的标识符,当结点C与结点B连接后,networkAddress[ C ] = B,由此可得C与B连接,再从networkAddress[ B ] = A找到A结点,所以结点C的根结点也是A,所以,结点ABC都拥有共同的根结点A,即表示这三个结点已建立连接。
- 判断两个结点(或连通分量也行,有可能是一个连通分量中有多个结点,另一个连通分量中只有初始的一个结点)的连接状态:
//判断两个网络分量的连接状态
public boolean ConnectionState(int networkAddress1, int networkAddress2) {
return (Search(networkAddress1) == Search(networkAddress2));
}
就是调用Search()方法查找两个结点所在连通分量的根结点(标识符),判断它们是否相等,作为是否要将它们连接的依据。
- 如果两个连通分量未连接,则为它们建立连接:建立连接前先获得两个连通分量的标识符,判断不相等后便把其中一个连通分量链接到另一个连通分量上,最后把记录网络中连通分量的变量componentCount--。
来看下运行结果: