非递归算法和递归算法的效率分析

在对算法进行效率分析时,非递归算法和递归算法的分析表现出差异性。这里将分类逐一介绍。

非递归算法效率分析

在分析非递归算法时,我们可遵循以下通用方案:
(1) 决定用哪个(哪些)参数表示输入规模。
(2) 找出算法的基本操作(作为一个规律,这个基本操作总是位于算法的最内层循环中)。
(3) 检查基本操作的执行次数是否只依赖于输入规模。如果它还依赖于一些其他的特性,则最差效率、平均效率和最优效率(如果有必要)都需要分别研究。
(4) 建立一个算法基本操作执行次数的求和表达式(某些情况下,需要建立一个递推关系式)。
(5) 利用求和运算的标准公式和法则来建立一个操作次数的闭合公式,或者至少确定它的解的增长次数。
接下来将从实例出发,练习如何对非递归算法进行分析。

查找元素最大值问题

问题描述如下:从n个元素的列表中查找元素的最大值。这里假设列表使用一维数组实现。该问题对应算法的伪代码实现如下:

A[0...n-1]表示存储n个元素的列表
max表示元素中的最大值
max = A[0]
while i < n-1 do 
    if(A[i] > max) then
        max = A[i]
    i++
return max

在该算法中,使用数组元素的数量作为输入规模。算法中执行最频繁的操作在while循环中。循环体中包含两个操作:比较操作A[i] > max和赋值操作max=A[i]。由于赋值操作是在比较操作结果为true时才执行,所以比较操作是该算法的基本操作。
对于输入规模n来说,比较次数不会因元素的位置而变化,所以该算法的基本操作不依赖其他特性,也就没必要进一步求最差效率、平均效率或最优效率。
如果使用 C ( n ) C(n) C(n)表示比较运算的执行次数,由于该算法每循环一次,就进行一次比较操作,所以其求和表达式为:
C ( n ) = ∑ i = 1 n − 1 1 C(n) = \sum_{i=1}^{n-1}1 C(n)=i=1n11

显然,上述表达式的结果为 n − 1 n-1 n1

元素唯一性问题

问题描述:验证给定数组中的N个元素是否都唯一。该问题对应算法的伪代码实现如下:

A[0...n-1]表示存储n个元素的列表
for i=0 to n-2 do 
    for j = i+1 to n-1 do
        if(A[i] = A[j]) then
            return false
return true

在该算法中,使用数组元素的数量作为输入规模。算法中执行最频繁的操作在内层for循环中,对应的基本操作是比较操作:A[i] = A[j]。对于输入规模n来说,比较次数会因元素的位置而变化,所以该算法的基本操作依赖其他特性,有必要进一步求最差效率、平均效率或最优效率。这里仅研究最差效率。
最坏的情况,就是遍历到第N-1个元素,才能确定元素是否唯一。这样,其算法的求和表达式为:
C ( n ) = ∑ i = n − 1 1 i C(n) = \sum_{i=n-1}^{1}i C(n)=i=n11i
通过计算可知,其结果为 ( n − 1 ) ∗ n / 2 (n-1)*n/2 (n1)n/2

递归算法效率分析

在分析递归算法时,我们可遵循以下通用方案:
(1) 决定用哪个(哪些)参数表示输入规模。
(2) 找出算法的基本操作(对于递归算法,其基本操作无法通过最内层循环定位)。
(3) 检查基本操作的执行次数是否只依赖于输入规模。如果它还依赖于一些其他的特性,则最差效率、平均效率和最优效率(如果有必要)都需要分别研究。
(4) 建立一个算法基本操作执行次数的递推关系式(递推)以及相应的初始条件(回归)
(5) 求解这个递推关系式,或者至少确定它的解的增长次数。

阶乘问题

问题描述:对于任意非负整数 n n n,计算阶乘函数 F ( n ) = n ! F(n)=n! F(n)=n!
因为当 n ≥ 1 n≥1 n1时,有: n ! = 1 ∗ 2 ∗ . . . ∗ ( n − 1 ) ∗ n n!=1*2*...*(n-1)*n n!=12...(n1)n,又因为 0 ! = 1 0!=1 0!=1,所以可以得到递归式: F ( n ) = F ( n − 1 ) ∗ n F(n)=F(n-1)*n F(n)=F(n1)n。对应伪代码实现如下:

if n = 0  
    return 1
else 
    reutrn F(n-1) * n

