数据结构-绪论(下)-笔记

 

 

# 绪论(下)

## (d)算法分析

### 01-D-1:算法分析

1.这里讲如何运用那一套尺度对DSA进行性能的分析
2.分析时的要点:去粗存精
3.算法分析包括两个部分:
a.算法自身正确性的证明(通过挖掘算法所具有的不变性和单调性来证明)
b.复杂度的分析和界定
4.对于b,需要将算法描述为某一计算模型的基本指令,并统计累计的执行次数?
——不必,得益于渐进复杂度概念。C++等高级语言的基本指令均等效于常数条计算模型的基本指令,在渐进意义下,二者大体相当
5.复杂度分析的主要方法:
迭代:级数求和
递归:递归跟踪+递推方程
猜测+验证

### 01-D-2:级数

1.级数
a.算术级数:与末项平方同阶
b.幂方级数:比幂次高出一阶
c.几何级数:与末项同阶
d.收敛级数:级数中各项逐次递减,且递减速度足够快,他们的总和不会超过某一个上界,这个上界虽然数值不同,但从渐进意义上,可以被视为常数,因而从大O意义上,可记作O(1)
e.可能未必收敛,但长度有限的级数
调和级数:
h(n)=1+1/2+1/3+...+1/n=确界(logn)
对数级数:
log1+log2+log3+...+logn=log(n!)=确界(nlogn)

### 01-D-3:循环

1.借助于级数的结论,如何具体分析代码段中所涉及的循环操作的复杂度?
2.从几何角度理解两个实例:
a.for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
b.for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
对于a:
n+n+...+n = O(n^2)
以i和j为两个轴,对应图像为一个矩形
对于b:
0+1+...+(n-1) = O(n^2)
对应图像为RT三角形
从图像可见,在渐进的意义上,二者都是平方的量级
c:
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j+=2013)
从图像的角度考察:外部循环的控制变量i依然是从0变化到n,这意味着内循环必然执行n躺,每一趟内循环步长变大了,等效于每一趟内循环的累计长度相对于b缩减到固定的1/2013,也相当于把坐标轴做一个压缩。这样的面积虽然貌似缩小了,但只是常系数的缩小,不影响渐进时间复杂度,依然是n^2量级
d:
for (int i = 0; i < n; i<<=1)
for (int j = 0; j < i; j++)
外循环的控制变量不再是以线性的增长,而是以2为倍数呈现出一个几何级数的形式

### 01-D-4:非极端元素+起泡排序

推广至一般性方法
1.取非极端元素
实例:给定整数子集s, 元素个数>3,找出其中既不是最大也不是最小的元素
简单分析可以得到:无论输入规模n有多大,上述算法需要的执行时间都不变
2.起泡排序
有序/无序序列中,任意/总有一对相邻元素顺序/逆序,采取扫描交换法:依次比较每一对相邻元素,如有必要,交换之,若整躺扫描都没有进行交换,则排序完成,否则,再做一躺扫描交换

### 01-D-5:正确性的证明

以起泡排序为例
1.该算法是否必然终止(有穷性)?至多需迭代多少躺?
——首先基于这个算法具有的一个不变性:经k轮扫描交换后,最大的k个元素必然就位
单调性:经过k轮扫描交换后,问题规模减至n-k
由不变性和单调性,得到正确性:经至多n躺扫描后,算法必然终止,且能给出正确解答
2.通过挖掘并且综合算法所具有的不变性和单调性,进而证明正确性的方法,是算法分析中一个基本而重要的技巧

### 01-D-6:封底估算-1

1.除了此前介绍的大O记号这样定性的定界方法,很多时候也需要进行准确的定量的估算,这种估算的重要方法之一就是所谓的Back-Of-The-Envelope Calculation
即封底估算,这种估算不需要借助笔和纸等工具,而是在头脑中抓住问题的主要方面,就可以很快的得出一个足够近似的估计
2.两个故事:费米和埃拉托斯特尼

