算法课 Week1 笔记

第一部分:union-find算法

用来解决动态连通性的问题,即判断两个顶点pq是否是相连的,相连就返回是,不相连的话将两个顶点连起来并返回否(或者是不连通的整数对)。

union-find算法的API,先定义API,再实现API,每次碰到问题先设计API来封装基本操作是个好习惯。

public class UF

UF(int N) // 以整数标识(0到N-1)初始化N个顶点(触点)
void union(int p, int q) // 将pq连接起来
int find(int p) // p所在的分量的标识符
boolean connected(int p, int q) // 判断pq是否在一个分量里,即是否相连,通过find函数判断
int count() // 返回连通分量的数量

实现的时候维护了一个数组id,其中存储的是当前所在分量的标识符,初始的时候,N个顶点就有N个标识符。

实现一:Quick Find

特点:贪心算法,find函数很快,只用访问一次id数组,返回数组中对应的id值即可。

相对来说,union就会麻烦一点,每次都要将p的所在分量的所有顶点的id值改成后面q的分量值,来保证pq在一个分量中。这样一来,对于每一对输入,union都要扫描一遍id数组。

时间复杂度:O( n 2 )

  • union:N
  • find:1

实现二:Quick Union

特点:lazy approch(尽量避免计算直到不得不算),union函数很快。引入根结点,union函数只要将pq的根结点统一就行(每次把p所在的根结点连接到q所在的根结点上),find函数要顺藤摸瓜找出根结点。

使用这种实现,就像是不断在构造树一样(树的深度是对结点而言,高度对于整棵树而言,是所有结点中的最大深度)。

时间复杂度:最坏情况下(一棵笔直没有分支的树)还是O( n 2 )

  • union:树的高度
  • find:树的高度

实现三:加权Quick Union

上面的方法只是随意(不加判断地)将一棵树根结点连接到另一棵的根结点上,会造成深度越来越大的问题,而深度越大,每一次find操作耗费的时间就会越大。

这种方法的改进就在于,每次union都要判断一下两棵树的规模大小,总是将小树连接到大树,尽量避免将大树放到相对较低的位置。这样一来需要维护另一个数组,存储的是各个根结点对应的分量的大小。

这样一来,平均深度会比用上面实现的平均深度小很多。

时间复杂度:最坏情况O(N lgN)

  • union:lgN
  • find:lgN

实现四:最优算法——带路径压缩的加权Quick Union算法

上面的方法虽然是将小树连到大树上,减少了深度,但是有一种让深度大幅减少的方法:每次执行find操作往上找根结点的时候,把路上遇到的结点统统都直接连到根结点上,这样就可以得到一个完全扁平的树。

时间复杂度:最坏情况下,O(N)

  • union和find的复杂度非常非常接近但是没有达到1(均摊成本)

从上面可以看出发明一个有用算法的步骤:

  1. 建立问题模型
  2. 据此找出一个可以解决问题的算法
  3. 够快吗?内存够不够用?
  4. 如果不满足,找到原因
  5. 想办法找到造成问题的源头
  6. 迭代算法直到满意

第二部分:算法分析

一般教科书上直接给出了结论,而这里给出了怎么研究算法的时间复杂度和空间复杂度的过程。

一般来说是通过观察,提出模型,用模型预测未来,继续观察来验证预测模型的准确性,如此反复直到观察与预测一致。

时间复杂度

DoublingTest是随机生成一系列随机输入数组,下次生成的时候就将数组长度加倍,并打印出待测程序处理每种输入规模所需的时间。

获得了一系列输入规模和耗费时间的数据后可以用描点法画出函数图像,进而得出假设的函数关系,用以预测下次实验,进而进行验证出假设的函数关系是否合理。

幂次法则:T(N) = a 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 n b

lg(Tn) = b lgN + lg a

l g 5.1 2 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 n 2

64000带入得 a = 20.5 / 64000 2

Knuth提出程序运行的总时间主要和两点有关:

  1. 执行每条语句的耗时,取决于计算机、编译器和操作系统
  2. 执行每条语句的频率,取决于程序本身的输入

但是对程序这么一条条算太麻烦啦,就用抓大头的方法,忽略掉对最终结果无关紧要的项,大头是内循环中的操作,判断它的执行次数就能知道大致的时间复杂度。规模一大起来,其他语句的耗时远远比不上内循环语句的耗时。

空间复杂度

分析空间复杂度只用将变量的数量和它们的类型对应的字节数分别相乘并汇总即可。

对于一个数据类型的所有内存用量:

  • 原始类型:
    • int:4 bytes
    • double:8 bytes
  • 对象引用:8 bytes(Object reference)
  • 数组:24 bytes + memory for each array entry
  • 对象:16 bytes + 每个实例对象占用的内存 + 8 bytes 如果存在inner class(是指向enclosing class的指针)
  • padding:填充,确保大小是8的倍数

对于一维数组:

  • char[]:2N + 24
  • int[]:4N + 24
  • double[]:8N + 24

对于二维数组:

  • char[][]:~2MN
  • int[][]:~4MN
  • double[][]:~8MN

猜你喜欢

转载自blog.csdn.net/dmbjzhh/article/details/80949196