一、计算
1.算法
1.1 计算 = 信息处理
- 借助某种工具,遵照一定规则,以明确而机械的形式进行
1.2 计算模型 = 计算机 = 信息处理工具
1.3 所谓算法,即特定计算模型下,旨在解决特定问题的指令序列
-
输入-待处理的信息(问题)
-
输出-经处理的信息(答案)
-
正确性-的确可以解决指定的问题
-
确定性-可描述为一个由基本操作组成的序列 (如加盐少许,加糖适量,煮至半熟... )
-
可行性-每一基本操作都可实现,且在常数时间内完成 (如把大象放进冰箱,有三步...)
-
有穷性-对于任何输入,经有穷次基本操作,都可以得到输出
2.有穷性
2.1 希尔顿序列 (Hailstone Sequence)
-
-
如Hailstone(42)={42,21, 64, 32, ..., 1}
-
总趋势是下降的
2.2 代码
int hailstone(int n){
int length = 1;
while (1 < n) { n % 2 ? n = 3 * n + 1 : n /= 2; length++; }
return length;
}
2.3 但对于希尔顿序列是否全部有穷,仍然未知,所以希尔顿序列不是算法
3.正确的算法
3.1 符合语法,能够编译、链接
-
能够正确处理简单的输入
-
能够正确处理大规模的输入
-
能够正确处理一般性的输入
-
能够正确处理退化的输入
-
能够正确处理任意合法的输入
3.2 健壮
- 能辨别不合法的输入并做适当处理,而不致非正常退出
3.3 可读
- 结构化 + 准确命名 + 注释 + ...
3.4 效率
- 速度尽可能快,存储空间尽可能少
二、计算模型
1.统一尺度
1.1 TA(P) = 算法A求解问题实例P的计算成本
1.2 同一问题通常有多种算法,实验统计时评估各类算法优劣最直接的方式,但不足以准确反映算法的真正效率
-
不同的算法,可能更适应于不同类型的输入
-
同一算法,可能由不同程序员、用不同程序语言、经不同编译器生成
-
同一算法,可能实现并运行于不同的体系结构、操作系统.. 1.3 为了给出客观的评判,需要抽象出一个理想的平台或模型
2.图灵机
2.1 构成部件
-
Tape-依次均匀地划分为单元格,各存有某一字符,初始均为'#
-
Alphabet-字符的种类有限
-
Head
- 总是对准某一单元格,并可读取或改写其中的字符
- 每经过一个节拍,可转向左侧或右侧的邻格
-
State
2.2 转换函数
- Transition Function:(q, c; d, L / R, p)
-
若当前状态为q,且当前字符为c,则将当前字符改写为d,转向左 / 右侧邻格, 转入'p'状态
-
特别地,一旦转入约定的状态'h',则停机
-
从启动至停机,所经历的节拍数目,即可用以度量计算的成本,亦等于Head累计的移动次数(无量纲)
-
2.3 实例(Increase)
-
功能:将二进制非负整数加一
-
原理:全'1'的后缀,翻转为全'0',原最低位'0'或'#'翻转为'1'
-
代码
-
(<, 1; 0, L, <) -- 左行,1->0
-
(<, 0; 1, R, >) -- 掉头,0->1
-
(<, #; 1, R, >)-- 遇到其他状况,该语句不执行
-
(>, 0; 0, R, >) -- 右行
-
(>, #; #, L, h/<) //遇到其他状况停止
-
3.RAM
3.1 组成
3.2 语言
- 赋值操作
-
R\[i]<-c -
R\[i]<-R\[j] -
R\[i]<-R\[R\[j]] -
R\[R\[i]]<-R\[j] -
R\[i]<-R\[j]+R\[k] -
R\[i]<-R\[j]-R\[k]
-
- 条件语句
-
IF R\[i]=0 GOTO 1(如果R[i]=0则跳往语句1) -
IF R\[i]>0 GOTO 1 -
STOP
-
3.3 效率
-
与TM模型一样,RAM模型也是一般计算工具的简化与抽象,使我们可以独立于具体的平台,对算法的效率做出可信的比较与评判
-
算法的运行时间 = 算法需要执行的基本操作次数
-
T(n) = 算法为求解规模为n的问题,所需执行的基本操作次
3.4 实例(Ceiling Division)
-
功能:向下取整的除法,0 <= c,0 < d
-
算法:反复地从 R[0] = c 中,减去 R[1] = d,统计在下溢之前,所做减法的次数x
-
代码
-
[0] R[3] <-1
-
[1] GOTO 4
-
[2] R[2] <- R[2] + R[3]
-
[3] R[0] <- R[0] – R[1]
-
[4] IF R[0] > 0 GOTO 2
-
[5] R[0] <-R[2]
-
[6] STOP
-
-
效果
-
表的行数即所执行基本指令的总条数
三、渐进复杂度
1.大O记号
1.1 渐进分析
-
基本操作次数 T(n)
-
存储单元数 S(n)
1.2 Big-O notation
-
T(n)=O(f(n)) iff ∃c>0 当n>>2时,有T(n)<c*f(n)
-
实例:(当常数趋近于n时)
-
与T(n)相比,f(n)在形式上更为简洁,但依然反映前者的增长趋势
1.3 T(n)=Ω(f(n)) iff ∃c>0 当n>>2时,有T(n)<c*f(n)
1.4 T(n)=Θ(f(n)) iff ∃>>0 当n>>2时,有**·f(n)>T(n)>·f(n)**
2.多项式
2.1 常数O(1)
-
从渐近的角度来看,再大的常数,也要小于递增的变数
-
实例:2 = 2022 = 2022 x 2022 = O(1),即 = O(1)
-
这类算法的效率最高,即不含转向(循环,调用,递归等),必顺序执行
//循环
for(i=0;i<n;i+=n/2013+1)
for(i=1;i<n;i=1<<i)
//分支转向
if((n+m)*(n+m)<48n8m) goto UNREACHABLE
//(递归)调用
if(2==(n*n)%5) O1(n)
2.2 对数()
- 不注明底数原因
-
∀a,b>1,=·=O()
-
∀c>0,=c·=O()
-
123·=O() - 这类算法非常有效,复杂度无限接近于常数:∀c>0,=O()
-
2.3 多项式(O())
- 即只取最大的次方,其他皆可无视
2.4 线性
- 指所有O(n)类函数
3.指数(O())
3.1
3.2 实例
3.3 从,是从有效算法到无效算法的分水岭
4.层次分级
四、复杂度分析
1.级数
1.1 级数
-
算术级数:与末项平方同阶
-
幂方级数:比幂次高出一阶
-
几何级数:与末项同阶 ,1<a$
1.2 收敛级数
- 几何分布:,0<λ<1
1.3 不收敛,但有限
- 调和级数:h(n)=
- 对数级数:
- 对数 + 线性 + 指数:
2.迭代
2.1 迭代 + 算术级数
(该代码执行时间与图形面积相等)
for( int i = 0; i < n; i++ )
for( int j = 0; j < n; j++ )
O1op(const i, const j)
for( int i = 0; i < n; i++ )
for( int j = 0; j < i; j++ )
O1op(const i, const j)
2.2 迭代 vs 级数
for( int i = 1; i < n; i <<= 1 ) (<<为在二进制中向左移动一位,即乘2)
for( int j = 0; j < i; j++ )
O1op( const i, const j )
for( int i = 0; i < n; i++ )
for( int j = 0; j < i; j += 2022 )
O1op( const i, const j )
2.3 迭代 + 复杂级数
for( int i = 0; i <= n; i++ )
for( int j = 1; j < i; j += j )
O1op( const i, const j )
3.封底估算
- 1天=24hr x 60min x 60sec≈25 x 4000=sec
- 一生=一世纪=100yr x 365=3 x day=3 x sec
五、迭代与递归
1.减而治之
1.1 为求解一个大规模的问题,可以
1.2 递归跟踪:绘出计算过程中出现过的所有递归实例(及其调用关系)
- 它们各自所需时间之总和,即为整体运行时间
1.3 实例
sum( int A[], int n ) {
return n < 1 ? 0 : sum(A, n - 1) + A[n - 1];
}
总体运行时间为T(n)=O(1) x (n+1)=O(n) 1.4 对于大规模的问题、复杂的递归算法,递归跟踪不再适用此时可采用另一抽象的方法
- 从递推的角度看,为求解规模为n的问题(T(n)),需递归求解规模为n-1的问题(T(n-1)),再累加上(O(1))
- 递推方程:T(n)=T(n-1)+O(1),T(0)=O(1)
- 解:T(n)=T(n+2)+O(2)=T(n-3)+O(3)=...=T(0)+O(n)=O(n)
void reverse( int * A, int lo, int hi )(将数组中的区间A[lo,hi]前后颠倒)
//减治
Rev(lo,hi)=[hi]+Rev(lo+1,hi-1)=[lo]
//递归:
if (lo < hi) {
swap( A[lo], A[hi] );
reverse( A, lo + 1, hi – 1 );
}//线性递归(尾递归),O(n)
//迭代
while (lo < hi) swap( A[lo++], A[hi--] ); //亦是O(n)
2.分而治之
2.1 为求解一个大规模的问题,可以将其划分为若干子问题 (通常两个,且规模大体相当),分别求解子问题,由子问题的解,合并得到原问题的解
2.2 实例
sum( int A[], int lo, int hi ) {
if ( hi - lo < 2 ) return A[lo];
int mi = (lo + hi) >> 1; return sum( A, lo, mi ) + sum( A, mi, hi );
}
2.3 从递推的角度看,为求解sum(A, lo, hi),需要递归求解和(2*T(n/2)),进而将子问题的解累加(O(1))
- 递推方程:
- T(n)=2·T(n/2)+O(1)
- T(1)=O(1)
- 解:T(n)=4·T(n/4)+O(3)=8·T(n/8)+O(7)=16·T(n/16)+O(15)=...=n·T(1)+O(n-1)=O(n)
2.4 Master Theorem
-
分治策略对应的递推式,通常(尽管不总是)形如:T(n)=a·T(n/b)+O(f(n))(原问题被分为a个规模均为n/b的子任务;任务的划分、解的合并总共耗时f(n))
-
若
- 实例:
-
若
- 实例:
-
若
- 实例:
六、动态规划
1.记忆法-fib()
1.1 fib(n)=fib(n-1)+fib(n-2)
int fib(n) {
return (2 > n) ? n : fib(n - 1) + fib(n - 2);
}
1.2 复杂度:
T(0)=T(1)=1;T(n)=T(n-1)+T(n-2)+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()(ϕ=(1+)/2≈1.618->黄金分割率)
1.3 封底估算
1.4 递归版fib()低效的根源在于,各递归实例均被大量地重复调用。先后出现的递归实例,共计O()个;而去除重复之后,总共不过O(n)种
| O() | O(n) |
|---|---|
1.5 动态规划
- 颠倒计算方向: 由自顶而下递归,改为自底而上迭代
f = 1; g = 0; //fib(-1), fib(0)
while ( 0 < n-- ) {
g = g + f; f = g - f;
}
return g;
此时T(n) = O(n),而且仅需O(1)空间
2.最长公共子序列(LCS)
2.1 定义
2.2 **递归 ** 对于序列A[0,n)和B[0,m),LCS(n,m)无非三种情况
-
若 n = 0 或 m = 0,则取作空序列(长度为零)
-
若A[n-1] = 'X' = B[m-1],则取作:LCS(n-1,m-1) + 'X' [
unsigned int lcs( char const * A, int n, char const * B, int m ) {
if (n < 1 || m < 1) return 0;
else if ( A[n-1] == B[m-1] ) return 1 + lcs(A, n-1, B, m-1);
else return max( lcs(A, n-1, B, m), lcs(A, n, B, m-1) )
}
2.3 复杂度:每经一次比对,至少一个序列的长度缩短一个单位
-
最好情况,只需O(n+m)时间
-
然而最坏情况下,子问题数量不仅会增加,且可能大量雷同子任务LCS(A[a],B[b])重复的次数,可能多达为;特别地,LCS(A[0], B[0])的次数可多达
-
当n = m时,为Ω()
unsigned int lcsMemo(char const* A, int n, char const* B, int m) {
unsigned int * lcs = new unsigned int[n*m];
memset(lcs, 0xFF, sizeof(unsigned int)*n*m);
unsigned int solu = lcsM(A, n, B, m, lcs, m);
delete[] lcs;
return solu;
}
unsigned int lcsM( char const * A, int n, char const * B, int m, unsigned int * const lcs, int const M ) {
if (n < 1 || m < 1) return 0;
if (UINT_MAX != lcs[(n-1)*M + m-1]) return lcs[(n-1)*M + m-1];
else return lcs[(n-1)*M + m-1] = (A[n-1] == B[m-1]) ? 1 + lcsM(A, n-1, B, m-1, lcs, M) max( lcsM(A, n-1, B, m, lcs, M), lcsM(A, n, B, m-1, lcs, M) );
}
2.4 动态规划
unsigned int lcs(char const * A, int n, char const * B, int m) {
if (n < m) { swap(A, B); swap(n, m); }
unsigned int* lcs1 = new unsigned int[m+1];
unsigned int* lcs2 = new unsigned int[m+1];
memset(lcs1, 0x00, sizeof(unsigned int) * (m+1));
memset(lcs2, 0x00, sizeof(unsigned int) * (m+1));
for (int i = 0; i < n; swap(lcs1, lcs2), i++)
for (int j = 0; j < m; j++)
lcs2[j+1] = (A[i] == B[j]) ? 1 + lcs1[j] : max(lcs2[j], lcs1[j+1]);
unsigned int solu = lcs1[m];
delete[] lcs1;
delete[] lcs2;
return solu;
}