几乎所有的算法,对于规模更大的输入都需要运行更长的时间。例如,需要更多时间来对更长的数组排序,更大的矩阵相乘也需要花费更多时间,等等。所以,使用一个以算法输入规模式n为参数的函数,来研究算法效率是非常合乎逻辑的。
统计算法每一步操作的执行次数——不可行。
统计算法中最重要的操作—基本操作的执行次数。
执行次数C(n)是输入规模n的函数,算法运行时间T(n)是执行次数的函数:T(n) ≈ copC(n)
其中: cop为特定计算机上一个基本操作的执行时间,是常量。
为什么对于大规模的输入要强调执行次数的增长次数呢?这是因为小规模输入在运行时间上差别不足以将高效的算法和低效的算法法区分开来
定义1 我们把函数t(n)包含在O(g(n)) 中,记作t(n) ∈ O(g(n)) ;它的成立条件是:对于所有足够大的n, t(n) 的上界由g(n)的常数倍数所确定,也就是说,存在大于0的常数c和非负的整数n0,使得:
定义2 我们把函数t(n)包含在Ω(g(n))中,记作t(n)∈Ω(g(n)),它的成立条件是:对于所有足够大的n, t(n)的下界由g(n)的常数倍所确定,也就是说,存在大于0的常数c和非负的整数n0,使得:
定义 3我们把函数t(n)包含在Θ(g(n)) 中,记作t(n) ∈ Θ(g(n)) ;它的成立条件是:对于所有足够大的n, t(n) 的上界、下界都由g(n)的常数倍数所确定,也就是说,存在大于0的常数c1,c2和和非负的整数n0,使得:
虽然符号O, Ω和Θ的正式定义对于证明它们的抽象性质是不可缺少的,但我们很小直接用它们来比较两个特定函数的增长次数。有一种较为简便的比较方法,它是基于对所计论的两个函数的比率求极限。有3种极限情况会发生:
对规模较小的问题,决定算法工作效率的可能是算法的简单性而不是算法执行的时间
当比较两个算法的效率时,若两个算法是同阶的,必须进一步考察阶的常数因子才能辨别优劣。
考虑一下从n个元素的列表中查找元素最大值的问题.简单起见,我们假设列表是用数组实现的。下面给出一个解决问题的标准算法的伪代码。
算法 MaxElement(A[0..n-1])
//求给定数组中最大元素的值
//输入:实数数组A[0..n-1]
//输出:A中最大元素的值
maxval←A[0]
for i←1 to n-1 do
if A[i]>maxval
maxval←A[i]
return maxval
确定基本操作:是赋值运算还是比较运算?
把C(n)记作比较运算的执行次数,并试图寻找一个公式将它表达为规模n的函数。:
下面这个简单直接的算法可以解决该问题。
算法 UniqueElements(A[0..n-1])
//验证给定数组中的无素是否全部惟一
//输入:数组A[0..n-1]
//输出:如果A中的元素全部惟一,返回“true”
// 否则,返回“false”.
for i←0 to n-2 do
for j←i+1 to n-1 do
if A[i]=A[j] return false
Return true
这个结果是完全可以预测的:在最坏的情况下,对于n个元素的所有n(n-1)/2对两两组合,该算法都要比较一遍。
算法伪代码:
MaxtrixMultiplication(A[0..n-1,0..n-1],B[0..n-1,0..n-1])
for i=0 to n-1 do
for j=0 to n-1 do
C[i,j]=0.0
for k=0 to n-1 do
C[i,j]=C[i,j]+A[i,k]*B[k,j]
return C
分析:
对于任意非负整数n,计算阶乘函数F(n)=n!的值。因为
当n≥1时,n!=1·…·(n-1)·n=(n-1)!·n
并且根据定义,0!=1,我们可以使用下面的递归算法 计算F(n)=F(n-1)·n
算法 F(n)
//递归计算n!
//输入:非负整数n
//输出:n!的值
if n=0 return 1
else return F(n-1)*n
我们用n本身来指出算法的输入规模(而不是它的二进制表示的比特数)。该算法的基本操作是乘法,我们把它的执行次数记作M(n)。因为函数F(n)的计算是根据下面公式:
当n>0时,F(n)=F(n-1)*n
所以,计算这个公式时,用到的乘法数量M(n)需要满足这个等式:
当n>0时,M(n)=M(n-1)+1
的确,计算F(n-1)需要用M(n-1)次乘法,还有一次乘法用来把该结果乘法n。为了确定一个惟一解,我们还需要一初始条件来告诉我们该序列的起始值。为了得到这个起始值,我们可以观察该算法停止递归调归调用时的条件:if n=0 return 1 所以,我们所遵循的初始条件是:
M(0)=0
这样,我们成功地建立了关于该算法的乘法次数M(n)的递推关系和初始条件:
当n>0时,M(n)=M(n-1)+1
M(0)=0
最终结果为 M(n)=M(n-1)+1=…=M(n-i)+i=…=M(n-n)+n=n
盘子移动时必须遵守以下规则:
分析:
输入规模:盘子的数量
记M(n) 为移动盘子的次数,则递归关系式
M(n)=M(n-1)+1+M(n-1)
M(1)=1
解该递归关系可得
M(n)=2n-1
这是个指数级的算法,是算法不好吗?
对这个问题而言,它是一个高效的算法.
斐波那契数列—0,1,1,2,3,5,8,13,21,34,…
这个数列可以用一个简单的递推式和两个初始条件来定义:
当n>1时,F(n)=F(n-1)+F(n-2)
F(0)=0,F(1)=1
算法 F(n)
//根据定义,递归计算第n个斐波那契数
//输入:一个非负整数n
//输出:第n个斐波那契数
if n≤1 return n
else return F(n-1)+F(n-2)
该算法的基本操作很明显是加法,我们把A(n)定义为这个算法在计算F(n)的过程中所做的加法次数。因而,计算F(n-1)和F(n-2)所需要的加法次数分别是A(n-1)和A(n-2),而该算法还需要做一次加法来计算它们的和。因此,对于A(n)我们有下面的递推式:
当n>1时,A(n)=A(n-1)+A(n-2)+1
从递推式中,我们可以预计到该算法的效率不高。的确,它包含两个递归调用,而这两个调用的规模仅比n略小一点。通过观察该算法的递归调用树,我们也能发现该算法效率低下的原因。相同的函数值被一遍一遍地重复计算,这很明显是一种效率低下的做法。
通过简单地对斐波那契数列的连续元素进行迭代计算,我们得到了一个快得多的算法,就像下面的这个算一样:
算法 Fib(n)
//根据定义,迭代计算第n个斐波那契数
//输入:一个非负整数n
//输出:第n个斐波那契数
F[0]←0;F[1]←1
for i←2 to n do
F[i]←F[i-1]+F[i-2]
return F(n)
很明显,这个算法要做n-1次加法运算。所以,它和n一样都是线性函数,“仅在”作为n的二进制位数的函数时,才表现为指出级函数。注意,没有必要特意使用一个数组在存储斐波那契数列的前面元素:为了完成该任务,只需要存储两个元素就足够了。