概念
计算:即信息处理,是指借助某种工具,遵循一定规则,以明确而机械的形式进行的操作。
计算模型:即计算处理工具。
算法:在特定计算模型下,解决特定问题的指令序列(操作步骤),算法具有如下特征:
1、输入、输出
2、确定性 算法的每个基本操作步骤必须有确定的含义
3、可行性 算法中每个基本操作步骤都可以实现,并且在有限时间内完成
4、有穷性 对于任何输入,经过有限次基本操作之后都可以得到输出
好算法:最重要的是效率,速度尽可能快,存储空间尽可能小。当然前提是正确的算法,健壮、可读是另一方面。
算法运行的“平台”——计算模型
对于特定问题的不同算法,很难用实验统计的方式确定真正的效率,因为不同的算法可能更适应于不同规模和类型的输入。为了给出客观评价,需要抽象出一个理想的平台或者模型,不依赖任何外部的各种因素。比如图灵机(Turing Machine)模型和随机存取机(Random Access Machine)模型。
1. 图灵机模型(Turing machine)
图灵机就是指一个抽象的机器,它有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的颜色。有一个机器头在纸带上移来移去。机器头有一组内部状态,还有一些固定的程序。
纸带:依次均匀地划分为单元格各存有某一字符,初始均为’#’
读写头:总是对准某一单元格,并可读取或改写其中的字符,每经过一个节拍可转向左侧或右侧的领格
状态:TM总是处于有限状态的某一种,每经过一个节拍可按照规则转向另一状态
2. RAM模型(Random access machine)
随机存储机(random access machine,RAM) 可以通过编号访问任意寄存器,避免读写头单格移动,极大提高读写效率及灵活性。同时,RAM模型是冯诺依曼结构的基础,访问所用的语言也是汇编语言的伪码形式。
常规操作:
R[i]<−cR[i]<−R[R[j]]R[i]<−R[j]+R[k]
R[i]<−R[j]R[R[i]]<−R[j]R[i]<−R[j]−R[k]
IFR[i]=0GOTOIFR[i]>0GOTO
思考:在TM、RAM等模型中衡量算法效率,为何通常只需考察运行空间?空间呢?
答:早期的存储器价格昂贵,所以设计算法时需要考虑到时间和空间两个方面。但随着硬件行业的发展,现今存储器的容量越来越大,价格却越来越便宜,相比之下运行时间成为衡量算法性能的更重要的指标。
衡量算法优劣的“尺子”——复杂度函数
考察算法的优劣应着眼于以下两个方面:
- 正确性:采用数学证明算法的功能与问题要求一致,一般不容易证明,例如hailstone序列。
- 成本:时间成本和空间成本。度量的方法是将计算成本描述为求解问题实例的函数。但可能出现的实例太多,而问题实例的规模往往是决定计算成本的最主要因素,所以根据实例的规模进行概括复杂度函数。
TA(n)=用算法A求解某一问题规模为n的实例,可简化为T(n)
复杂度函数可将原本用于直观认识算法效率的运行时间,等价地转化为基本操作的次数,从而使得算法分析由一个复杂的统计问题,转化为一个明确的代数求和问题。
“尺子”上的“刻度”——“大O记号”
大o曲线(即c⋅f(n))前面一段比T(n)小,但只要c足够大,一定会有大o曲线一直高于T(n),即c⋅f(n)>T(n)
1. 常数阶复杂度O(1)
复杂度与输入规模无关,主要是一些不含有循环、分支及递归的顺序执行序列。
2. 对数阶复杂度O(log n)
常底数可替换为任意值,常数次幂可转换为常系数忽略,对数多项式的低次项可忽略。
3. 多项式阶复杂度O(n^c)
4. 指数阶复杂度O(2^n)
总结:1 < logn < n^c < 2^n。除此之外,由于循环结构的嵌套等,还有可能出现类似 O(nlogn)、O(nloglogn)这样的组合型复杂度.
算法分析方法
算法分析主要基于时间复杂度的 “大O”记号,统计算法描述为RAM的基本指令的执行次数。高级语言的基本指令等效于常数条RAM基本指令,渐进分析下两者指令数量大体相当,因此基本指令执行次数的计数又可以转化为对于源代码语句块的计数。
-
常数阶O(1):顺序结构,分支结构
-
对数阶O(logn):二分查找
-
线性阶O(n):单个循环
-
平方阶O(n2):循环嵌套
实际应用过程中会出现更加复杂的情况,比如调用和递归(自我调用)就不能用以上复杂度计算。因此,我们针对常见迭代和递归有如下方法:
-
迭代:级数求和
-
递归:递归跟踪、递推方程
1. 级数求和
-
收敛级数:T(n)=1+221+321+...+n21<1+221+321+...=6π2=O(1)
-
调和级数:T(n)=1+21+31+...+n1=O(logn)
-
对数级数:T(n)=log1+log2+log3+...+logn=O(nlogn)
-
算术级数:T(n)=1+2+...+n=2n(n+1)=O(n2),与末项平方同阶
-
幂方级数:T(n)=12+22+32+...+n2=∑k=0nk2≈∫0nx2dx=31n3=O(n3),比幂次高出一阶
-
几何级数:T2(n)=1+2+4+8+...+2n=∑k=0n2k=2n+1−1=O(2n+1)=O(2n),与末项同阶
2. 递归跟踪
递归跟踪,即把整个递归调用的过程用一张图表示出来。递归跟踪分析包含以下三个步骤:
- 枚举(可以采用树状图的方法)所有递归实例
- 归纳出每个递归实例时间开销的规律(注意:进入或者说调用下一层递归复杂度仅为O(1))
- 累计
递归跟踪虽然直观形象,但仅适用于简明的递归模式。对于比较复杂的递归调用形式,很难使用图绘制出来。因此需要另外一种较为间接抽象,但更适用于复杂的递归模式的递归方程。
3. 递推方程
如果说递归跟踪分析是偏向“几何”的方法, 那递推方程就是纯正的“代数”方法了
类似于解数列通项表达式或者微分方程等从隐式表达解显式方程的过程,我们借助递推关系归纳出T(n)与T(n – 1)等的关系,联立base case(比如T(1) = c),解出T(n)的表达式再化简为O(f(n))
1、迭代法:不断用递推方程的右部替换左部、换元迭代
2、递归树:建立递归树,每次迭代将函数项作为儿子,非函数项作为根的值。以二分归并排序递归方程为例。
3、主定理
算法设计思想
算法设计思想实践中的总体趋向是缩减问题规模,根据划分的问题是否重叠可分为分治和动态规划,而分治根据划分规模又可以分为减而治之、分而治之。
1. 减而治之
定义:将大规模的问题划分两个不相关子问题:其一平凡,另一规模缩减,分别求解,由子问题的解,得到原问题的解。
本程序采用减而治之的方法计算数组所有元素和,问题划分为子问题返回0,另一规模缩减为sum(n-1),再分别减治两个子问题,直至问题规模为O(1):
sum(int a[], int n)
{ return n < 1 ? 0 : sum(A, n-1) + a[n - 1]; }
递归跟踪
(1)检查每个递归实例为sum(),总计n+1项
(2)每个递归实例仅执行一条语句,实例时间开销为O(1)
(3)累计时间复杂度T(n)=O(1)∗(n+1)=O(n)
递归方程:T(n)=T(n−1)+O(1)
递归基:T(0)=O(1)
求解:T(n)=T(n−1)+O(1)=T(n−2)+O(1)+O(1)=...=T(0)+n=O(1)+n=O(n)
2. 分而治之
定义:将大规模的问题划分为若干不相关子问题,规模大体相当,分别求解子问题,由子问题的解,得到原问题的解。
本程序采用分而治之的方法计算数组所有元素和,问题划分为左子问题sum(a, lo, mi)和右子问题sum(a, mi+1, hi),再分别分治两个子问题,直至问题规模为O(1):
sum(int a[], int lo, int hi)
{
if (lo == hi) return a[lo];
int mi = (lo + hi) >> 1;
return sum(a, lo, mi) + sum(a, mi+1, hi);
}
递归跟踪
(1)检查每层递归实例sum()数量,总计(20+21+22+...+2logn)项
(2)每个递归实例仅执行3条语句,实例时间开销为O(1)
(3)累计时间复杂度T(n)=O(1)∗(20+21+22+...+2logn)=O(1)∗(21+logn−1)=O(n)
递归方程:T(n)=2∗T(n/2)+O(1)
递归基:T(1)=O(1)
求解:T(n)=2∗T(n/2)+O(1)=4∗T(n−2)+O(3)=...=n∗T(0)+O(n−1)=O(2n−1)+n=O(n)
3. 分治法缺陷
分治法将问题划分为互不相交的子问题,递归求解子问题,再将它们的解组合起来,求出原来问题的解。而当子问题出现重叠时,分治算法会做许多重复的工作,浪费大量资源和时间。下面以
fib(n)=fib(n−1)+fib(n−2)为例:
int fib(n){
if(n < 2){
return n;
}else{
return fib(n - 1) + fib(n - 2);
}
}
递推方程:T(n)=T(n−1)+T(n−2)+1,T(0)=T(1)=1,∀n>1
令S(n)=[T(n)+1]/2
则S(0)=1=fib(1),S(1)=1=fib(2)
故S(n)=S(n−1)+S(n−2)=fib(n+1)
T(n)=2∗S(n)+1=2∗fib(n+1)−1=O(fib(n+1))=O(φn)
其中φ=(1+√5)/2≈1.618,φ43≈230≈109flops=1s,φ67≈1014flops=105s=1day
封底估算:介于猜测和铁证之间的一个概念,指的是一个很粗略的计算,不精确,但可以被用作对某个观点的支持或论据。本课程的封底估算是用flops(每秒所执行的浮点运算次数)估算算法的运行时间,1秒可执行10^9条浮点数指令。
4. 动态规划
定义:动态规划和分治法相似,都是通过组合子问题的解来求解原问题。不同的是,动态规划颠倒计算方向,由自顶而下的递归改为自顶而上迭代,使得对每个子问题只求解一次,避免重复计算的步骤。
int fib(int n)
{
f = 1; g = 0;
while (0 < n--)
{
g = g + f;
f = g - f;
}
return g;
}
复杂度:T(n)=O(n),而且仅需O(1)空间