一、引论
运行时间。 在许多问题当中,一个重要的观念是:写出一个可以工作的程序并不够。如果这个程序在巨大的数据集上运行,那么运行时间就变成了重要的问题。分析方法。 数据结构分析中的结论的两个最常用的方法是归纳法和反证法。归纳法有两个标准的部分:基准情形与归纳假设。反证法即举出一个反例。递归的四条法则。 基准情形:必须有某些基准的情形,它们不用递归就能求解;不断推进:对于那些需要递归求解的情形,递归调用必须能够朝着产生基准情形的方向推进;设计法则:假设所有的递归调用都能运行;合成效益法则:在求解一个问题的同一实例时,切勿在不同的递归调用中做重复性工作。二、算法分析
数学基础T(N)=O(f(N)). T(N)相对增长率小于或等于f(N)。 T(N)=Ω(g(N)).T(N)相对增长率大于或等于g(N)。 T(N)=θ(h(N)).T(N)相对增长率等于h(N)。 T(N)=o(p(N)).T(N)相对增长率小于p(N)。(不含等于)
大O分析中常数项可以弃掉,低阶项可以忽略。
使用洛必达法则进行分析 (lim)┬(N→∞) f(N)/g(N) : 极限为0:f(N)=o(g(N));极限为c≠0:f(N)=θ(g(N)); 极限为∞:g(N)=o(f(N));极限摆动:二者无关。
典型增长率: 常数c 对数级logN 对数平方级(logN)^2 线性级N NlogN 平方级N^2 立方级N^3 指数级2N 对数级增长十分缓慢,慢于线性级
计算模型模型机做任意一件简单的工作都恰好花费一个时间单元并且有无限内存。
运行时间for循环:内部语句运行时间乘以迭代次数; 嵌套的for循环:所有for循环大小乘积;
for(i=0;i<N;i++) for(j=0;j<N;j++) k++; //运行时间为O(N2)。顺序语句:取最高阶,省去常数系数与低阶项;
if/else语句:判断时间+max(if,else)。
递归使用尽量避免重复工作 long int Fib(int N) { if(N<=1) return 1; else return Fib(N-1)+Fib(N-2); } T(N)为Fib(N)的运行时间,则存在T(N)=T(N-1)+T(N-2)+2。其中2为判断时间和加法运算时间。 T(N)>=Fib(N)>=(3/2)^n,因而程序呈指数速度增长。其违反了合成效益法则,是“计算任何事情不要超过一次”的最好实例。 最大子序列和算法1(比较所有可能和):
int MaxSubsequenceSum(const int A[ ],int N) { int ThisSum, MaxSum, i, j, k; MaxSum=0; for(i) } 得运行时间为O(N^3),可看出第五行第六行计算过分耗时了。算法2(去重复):
int MaxSubSequenceSum(const int A[],int N) { int ThisSum,MaxSum,i,j; MaxSum=0; for(i=0;i<N;i++) { ThisSum=0; for(j=i;j<N;j++) { ThisSum+=A[j]; if(ThisSum>MaxSum) MaxSum=ThisSum; } } return MaxSum; } 可得运行时间为O(N^2)。 第一遍循环从全部序列中找子序列,和最大作为MaxSum; 第二遍循环将从去掉第一个数字的序列所有子序列和与前者比较,更大的子序列和作为MaxSum…最终找到最大子序列和。算法3(分治):
static int MaxSubSum(const int A[],int Left,int Right) { int MaxLeftSum,MaxRightSum; int MaxLeftBorderSum,MaxRightBorderSum; int LeftBorderSum,RightBorderSum; int Center,i; //基准情形 if(Left==Right) if(A[Left]>0) return A[Left]; else return 0; //求左右最大子序列和 Center=(Left+Right)/2; MaxLeftSum=MaxSubSum(A,Left,Center); MaxRightSum=MaxSubSum(A,Center+1,Right); //求跨中间最大子序列和(两边界和最大) MaxLeftBorderSum=0; LeftBorderSum=0; for(i=Center;i>=Left;i--) { LeftBorderSum+=A[i]; if(LeftBorderSum>MaxLeftBorderSum) MaxLeftBorderSum=LeftBorderSum; } MaxRightBorderSum=0; RightBorderSum=0; for(i=Center;i<=Right;i--) { RightBorderSum+=A[i]; if(RightBorderSum>MaxRightBorderSum) MaxRightBorderSum=RightBorderSum; } return Max3(MaxLeftSum,MaxRightSum,MaxLeftBorderSum+MaxRightBorderSum); } //主函数 int MaxSubsequenceSum(const int A[],int N) { return MaxSubSum(A,0,N-1); } 递归形式处理(所有递归调用都能运行)。 最大子序列和可能出现在三处:左半部、右半部、跨越中间。 基准情形,即开头为正则返回原值,为负则返回零。因为如果开头为负,总能将其去掉使得序列和最大。 计算两边最大子序列花费时间为2*T(N/2),计算跨越中间最大序列花费时间O(N)。 所以T(N)=2T(N/2)+O(N)=O(NlogN)算法4(联机算法):
int MaxSubsequenceSum(const int A[],int N) { int ThisSum,MaxSum,j; ThisSum=MaxSum=0; for(j=0;j<N;j++) { ThisSum+=A[j]; if(ThisSum>MaxSum) MaxSum=ThisSum; else if(ThisSum<0) ThisSum=0; } return MaxSum; } 加和过程中只要小于零就重置,大于零留下。 该算法优点为它只对数据进行一次扫描,一旦完成对A[i]的读入和处理,就不再需要记忆它。 联机算法仅需要常量空间并以线性时间运行,几乎完美的算法。 联机直接使用已给顺序,脱机改变顺序恰当后进行。 运行时间中的对数一般法则:如果一个算法用常数时间(O(1))将问题的大小削减位其一部分,通常为1/2,那么该算法就是O(logN)的。另一方面,如果是用常数时间只是把问题减少一个常数,那么该算法就是O(N)的。
①对分查找,也叫作二分查找、折半查找。
例:给定一个整数X和整数序列A0,A1,A2,……,AN-1。 后者已经预先排序并在内存中,在序列中寻找整数X,返回下标i。 解法一 从左到右进行扫描,显然运行花费线性时间。没有用到序列已经排序的事实。 解法二 与居中元素相比较,如果大于,与右侧子序列居中元素比较,如果小于则比较左侧。 int BinarySearch(const ElementType A[],ElementType X,int N) { int Low,Mid,High; Low=0;High=N-1; while(Low<=High) { Mid=(Low+High)/2; if(A[Mid]<X) Low=Mid+1; else if(A[Mid]>X) High=Mid-1; else return Mid; } return NotFound; } 对分查找提供了在O(logN)时间内的Find查找操作数据结构实现方法。 其他所有操作尤其是Insert插入操作均需要O(N)的时间。②欧几里得算法
两个整数的最大公因数是同时整除二者的最大整数。 unsigned int Gcd(unsigned int M,unsigned int N) { unsigned int Rem; while(N>0) { Rem=M%n; M=N; N=Rem; } return M; } 算法连续计算余数直到余数是0为止,最后的非零余数就是最大公因数。 事实上,在一次迭代中余数并不按照一个常数因子递减。 然而,我们可以证明,在两次迭代以后,余数最多是原始值的一半。 这就证明了迭代次数至多是2logN=O(logN),从而得到运行时间。③幂运算
long int Pow(long int X,unsigned in N) { if(N==0) return 1; if(N==1) return X; if(IsEven(N)) return Pow(X*X,N/2); else return Pow(X*X,N/2)*X; } 将N次幂的N次相乘变为最多需要2logN次相乘。 检验你的分析确定是否最优:编程并比较实际的观察运行时间与分析的运行时间是否相匹配。 分析结果准确性:平均情形的分析十分复杂,一般最坏情形的界尽管过分悲观但却是最好的已知结果分析。
分享的内容作为自己的学习记录,同时也希望可以帮助到大家。
水平有限,讲解不详细的地方可以留言评论,我看到会及时回复进行交流;文中存在的错误还请大佬进行斧正!
书中内容大多来自数据结构与算法分析 C语言描述(机械工业出版社)一书,侵权联删。