算法分析
如何评价一个算法?我们用什么标准来评价?具体是怎样分析的?
- 科学的方式?
Observe some feature of the natural world, generally with precise measurements
Hypothesize a model that is consistent with the observations
Predict events using the hypothesis
Verify the predictions by making further observations
Validate by repeating until the hypothesis and observations agree
The experiments we design must be reproducible and the hypotheses that we formulate must be falsifiable
设计实验可以重复的,结果也是可以重复的
直觉告诉我们,计算性任务的困难程度可以由问题的规模
来衡量,越是规模大的问题所需时间就越长,故从探讨:如何将问题规模
和运行时间
的关系量化是很合适的起点
使用ThreeSum
程序,根据输入问题的规模来实验:
猜测复合幂函数
分布,并预测了跟大规模的用时,通过比较验证了程序的运行时间根问题规模基本呈幂函数
分布
对于一段程序对其运行时间的数学建模往往需要一下步骤:
- 确定输入,定义问题规模
- 识别
内循环
- 根据内循环操作,确定成本模型
- 对于给定的输入,判断操作的执行频率
并查集Union Find
并查集
,是一种常见的数据结构方式,用以解决诸如集合合并查找类的问题,一开始并不关注其使用
还是照着老路子,我们要从基本的数据单元得到最合适的API及其实现
使用一个网络连接布线的例子,也称为动态连通性问题
,如果两个触点之间已经可以连通,就不需要布线了,使用数值代表触点,并有以下约定:
- 使用0-9来代表10个集合编号并且每个集合内的元素就是集合号本身--也就是触点
- 如果假定集合4和3拥有共同的某种属性,我们就称之为集合3等同于集合4,使用3=4标记--就是3 4 触电连接
- 如果3=4,同时4=5,5=7,1=9的话,[1 3 4 5 7 9]可归并为同一个集合,拥有相同的集合名称 -- 触电连接可传递
约定本篇不再区分触点
和集合
,连通
和合并,归并
等术语,设计的API如下:
1 | public calss UF 的API: |
使用数组id[]作为基本数据结构,一开始有N个分量,每个触电都含有一个分量,示例代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40public class UF {
private int[] id;
private int count;
// 初始化:N个分量N个触点,每个触点一个分量
public UF(int N) {
count = N;
id = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i;
}
}
public int count() {return count;}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public int find(int p) // 留待
public void union(int p, int q) // 留待
// 测试及输出说明
// 输入N个触点,通过每队整数调用find()方法:
// 若已连通,忽略;
// 若不连通,则调用union()方法连通;
// 最终输出各个连通分量对和总共的连通数
public static void main(String[] args) {
int N = StdIn.radInt();
UF uf = new NF(N);
while!(StaIn.isEmpty()) {
int p = StdIn.readInt();
int q = StdIn.readInt();
if(uf.connected(p , q)) continue; // 访问2次
uf.union(q,p);
StdOut.println(p + "连接" + q);
}
StdOut.println(uf.count() + "条连接");
}
}
剩下的就在于find()
和union()
的实现了,而其中很多地方如connected()
本质上都需要find()
方法,故它的具体实现会直接决定我们算法的效率,而各种查找算法真是本书的一大章节,这篇不会详细介绍留待后边,由于我们特殊的限定(即i就是触点名)这里选用最朴素的方式直接返回即可; 然后不妨在来考虑union()
的实现,由于我们使用id[i]表示触点,i为触点号,所以对于任意的两个触点连通就意味着2个触点同属一个触点的不同分量,所以对于id[x] = id [y] = SetName
就完成了触点的归并,如果若B触点都拥有多个分量值,只要B中有一个分量与A触点连通,根据连通传递的特性,那么B中所有分量都与A连通,故有:
1 | public void union(int q, int p) { |
定性的分析以下可知,find()一次会访问数组一次,整个测试中connected()和union()中又多次find,特别是在union中有对数组的完整遍历要访问N次,如果全部触点都连通的话,在main中又会有N次union(),所以综合来看动态连通性问题的quick-find
算法会是N^2级别的数组访问次数,在处理大型数据时并不能接受,是否还能提高呢?
quick-union
它是针对于上述quick-find
的改进,它使用的并不是单纯的数组,而是使用树
结构,一开始每一个树都只有一层一个节点,自然自己跟自己是连通的;如果p和q的root节点不同,表示两者不连通,如果两者root节点相同表示连通;如果两个不连通的树,调用union之后,会将一个树作为另一个树的分支,合并为拥有一个root节点的大树,通过这些描述,我们有了实现:
1 | public int find(int q){ |
个人在理解的时候,倒是find()
给我造成了些困惑,其实需要牢记先前的初始化是的一个约定,一个触点/集合的名称编号为数组的i,而集合的分量是在变动的,注意参考图示
如:
但是,对比quick-find
,quick-union
真的好吗?我们来分析它们的效率
对于前者,如果需要添加一个新的连通时,如a连通b,那么由于并不直到根b连通的已经有多少,只能遍历整个数组N,保证让a,b以及各自连通的分量都使用一个名称a,这样如果添加了N个每个都要N次,那么就是N^2级别的
对于后者,在最坏的情况下,即按顺序的查找如0-1,0-2,0-3…这样的顺序,最终会得到的树没有分支,就是一整条链表,如果这时从链表最底部的开始查询,需要遍历真个数组N级别的,然后如果完成全部连通,也是N^2级别的
但在其他情形之下,由于树都是有分支的,
…