### 01-D-7:封底估算-2

1.封底估算的关键在于抓住问题的主要方面,从而简洁地得出问题的总体规律,这一方面可以运用到复杂度的分析过程中(对象变成了时间)
2.对时间量的直接概念:
1天近似为10^5秒
1世纪近似为3*10^9秒
3.封底估算的实例:
对全国人口的普查数据排序
通过硬件的改进:30年缩短为20分钟
通过算法的改进:30年缩短为30秒

## (e)迭代与递归

### 01-E-1:迭代与递归

1.上面介绍了迭代式程序的算法复杂度分析的方法和技巧,接下来介绍递归式程序
2.这里开始,开始转向另一个问题,即在已经能够逐步了解和评判DSA的性能优劣之后,学习如何设计一个高效的DSA,这里介绍——分而治之
2.分而治之
凡治众如治寡,分数是也
3.数组求和:迭代
时间复杂度O(n)
空间复杂度:约定凡是谈论到空间复杂度的时候,都是在考量除了输入本身所占的空间之外我们所需另加的用于计算所必须的那些空间总量。这里是O(1)

### 01-E-2:减而治之

1.为了求解一个大规模的问题,可以将其划分为两个子问题:其一平凡,另一规模缩减,分别求解子问题,由子问题的解,得到原问题的解

### 01-E-3:递归跟踪

1.数组求和:线性递归
2.递归式算法分析的第一种主要技巧:
递归跟踪分析:
具体来说,就是把整个递归调用的过程,用一张图的形式表示出来
对于线性递归:整个调用过程都可以按线性的次序排列成一个队列
检查每个递归实例累计所需的时间(调用语句本身,计入对应的子实例),其总和即算法执行时间
本例中,单个递归实例自身只需O(1)时间,T(n) = O(1) * (n+1) = O(n)
3.线性递归是最简单的一种递归,每一个递归实例只会生成至多一个递归实例,所有递归实例按照这种关系可以排成一个线性的次序
4.递归跟踪
直观形象,仅适用于简明的递归模式

### 01-E-4:递归方程

1.如果递归跟踪是几何,那么递归方程就是代数
2.数组求和:线性递归
从递推的角度,看整个算法过程:
为求解sum(A, n), 需
递归求解规模为n-1的问题sum(A, n-1)
再累加上A[n-1]
递归基为sum(A, 0)
得到递推方程:
T(n) = T(n-1) + O(1) //recurrence
T(0) = O(1) //base
3.这种方法可以比作解微分方程

### 01-E-5:数组倒置

1.减而治之的另一个实例:数组倒置
任给数组A[0, n],将其前后颠倒
2.递归版
问题规模的奇偶性不变,需要两个递归基

### 01-E-6:分而治之

1.分而治之:
为求解一个大规模的问题,可以
将其划分为若干(通常两个)子问题,规模大体相当
分别求解子问题
由子问题的解,得到原问题的解

### 01-E-7:二分递归:数组求和

1.数组求和:二分递归
复杂度分析:
a.递归跟踪
T(n) = 各层递归所需时间之和
= O(n)
抓主要矛盾:层次关系构成几何级数,几何级数的总和与末项是同阶的
即,从渐进意义上,最底层的总数可以代表整体的总数
b.递推方程
T(n) = 2*T(n/2) + O(1)
T(1) = O(1)

### 01-E-8:二分递归:Max2

1.减而治之和分而治之都是算法分析的强有力的武器
2.实例:Max2: 迭代1
从数组区间[ l0, hi) 中找出最大的两个整数A[x1]和A[x2]
要求元素比较的次数尽可能地少
时间复杂度 = 确界(2n-3)
3.简单改进:
将三重循环改进为一重,但增加了一次比较
并没有实质性改进

### 01-E-9:Max2:二分递归

