用来解决动态连通性的问题,即判断两个顶点pq是否是相连的,相连就返回是,不相连的话将两个顶点连起来并返回否(或者是不连通的整数对)。
union-find算法的API,先定义API,再实现API,每次碰到问题先设计API来封装基本操作是个好习惯。
public class UF
UF(int N) // 以整数标识(0到N-1)初始化N个顶点(触点)
void union(int p, int q) // 将p和q连接起来
int find(int p) // p所在的分量的标识符
boolean connected(int p, int q) // 判断pq是否在一个分量里,即是否相连,通过find函数判断
int count() // 返回连通分量的数量
实现的时候维护了一个数组id,其中存储的是当前所在分量的标识符,初始的时候,N个顶点就有N个标识符。
特点:贪心算法,find函数很快,只用访问一次id数组,返回数组中对应的id值即可。
相对来说,union就会麻烦一点,每次都要将p的所在分量的所有顶点的id值改成后面q的分量值,来保证pq在一个分量中。这样一来,对于每一对输入,union都要扫描一遍id数组。
时间复杂度:O( n2 n 2 )
特点:lazy approch(尽量避免计算直到不得不算),union函数很快。引入根结点,union函数只要将pq的根结点统一就行(每次把p所在的根结点连接到q所在的根结点上),find函数要顺藤摸瓜找出根结点。
使用这种实现,就像是不断在构造树一样(树的深度是对结点而言,高度对于整棵树而言,是所有结点中的最大深度)。
时间复杂度:最坏情况下(一棵笔直没有分支的树)还是O( n2 n 2 )
上面的方法只是随意(不加判断地)将一棵树根结点连接到另一棵的根结点上,会造成深度越来越大的问题,而深度越大,每一次find操作耗费的时间就会越大。
这种方法的改进就在于,每次union都要判断一下两棵树的规模大小,总是将小树连接到大树,尽量避免将大树放到相对较低的位置。这样一来需要维护另一个数组,存储的是各个根结点对应的分量的大小。
这样一来,平均深度会比用上面实现的平均深度小很多。
时间复杂度:最坏情况O(N lgN)
上面的方法虽然是将小树连到大树上,减少了深度,但是有一种让深度大幅减少的方法:每次执行find操作往上找根结点的时候,把路上遇到的结点统统都直接连到根结点上,这样就可以得到一个完全扁平的树。
时间复杂度:最坏情况下,O(N)
从上面可以看出发明一个有用算法的步骤:
一般教科书上直接给出了结论,而这里给出了怎么研究算法的时间复杂度和空间复杂度的过程。
一般来说是通过观察,提出模型,用模型预测未来,继续观察来验证预测模型的准确性,如此反复直到观察与预测一致。
DoublingTest是随机生成一系列随机输入数组,下次生成的时候就将数组长度加倍,并打印出待测程序处理每种输入规模所需的时间。
获得了一系列输入规模和耗费时间的数据后可以用描点法画出函数图像,进而得出假设的函数关系,用以预测下次实验,进而进行验证出假设的函数关系是否合理。
幂次法则:T(N) = a Nb N b
题:观察T(n)(运行时间,单位s)在不同规模的n(输入大小),最好的模型为 ____
n | T(n) |
---|---|
1000 | 0.0 |
2000 | 0.0 |
4000 | 0.1 |
8000 | 0.3 |
16000 | 1.3 |
32000 | 5.1 |
64000 | 20.5 |
答:
假设格式为Tn = a nb n b
lg(Tn) = b lgN + lg a
lg5.1≈2 l g 5.1 ≈ 2 , lg20.5≈4 l g 20.5 ≈ 4 ,带入上式:
2 = b lg 32000 + lg a 得 2 = 15 b + lg a
4 = b lg 64000 + lg a 得 4 = 16 b + lg a,下式减上式得出b = 2,带回Tn = a n2 n 2
64000带入得 a = 20.5 / 640002 64000 2
Knuth提出程序运行的总时间主要和两点有关:
但是对程序这么一条条算太麻烦啦,就用抓大头的方法,忽略掉对最终结果无关紧要的项,大头是内循环中的操作,判断它的执行次数就能知道大致的时间复杂度。规模一大起来,其他语句的耗时远远比不上内循环语句的耗时。
分析空间复杂度只用将变量的数量和它们的类型对应的字节数分别相乘并汇总即可。
对于一个数据类型的所有内存用量:
对于一维数组:
对于二维数组: