在计算机程序中,描述迭代的一种方法是使用循环,另一种完全不同的迭代实现方法就是递归。
阶乘函数(通常表示为n!)是一个经典的数学函数,它有一个固有的递归定义。
** 英式标尺具有的递归模式是分形结构的一个简单例子。**
二分查找是最重要的计算机算法之一。在一个拥有数十亿以上条目的数据集中,它能让我们有效的定位所需的某个值。
计算机的文件系统有一个递归结构,在该结构中,目录能够以任意深度嵌套在其他目录上。递归算法被广泛勇于探索和管理这些文件系统。
说明性的例子
阶乘函数
def factorial(n):
if n == 0:
return 1
else:
return n* factorial(n-1)
绘制英式标尺
def draw_line(tick_length,tick_label=''):
line = '-'* tick_length
if tick_label:
line += ' '+tick_label
print(line)
def draw_interval(center_length):
if center_length > 0:
draw_interval(center_length-1)
draw_line(center_length)
draw_interval(center_length-1)
def draw_ruler(num_inches,major_length):
draw_line(major_length,'0')
for j in range(1,1+num_inches):
draw_interval(major_length-1)
draw_line(major_length,str(j))
draw_ruler(1,5)
----- 0
-
--
-
---
-
--
-
----
-
--
-
---
-
--
-
----- 1
二分查找
def binary_search(data,target,low,high):
if low > high:
return False
else:
mid = (low+high)/2
if target == data[mid]:
return True
elif target < data[mid]:
return binary_search(data,target,low,mid-1)
else:
return binary_search(data,target,mid+1,high)
文件系统
我们考虑这样一个算法:计算嵌套在一个特定目录中的累计磁盘空间
import os
def dist_usage(path):
total = os.path.getsize(path) # 返回由字符串路径(如:/usr/rt/courses)标识的文件或者目录使用的即时磁盘空间大小
if os.path.isdir(path):
for filename in os.listdir(path):
childpath = os.path.join(path,filename)
total += dist_usage(childpath)
print('{0:<7}'.format(total),path)
return total
dist_usage('D:\django')
分析递归算法
对于递归算法,我们将解释基于函数的特殊激活并且被执行的每个操作,该函数在被执行期间管理控制流。换句话说,对于每次函数调用,我们只解释被调用的主体内执行的操作数目。然后,通过在每个单独调用过程中的操作数的总和,即调用次数,我们可以解释被视为递归算法的一部分而执行的操作的总数。
计算阶乘
为了计算上面的阶乘函数factorial(n),共执行了n+1次函数调用,阶乘的每次调用执行了一个常数级别的运算。因此,我们得出这样的结论:计算factorial(n)的操作总次数是O(n),因为有n+1此函数的调用,所以每次调用占的操作数是O(1)。
绘制一个英式标尺
从n=0到5打印的行数为:
0,1,3,7,15。。。
推测输出行数为2n-1
证明:通过调用draw_interval(c)函数打印的行数比通过调用draw_interval(c-1)函数产生的行数的两倍还多1,通过归纳法,我们计算出行数1+2(2c-1-1) = 1+2c-2=2c-1
执行二分查找
我们观察到二分查找方法的每次递归递归调用中被执行的基本操作次数是恒定的。因此,运行时间与执行递归调用的数量呈正比。
对于含有n个元素的有序序列,二分查找算法的时间复杂度是O(logn)
第一次递归调用时,候选条目数是n,在进行一次二分查找调用之后,它至多是n/2;第三次调用之后,它至多是n/4;以此类推。一般情况下,在进行j次二分查找调用之后,剩下的候选条目数至多是n/2j。在最坏的情况下,当没有更多的候选条目时递归调用停止。听此,在进行递归调用最大次数,有最小整数r,使得
n/2r < 1,即r>logn。因此,二分查找算法的时间复杂度是O(logn)
计算磁盘空间使用情况
时间复杂度为O(n)
我们选择考虑在在所有递归调用中for循环迭代的总数。我们断言刚好有n-1个该循环的这种迭代。这一声明基于这样一个事实,即该循环的每次迭代进行一次对disk_usage函数的递归调用,并且已经得出结论,即对disk_usage函数共进行了n次调用(包括最初的调用)。因此,我们得出这样的结论:有O(n)次递归调用,每次递归调用在循环外部使用O(1)的时间,并且循环操作的总数为O(n)。总结这些限制条件,操作的总数是O(n)。
递归算法的不足
例如:元素唯一性问题。我们可以用下面的递归公式来确定序列中所有的n个元素是否都是唯一的。
def unique3(S,start,stop):
if stop-start <= 1:
return True
elif not unique3(S,start,stop-1):
return False
elif not unique3(S,start+1,stop):
return False
else:
return S[start] != S[stop-1]
对于一个大小为n的问题,对unique3函数的单一调用可能导致对两个大小为n-1的问题的unique3函数调用。反过来,这两个大小为n-1的调用可能又产生4个大小为n-2的调用,然后是8个大小为n-3的调用,以此类推。因此在最坏的情况下,函数调用的总数由如夏季和公式给出:
1+2+4+...+2n-1
所以函数unique3的时间复杂度为O(n2)。这个函数解决元素唯一性问题的效率如此低下。其低效率不是因为使用递归,而是缘于所使用的递归不加这样一个事实,这是我们以后要解决的问题。
一个低效的计算斐波那契数的过程
def bad_fibonacci(n):
if n <= 1:
return n
else:
return bad_fibonacci(n-2)+bad_fibonacci(n-1)
不幸的是,这样的斐波那契数公谥的直接实现会导致函数的效率非常低。以这种方式计算第n个斐波那契数需要对这个函数进行指数级别的调用。意味着bad_fibonacci(n)是的调用的总数是n的指数级。
一个高效的计算斐波那契数列的递归算法
def good_fibonacci(n):
if n <= 1:
return (n,0)
else:
(a,b) = good_fibonacci(n-1)
return (a+b,a)
我们认为函数 good_fibonacci(n)使用的时间为O(n),每次对函数的递归调用会使参数n减小1,因此,递归追踪包括一系列的n个函数调用。整体的运算执行在O(n)的时间内完成。
Python中最大的递归深度
在递归的误用中,另一个危险的就是所谓的无线递归。
为了避免无限递归,Python的设计者做了一个有意的决定来限制可以同时有效激活的函数的总数,典型的默认值为1000.如果达到这个限制,Python解释器就生成了一个RuntimeError消息:超过最大递归深度
Python解释器可以动态的重置,以更改默认的递归限制。使用如下
import sys
old = sys.getrecursionlimit()
sys.setrecursionlimit(1000000)
递归的其他例子
如果一个递归调用最多开始一个其他递归调用,我们称之为线性递归
如果一个递归调用可以开始两个其他递归调用,我们称之为二路递归
如果一个递归调用可以开始三个或者其他递归调用,我们称之为多重递归
线性递归
元素序列的递归求和
线性递归可以作为一个有用的工具来处理数据序列,例如:python列表。
例如:计算一个含有n个整数的序列S的和
def linear_sum(S,n):
if n == 0:
return 0
else:
return linear_sum(S,n-1)+S[n-1]
对于大小为n的输入, linear_sum算法执行了n+1次函数调用,因此这将需要O(n)的时间。
使用递归逆置序列
def reverse(S,start,stop):
if start < stop-1:
S[start],S[stop-1] = S[stop-1],S[start]
reverse(S,start+1,stop-1)
该递归算法过程运行时间为O(n)
用于计算幂的递归算法
即数x的n次幂问题,其中n是非负整数
def power(x,n):
if x == 0:
return 1
else:
return x * power(x,n-1)
时间复杂度为O(n)
不过有一种更快的方法用以计算幂函数,即采用了平方技术的定义。
def power(x,n):
if n == 0:
return 1
else:
partial = power(x,n//2)
result = partial * partial
if n % 2 == 1:
result *= x
else:
return result
产生O(logn)次递归调用
二路递归
例如:用二路递归计算一个序列的元素之和
def binary_sum(S,start,stop):
if start >= stop:
return 0
elif start == stop-1:
return S[start]
else:
mid = (start + stop)//2
return binary_sum(S,start,mid)+binary_sum(S,mid,stop)
递归深度为1+logn,使用O(logn)数量级的额外空间,与之前线性递归使用O(n)数量级的空间相比,这是一个进步。时间复杂度为O(n)
多重递归
对于一个文件系统磁盘空间使用状况分析的递归是多重递归的一个例子,另一个多重递归的常见应用是通过美剧各种配置来解决组合谜题的情况。