1.Max2: 递归+分治
到此为止:已经学习了递归和迭代的算法,包括两种重要的算法的策略:减而治之 和 分而治之,最重要的是,结合这两种策略的介绍和实例,给出了递归算法的两种典型的常用的分析方法——递归跟踪和递推式

## (xc)动态规划

### 01-XC-1:动态规划

1.动态规划是DSA设计与优化的一种重要手段
work->right->fast
2.从某种意义上讲,所谓的动态规划,也可以理解为通过递归,找出了算法的本质,并且给出一个初步的解之后,再将其等效地转化为迭代的形式
3.实例:斐波那契数列
首先把它用递归形式实现,显然是可以work,并且right,但不够fast

### 01-XC-2: FIB():递推方程

1.仍考虑斐波那契数列问题
得到递推方程
T(n) = T(n-1) + T(n-2) + 1
T(0) = T(1) = 1
分析得到T(n) = O(2^n)
这是一个指数级复杂度

### 01-XC-3: FIB():封底估算

1.
Fi^36 = 2^25(6^2 --- 5^2)

### 01-XC-4: FIB():递归跟踪

1.可以采用递归跟踪对前述算法分析
2.递归版fib()低效的根源在于:各递归实例被大量重复地调用
而事实上每个递归实例值需计算一次,那么如果要对这个算法改进,就是对每个实例只计算一次,这是可以实现地

### 01-XC-5: FIB():迭代

1.第一种改进
在每次试图生成一个新的递归实例的时候,都去检查一下,是否在此之前已经被唤醒,并计算得到了相应的结果,如果是就不必再唤醒它进行计算,这样直接把原先的结果取出返回即可
这种方法称为 记忆法
即将已计算过的实例的结果制表备查
2.第二种改进
就此例而言,递归的计算方向是从大到小,自顶向下;不妨改为自底向上,这样就把递归算法该进程了迭代算法
时间复杂度优化为O(n),且只需要O(1)空间

### 01-XC-6: 最长公共子序列

1.动态规划的另一个应用实例:
最长公共子序列LCS
2.子序列:由序列中若干字符,按原相对次序构成
3.最长公共子序列:两个序列公共子序列中最长者;可能会有多个,可能有歧义
首先忽略这些细节,关注:如何计算最长公共子序列的长度

### 01-XC-7: LCS:递归

1.LCS:递归
减而治之
分析此算法的正确性及效率

### 01-XC-8: LCS:理解

1.如果沿用之前的表示方式,可以把在这个算法过程中所能够生成的所有递归实例汇合整理成一张表
减而治之 分而治之 两种策略

### 01-XC-9: LCS:复杂度

1.LCS:递归
这个算法的正确性可以得到保证,由于
a.单调性:无论如何,每经过一次比对,原问题的规模必然可减少,具体的,作为输入的两个序列,至少其一的长度缩减一个单位
2.
最好的情况下,不出现分而治之,只需要O(n+m)时间
但问题在于,在分而治之情况下,原问题分解为两个子问题,更糟糕的是,他们在随后进一步导出的子问题,可能雷同
这种情况下,复杂度为指数级

### 01-XC-A: LCS:动态规划

1.LCS:迭代
对比可知
这个问题与fib()类似,
二者都有大量重复的递归实例(子问题)
(最坏情况下)先后共计出现O(2^n)个
2.采用动态规划的策略
只需O(m*n)时间即可计算出所有子问题
为此,只需
0)将所有子问题列成一张表
1)颠倒计算方向, 从LCS(A[0], B[0])出发,依次计算出所有项
3.总结
递归虽然可以帮助我们很好的找到一个可行并且正确的解,但是,如果要把效率提高,使之变成一个实用算法的话,往往还需要进行进一步调试,在这个过程中,动态规划扮演着非常重要的角色

递归:设计出可行且正确的解
动态规划:消除重复计算,提高效率

posted @ 2019-07-28 10:32  ldmeng  阅读(392)  评论(0编辑  收藏  举报