在该算法中,使用 n n n作为输入规模。该算法的基本操作是乘法(F(n-1)*n)。这里把乘法的执行次数记为 M ( n ) M(n) M(n)
因为当 n ≥ 1 n≥1 n1时, F ( n ) = F ( n − 1 ) ∗ n F(n)=F(n-1)*n F(n)=F(n1)n,所以 M ( n ) = M ( n − 1 ) + 1 M(n)=M(n-1) + 1 M(n)=M(n1)+1。其中 M ( n − 1 ) M(n-1) M(n1) F ( n − 1 ) F(n-1) F(n1)计算所需执行乘法次数,常数 1 1 1表示将 F ( n − 1 ) F(n-1) F(n1)乘以 n n n
注意,满足这个递推式的序列有无数个,因为我们并没有限定n的起始值。所以,为了确定一个唯一解,这里确定下初始条件。对于 F ( n ) F(n) F(n)来说,其递推回归的条件是:
i f n = 0 t h e n r e t u r n 1 if n = 0 then return 1 ifn=0thenreturn1。所以,对应 M ( n ) M(n) M(n)的初始条件是: M ( n ) = 0 M(n)=0 M(n)=0,表示当n=0时,不做乘法操作。这样, M ( n ) M(n) M(n)的递归式可以表示为:
当 n > 0 时 , M ( n ) = M ( n − 1 ) + 1 当n>0时,M(n) = M(n-1) + 1 n>0M(n)=M(n1)+1
M ( 0 ) = 0 M(0)=0 M(0)=0
接下来求解这个递归关系式。这里使用一种称为反向替换法(method of backward substitution)。针对上述递归式,其计算过程如下:
M ( n ) = M ( n − 1 ) + 1 M(n) = M(n-1) + 1 M(n)=M(n1)+1
M ( n ) = [ M ( n − 2 ) + 1 ] + 1 = M ( n − 2 ) + 2 M(n) = [M(n-2) + 1] + 1 = M(n-2) + 2 M(n)=[M(n2)+1]+1=M(n2)+2
M ( n ) = [ [ M ( n − 3 ) + 1 ] + 1 ] + 1 = M ( n − 3 ) + 3 M(n) = [[M(n-3) + 1] + 1] + 1 = M(n-3) + 3 M(n)=[[M(n3)+1]+1]+1=M(n3)+3
根据数学归纳法可知, M ( n ) = M ( 0 ) + n = n M(n) = M(0) + n = n M(n)=M(0)+n=n。也就是说,对于输入规模n,需要执行n次乘法操作。

汉诺塔(Tower of Hanoi)问题

问题描述:有n个不同大小的盘子和3根木桩。一开始,所有的盘子都按照大小顺序套在第1根木桩上,最大的盘子在底部,最小的盘子在顶部。我们的目的是把所有的盘子都移到第3根木桩上,在必要的时候,可以借助第2根木桩。我们每次只能移动一个盘子,但是不能把较大的盘子放在较小的盘子的上面。
该问题的递归算法的伪代码实现如下:

把n-1个盘子递归的从木桩1移动到木桩2(借助木桩3)  
把第n个盘子从木桩1移动到木桩3  
把n-1个盘子递归的从木桩2移动到移到3(借助木桩1)  

在该算法中,使用移动次数 n n n作为输入规模。盘子的移动作为该算法的基本操作。将n个盘子的移动次数定义为 M ( n ) M(n) M(n)。对于 M ( n ) M(n) M(n),有下列递推等式: 当 n > 1 时 , M ( n ) = M ( n − 1 ) + 1 + M ( n − 1 ) 当n>1时,M(n) = M(n-1) + 1 + M(n-1) n>1M(n)=M(n1)+1+M(n1) M ( 1 ) = 1 M(1) = 1 M(1)=1使用反向替换法求解这个递归式:
M ( n ) = M ( n − 1 ) + 1 + M ( n − 1 ) M(n) = M(n-1) + 1 + M(n-1) M(n)=M(n1)+1+M(n1)
M ( n ) = 2 ∗ M ( n − 1 ) + 1 M(n) = 2*M(n-1) + 1 M(n)=2M(n1)+1
M ( n ) = 2 ∗ [ 2 ∗ M ( n − 2 ) + 1 ] + 1 = 2 2 ∗ M ( n − 2 ) + 2 + 1 M(n) = 2*[2*M(n-2) + 1] + 1 = 2^2*M(n-2) + 2 + 1 M(n)=2[2M(n2)+1]+1=22M(n2)+2+1
M ( n ) = 2 ∗ [ 2 ∗ [ 2 ∗ M ( n − 3 ) + 1 ] + 1 ] + 1 = 2 2 ∗ M ( n − 3 ) + 2 2 + 2 + 1 M(n) = 2*[2*[2*M(n-3) + 1] + 1] + 1 = 2^2*M(n-3) + 2^2 + 2 + 1 M(n)=2[2[2M(n3)+1]+1]+1=22M(n3)+22+2+1
根据数学归纳法,可知: M ( n ) = 2 i M ( n − i ) + 2 i − 1 + . . . + 2 + 1 M(n) = 2^iM(n-i) + 2^{i-1} + ... + 2 + 1 M(n)=2iM(ni)+2i1+...+2+1。计算后i项,可得: M ( n ) = 2 i M ( n − i ) + 2 i − 1 M(n) = 2^iM(n-i) + 2^i - 1 M(n)=2iM(ni)+2i1。当i = n-1时,上述表达式可表示为: M ( n ) = 2 n − 1 M ( 1 ) + 2 n − 1 − 1 M(n) = 2^{n-1}M(1) + 2^{n-1} - 1 M(n)=2n1M(1)+2n11。又因 M ( 1 ) = 1 M(1) = 1 M(1)=1,所以其结果为 M ( n ) = 2 n − 1 M(n) = 2^n - 1 M(n)=2n1
一般地,如果一个递归算法会不止一次的调用自身,处于分析的目的,构造一棵它的递归调用树是很用的。在这棵树中,节点相当于递归调用,可以用递归调用参数的值作为节点的标记。对于汉诺塔来说,它的递归调用树表示如下:
非递归算法和递归算法的效率分析_第1张图片
可见,盘子移动的次数,就是上述调用树中节点总数。由于这里是二叉完整树,所以其值为 2 n − 1 2^n-1 2n1。其结果和使用反向替换法求解递推式的结果一致。
力扣对应算法练习,可参考链接。

参考

《算法设计与分析基础》 第三版 Anany Levitin 著 潘彦 译
https://blog.csdn.net/qq_19446965/article/details/81591945 汉诺塔的图解递归算法

原创不易,如果本文对您有帮助,欢迎关注我,谢谢 ~_~

你可能感兴趣的:(数据结构和算法,算法)