《数据结构与算法分析》第二章:算法分析
算法是为求解一个问题需要遵循的、被清楚地指定的简单指令的集合。
本章内容:如何估算一个程序所需要的时间,有效降低时间,递归的粗心使用后果,最大子序列问题
一.数学基础:估计算法资源(时间)消耗所需分析
(1)四个基本定义:
定义一:如果存在正常数c和n0使得当N>=n0时,T(N)<=cf(N),则记为T(N)=O(f(N))。
定义二:如果存在正常数c和n0使得当N>=n0时,T(N)>=cg(N),则记为T(N)=Ω(g(N))。
定义三:T(N)=Θ(h(N))当且仅当T(N)=O(h(N))且T(N)=Ω(g(N))。
定义四:如果T(N)=O(p(N))且T(N)≠Θ(p(N)),则T(N)=o(p(N))。
(2)相应解释及重要结论(考察目标是T(n))
解释:定义一称为大O记法,表示T(N)的增长率小于等于f(N)的增长率,f(N)是T(N)的一个上界;
第二个定义表示T(N)的增长率大于等于g(N)的增长率,g(N)是T(N)的一个下界;
第三个定义表示T(N)的增长率等于h(N)的增长率;
第四个定义表示T(N)的增长率小于p(N)的增长率。
重要结论:
法则一:
如果T1(N)=O(f(N))且T2(N)=O(g(N)),那么
(a)T1(N) + T2(N)=max(O(f(N)),O(g(N))),
(b)T1(N) * T2(N)=O(f(N) * g(N))。
法则二:
如果T(N)是一个k次多项式,则T(N) = Θ(N^k)。(N^k表示N的k次方)
法则三:
对任意常数k,(logN)^k = O(N)。可知,对数增长得非常缓慢,不管其是多少次方。
(3)洛必达法则
通过计算极限lim f(N)/g(N)(当N趋于无穷)来确定两个函数f(N)和g(N)的相对增长率,必要的时候使用洛必达法则。
极限是0:意味着T(N)=O(g(N));
极限是c≠0:意味着T(N)=Θ(g(N));
极限是∞:意味着T(N)=o(g(N));
极限摆动:二者无关。
二.计算模型与问题分析
计算模型:假设模型基于一台标准计算机,有标准的简单指令系统,且假设模型机做一件简单的工作都恰好话费一个时间单元。
问题分析:运行时间通常是要分析的最重要的资源,且通常需要分析算法在所需要的量是最坏情况下的运行时间,即计算大O运行时间(上界)。
大O计算的常用法则:
法则一:for循环中,依次for循环的运行时间至多是该for循环内语句(包括测试语句)的运行时间乘以迭代次数;
法则二:嵌套的for循环,由里向外分析,在一组嵌套循环内部的一条语句总的运行时间为该语句的运行时间乘以改组所有的for循环的大小的乘积;
法则三:顺序语句,各语句运行时间求和即可;
法则四:if/else语句,一个if/else语句的运行时间不会超过判断再加上if和else内部运行时间长者的总的运行时间。
计算任何事情不要超过一次。
三.最大子序列求和问题(重点不在于程序,在于时间分析过程)
(1)问题描述
给定一组整数(可能有负数),求其中某个连续子序列的和的最大值。
如:输入-2,11,-4,13,-5,-2,答案为20(子序列为11,-4,13)。
(2)算法一:运行时间O(N^3)--穷举式地尝试所有的可能,依次从第一个元素开始,计算所有子序列,比较其子序列的和,一直循环到最后。
int MaxSubsequenceSum(const int A[],int N) //最大子序列求和
{
int ThisSum,MaxSum,i,j,k;
MaxSum = 0;
for(i=0;i<N;i++)
for(j =i;j<N;j++)
{
ThisSum = 0;
for(k=i;k<=j;k++)
{
ThisSum += A[ k ];
}
if(ThisSum>MaxSum)
MaxSum = ThisSum;
}
return MaxSum;
}
(3)算法二:运行时间O(N^2)--通过撤除一个for循环来避免立方运行时间。
int MaxSubsequenceSum(const int A[],int N) //最大子序列求和
{
int ThisSum,MaxSum,i,j,k;
MaxSum = 0;
for(i=0;i<N;i++)
{
ThisSum = 0;
for(j=i;j<N;k++)
{
ThisSum += A[ j ];
}
if(ThisSum>MaxSum)
MaxSum = ThisSum;
}
return MaxSum;
}
(4)算法三:运行时间O(NlogN)--分治策略,把问题分成两个大致相等的子问题,递归求解,然后将两个子问题的解合并到一起并可能字做些少量的附加工作,最后得到整个问题的解。
进一步分析:最大子序列可能出现在三个地方,整个出现在前半部分,整个出现在后半部分或者跨越输入数据的中间部分从而占据左右两半。前两种情况可用递归求解,后一种情况可以求出前半部分中包含最后一个元素的最大和与后半部分包含第一个元素的最大和而得到。
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) //Base case
if(A[Left] > 0)
return A[Left];
else
return 0;
Center = (Left+Right)/2; //call recursion
MaxLeftSum = MaxSubSum(A,Left,Center);
MaxRightSum = MaxSubSum(A,Center+1,Right);
MaxLeftBorderSum = 0 ; LeftBorderSum = 0; //MaxLeftBorderSum
for(i = Center;i>=Left;i++)
{
LeftBorderSum +=A[ i ];
if(LeftBorderSum>MaxLeftBorderSum)
MaxLeftBorderSum = LeftBorderSum;
}
MaxRightBorderSum = 0 ; RightBorderSum = 0; //MaxRightBorderSum
for(i = Center+1;i<=Left;i++)
{
RightBorderSum +=A[ i ];
if(RightBorderSum>MaxRightBorderSum)
MaxRightBorderSum = RightBorderSum;
if(MaxLeftSum>=MaxRightSum)
if(MaxLeftSum>=MaxLeftBorderSum+MaxRightBorderSum)
return MaxLeftSum;
else
return MaxLeftBorderSum+MaxRightBorderSum;
else
if(MaxRightSum>=MaxLeftBorderSum+MaxRightBorderSum)
return MaxRightSum;
else
return MaxLeftBorderSum+MaxRightBorderSum;
}
}
int MaxSubsequenceSum(const int A[ ],int N) //最大子序列求和
{
return MaxSubSum(A,0,N-1);
}
递归程序分析:第一小段为变量定义;第二小段为递归的基准情形,当Left=Right时,表明只有一个元素,当该元素为正时返回正数,为负时返回零;第三小段主要是递归调用,不断的推进过程;第四和五段,是计算达到中间分解出处的两个最大和的和数;最后,第六段程序返回三个可能最大和中的最大者。调试未通过,后更(2017.6.12)
(5)算法四:运行时间O(N),简单有效,对数据只进行一次扫描,记录最大的和,时刻与当前的和比较并更新。绝妙的地方在于:当当前的和为负时,重置,即重当前开始计算和,不会影响后续和的计算。在任意时刻,该算法都能对它已经读入的数据给出子序列问题的正确答案,具有这种特性的算法叫做联机算法。
仅需要常量空间并以线性时间运行的在线算法几乎是完美的算法。
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;
}
四.运行时间中的对数:(重点不在于程序,在于时间分析过程)
注:如果一个算法用常数时间(O(1))将问题的大小削减为其一部分(通常是1/2),那么该算法就是O(logN)。另一方面,如果使用常数时间只是把问题减少一个常数,那么这种算法就是O(N)的。下面提供具有对数特点的三个例子。
(1)对分查找:给定一个整数X和和一组预先排序的整数列,求整数列中X的下标i(数列下标从0开始),如果X不在数列中,这返回i=-1。
int BinarySearch(const ElementType A[],ElementType X,int N)
{
int Low,Mid,High;
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; //found
}
return NotFound; //NotFound is defined as -1
}
(2)计算最大公因数(欧几里得算法):两个整数的最大公因数(Gcd)是同时整除二者的最大整数。不断取余。
unsigned int Gcd (unsigned int M,unsigned ing N)
{
unsigned int Rem;
while(N>0)
{
Rem = M%N;
M = N;
N= Rem;
}
return M;
}
(3)幂运算:处理一个整数的幂。使用递归算法,先给出基准情形,极其高效。
long int Pow(long int X,unsigned int 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;
}
五.检验分析结果,判断分析结果的准确性
分析进行后,一种检验方法是编程并比较实际观察到的运行时间与通过分析描述的运行时间是否相匹配。
经验指出,有时分析会估计过大,此时需要分析得更细。