递归
基本概念
让我们来举一个例子:数学中的阶乘运算:
在这个方程中,我们可以看到,当n为0时,结果为一个确定值1,当n≠0时,结果为与n有关的递推方程式。我们称上面一个式子为边界条件,下面一个式子为递归方程。而边界条件和递归方程是递归函数的两个要素,只有具备了这两个要素,递归函数才能在有限次计算后得出结果。
让我们再来看几个例子加深一下对递归思想的理解吧!
斐波那契数列
斐波那契数列是一个有名的数列,该数列从第3项开始,每一项都等于前两项之和:1、1、2、3、5、8、13、21、34......
汉诺塔问题
设a、b、c是3个塔座。开始时,在塔座a上有n个圆盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1、2、...、n,现要求将塔座a上的这一叠圆盘移动到塔座b上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:
- 规则1:每次只能移动一个圆盘;
- 规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘之上;
- 规则3:在满足移动规则1和2的前提下,可将圆盘移动至a,b,c中任一塔座上。
总结一下,递归的优点是结构清晰、可读性强,而且容易用数学归纳法来证明算法的正确性,因此为算法设计带来了很大方便。但其缺点是递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法(迭代)要多。
递归和迭代
Q:所有的递归实现都可以转换为迭代实现吗?反过来呢?
A:
- 迭代转化为递归通常较简单。
二分搜索
选择排序
插入排序
- 递归转化为迭代时,要复杂很多。例如很难将汉诺塔问题转化为循环迭代。
- 从实际上说,所有的迭代可以转换为递归,但递归不一定可以转换为迭代。
生成排序
如已知X = {1,2,3}的全排列P(x),Y = {1,2,3,4}的全排列P(Y),则P(X)和P(Y)之间存在什么关系?
4123 4132 4213 4231 4312 4321
1423 1432 2413 2431 3412 3421
1243 1342 2143 2341 3142 3241
1234 1324 2134 2314 3124 3214
因此,得出n个元素的集合Y = {y1, y2, ..., yn}的全排列的递归式为:
- 算法的复杂度由if和else两部分决定,直觉上觉得是else起决定作用。
- 实际上输出语句执行了nn!次,其中n!代表排序的总数,而每个排序有n个输出
- 所以复杂度由if决定为是Θ(nn!)
(当n≥2时,for循环执行n次(+n),加上每一次递归调用函数(f(n-1)),所以执行次数总和为f(n) = nf(n-1) + n)
整数划分
一个正整数n可以表示成一个或多个正整数之和:n = n1 + n2 + ... + nk,其中n1 ≥ n2 ≥ ... ≥ nk ≥ 1,k ≥ 1。正整数n的这种表示称为正整数n的划分,显然,当n > 1时,n存在多种划分。整数划分问题就是求正整数n的不同划分的个数。
例如正整数6有以下11种不同的划分:
1+1+1+1+1+1;
2+2+2,2+2+1+1,2+1+1+1+1;
3+3,3+2+1,3+1+1+1;
4+2,4+1+1;
5+1;
6。
- 下面的方法是否可行?
1 + (n - 1),2 + (n - 2),3 + (n - 3),...,[n/2] + [n/2]
则:
p(n) = p(1) + p(n - 1) + p(2) + p(n - 2) + ... + p([n/2]) + p([n/2])
存在重复计算
- 我们换一种思路:对于任何一个正整数n,其本身就有一个边界条件是1 + 1 + ... + 1(n个1),我们用p(n,≤ 1)表示这个划分,也就是p(n,≤ 1)只包含小于等于1的划分的个数,即这些划分中所有的加数都小于等于1。这样,p(n,≤ 2)表示小于等于2的划分的个数,其由两部分组成:①小于等于1的划分p(n,≤ 1);②包含2的划分,用p(n,= 2)表示。即:
- p(n,≤ 2) = p(n,≤ 1) + p(n,= 2)
- p(n,≤ m) = p(n,≤ m - 1) + p(n,= m), m ≤ n
- 其中p(n,= m)所代表的划分看成:
- 由此可得:p(n,= m) = p(n - m, ≤m)
- p(n,≤ m) = p(n,≤ m - 1) + p(n - m, ≤m), m ≤ n
- 整数划分的最终递归式为:
时间复杂度
计算迭代次数
通常,程序的运行时间和程序的迭代次数成比例。因此计算程序的迭代次数就可以作为算法运行时间的指示器。这在很多算法中都可以得到应用,如查找、排序等等。
例:
分析:while迭代的次数是k + 1次(因为n ≥ 1可以写成n ≥ 2^0,运行过程n = 2^k -> 2^0),k = logn。在每次while循环里面for循环依次执行n,n/2,n/4,...,1次,所以,算法的时间复杂度为:
为什么在上面计算算法的时间复杂度时不考虑step6,而是只考虑step4呢?
如果同时考虑step4和step6,我们有
总结:使用计算迭代次数的技术来分析算法的时间复杂度时,只需要考虑最深层次的迭代。
频度分析
对于有些算法,计算迭代次数非常麻烦,有时甚至是不可能的。这个时候,可以使用频度分析。
如果算法中的一个元运算具有最高频度,所有其他元运算频度均在它的频度的常数倍内,则称这个元运算为基本运算。
例: 在MERGE算法中,赋值运算具有最大频度:
- 将A的每个元素移到B(语句5和8)
- 又将B的每个元素移到A(语句16) 所以算法复杂度为2n
最坏情况和平均情况分析
平均情况分析:通常考虑均匀分布情况下的复杂度。
如插入排序中,将第i个元素插入到前面已经排序好的数组中:
- 插入到第1个位置,比较i-1次
- 插入到其他位置(位置j),比较i-j+1次
- 平均比较次数为:
- 插入排序总的平均比较次数为:
- 插入排序最差的比较次数为:
复杂度的递归求解
在计算算法的复杂度时,经常会遇到算法的复杂度是一个递归表达式,所以需要求解这个递归式才能得出算法的复杂度。如对于某算法,设T(n)为输入规模为n的复杂度,其递归式为:
- 展开法:通过直接对递归的展开计算复杂度;
- 代入法:先猜测解的形式,再通过数学归纳法证明;
- 递归树方法:通过画递归树的方法来求解,因为这种方法适用性比较好,所以是最重要的方法;
- 主方法:针对T(n) = aT(n/b) + f(n)的形式,主方法给出了相关的规律,所以主方法是最简单的方法,但适用性较差。
展开法
展开法通过直接对递归的展开来计算复杂度,通常只能用于计算形式如T(n) = T(n - 1) + cn或者T(n) = T(n/2) + cn这种比较简单的递归式。
代入法
- 步骤:先猜测解的形式,再通过数学归纳法证明;
- 适用于对递归形式比较熟悉的情况;
- 代入法另外一用法是对展开法或者递归树法求得的复杂度,进行进一步确认。
这个例子告诉我们,对于复杂度的证明,可以通过任意一种符合复杂度的假设进行归纳证明,如要证明T(n) = Θ(n^2),可以假设T(m) = cm^2(m < n,下同),也可以假设T(m) = cm^2 + bm,或者T(m) = cm^2 + bm + d都可,但归纳到T(n)时,必须是一致的。
递归树方法
主方法
该递归式用主方法求得的复杂度T(n) = Θ(nlogn),但对上式通过递归树方法得T(n) = Θ(n(logn)^2)。
几种递归形式的分析