递归概念
任何可以用计算机求解的问题所需的计算时间都与其规模有关,问题的规模越小,解题所需的计算时间往往也越短,从而比较容易处理。直接或者间接调用自身的算法称为递归算法,用函数自身给出定义的函数称为递归函数。使用递归技术往往使函数的定义和算法的描述简捷且易于理解,有些数据结构,如二叉树等,由于其本身固有的递归特性,特别适合用递归的形式来描述。
当我们设计递归算法时,应满足三点:①符合递归的描述:需要解决的问题可以化为子问题求解,而子问题求解的方法与原问题相同,只是数量增大或减少;②递归调用的次数是有限的;③必须有递归结束的条件
其中第③点是我们在编写递归函数时必须要注意的,一定要有递归结束条件,即每个递归函数都必须有非递归定义的初始值,在函数递归结束条件下,没有再调用递归函数。
if (满足递归结束条件):
这个程序块中没有调用递归函数的语句
函数递归的调用机制
递归函数调用同样遵守函数调用机制,当函数调用自己时也要将函数状态、返回地址、函数参数、局部变量压入栈中进行保存。
实际上函数被调用时执行的代码是函数的一个副本,与调用函数的代码无关。当一个函数被调用两次,则函数就会有两个副本在内存中运行,每个副本都有自己的栈空间且与调用函数的栈空间不同,因此不会相互影响。这种调用机制决定了函数是可以递归调用的。
当有多个算法构成嵌套调用的时候,按照后调用先返回的原则进行,算法之间的信息传递和控制转移必须通过栈来实现,即系统将整个程序运行时所需的数据空间安排在一个栈中,每调用一个算法,就为它在栈顶分配一个存储区,每退出一个算法,就释放它在栈顶的存储区,当前运行算法的数据一定是在栈顶的。
递归算法的优缺点
递归算法结构清晰,可读性强,并且容易用数学归纳证明算法的正确性,为设计算法和调试程序带来很大的方便。但是递归调用运行效率比较低,无论是计算时间还是占用的存储空间都比非递归算法要多。
有时希望在递归算法中消除递归调用,使其转化为一个非递归算法。通常采用的方法是,用一个用户的栈来模拟系统的递归调用工作栈,从而达到将递归算法改为非递归算法的目的。
算法实例
阶乘函数
我们首先通过阶乘函数来了解递归,我们可以很容易的将阶乘函数的递归公式写出来:
我们通过这个递归公式,可以很容易的写出计算阶乘的递归函数
#阶乘问题
def factorial(n:int):
'''
n为输入,为n!
'''
if n == 0:
return 1
else:
return n*factorial(n-1)
#时间复杂度为O(n)
可以发现,我们在写递归函数的时候,很大程度上依赖于我们根据阶乘这个具体问题总结出来的递归公式,根据递归公式可以很容易的写出对应的递归函数。
Fabonacci数列
学过数学的人应该都知道Fabonacci数列,1,1,2,3,5,8,13…,我们可以很容易的写出这个数列的递归公式表示
同样的,根据这个递归公式我们就可以方便的写出递归函数
#Fabonacci数列
def fabonacci(n:int):
'''
n为输出fabonacci数列的第多少个
'''
if n <= 1:
return 1
else:
return fabonacci(n-1)+fabonacci(n-2)
#时间复杂度为O(2^n) 调用过程类比二叉树
全排列问题
求解一个无重复元素数组的全排列,即list中所有元素需要考虑顺序问题的全部可能的排列。比如[1,2,3],它的全排列是 123,132,213,231,312,321,它的个数为len(list)!
我们根据上面的两个问题的思路,我们这里也要根据全排列问题,总结出来一个递归公式。我们记数组 R [ r 1 , r 2 , r 3 , . . . ] R[r_1,r_2,r_3,...] R[r1,r2,r3,...]的长度为n,记 P ( R ) P(R) P(R)为数组R的全排列,记 R i = R − [ r i ] R_i=R-[r_i] Ri=R−[ri],记 r i P ( R i ) r_iP(R_i) riP(Ri)表示在全排列 P ( R i ) P(R_i) P(Ri)的每一个排列前加上前缀 r i r_i ri得到的排列组合。
(GitHub貌似显示不出来latex)
当n=1时,我们可以得到 P ( R ) = R = [ r ] P(R) = R = [r] P(R)=R=[r],数组R中只有r一个元素,自然也就只有一种排列。
当n>1时,我们可以写出 P ( R ) = r 1 P ( r 1 ) + r 2 P ( r 2 ) + . . . + r n P ( r n ) P(R) = r_1P(r_1)+r_2P(r_2)+...+r_nP(r_n) P(R)=r1P(r1)+r2P(r2)+...+rnP(rn)
上面的关系实际上给出了计算 P ( R ) P(R) P(R)的递归公式如下:
既然总结出了全排列问题的递归公式,我们接下来就可以写出具体的递归函数
#组合全排列问题
def swap(s:list,m:int,k:int):
'''
交换数组s中指定位置m,k处的两个字符
'''
s_ = s.copy() #切记不可以直接使用 s_ = s,否则修改s_会直接影响到s,因为使用s_ = s时,是将s_指向s所指向的数组首地址,这样其实s_和s指向的是同一个数组。
temp = s_[m]
s_[m] = s_[k]
s_[k] = temp
return s_
def perm(s:str):
'''
s为输入的list
'''
result = [''] #每次函数调用完返回的中间数组
result_ = [] #临时辅助数组
string = '' #临时辅助空字符串
if len(s) < 1:
return ['']
elif len(s) == 1:
return s
else:
for i in range(len(s)):
str_ = swap(s,0,i)
for j in perm(str_[1:]): #获得riP(ri),其中perm(str_[1:])获得的是P(ri)
string = s[i]+j
for w in result:
result_.append(w+string) #更新list
result = result_ #这里可以直接使用 = ,是因为下面result_指向了【】,而result并没有改变
result_ = []
return result
整数划分问题
正整数可以表示为一系列的正整数之和,例如:
n
=
n
1
+
n
2
+
n
3
+
.
.
.
+
n
k
n = n_1 + n_2 + n_3 + ... + n_k
n=n1+n2+n3+...+nk (其中
n
1
>
=
n
2
>
=
.
.
.
>
=
n
k
>
=
1
n_1>=n_2>=...>=n_k>=1
n1>=n2>=...>=nk>=1)
我们目的是对于一个正整数n,到底有多少条符合条件的划分
我们考虑能不能把问题分解,使问题的规模变小,这里我们引入一个中间变量m,对于正整数n,将可能的划分中最大加数 n 1 n_1 n1不大于m的划分个数记为q(n,m)。
那么我们很容易就能得到,当 m = 1 或者 n = 1 的时候,q(n,m) = 1, m=1时即 n i n_i ni全为1,自然也就只有一种情况。
当 n=m 的时候,划分可以由 n 1 n_1 n1 = n 的划分 和 n 1 < = n − 1 n_1<=n-1 n1<=n−1 的划分组成 ,即q(n,m)=q(n,n)=1+q(n,n-1)
当 n < m 的时候,n_1 <= n < m ,则q(n,m) = q(n,n)
当 n > m 的时候,划分可以由 n 1 n_1 n1 = m的划分 和 n 1 n_1 n1<=m-1的划分组成,这个时候q(n,m) = q(n-m,m) + q(n,m-1)
根据上面的关系得到q(n,m)的递归公式如下
所以整数划分的递归函数可以写成下面这个样子
#整数划分问题
def q(n:int,m:int):
if n < 1 or m < 1:
return 0
if n == 1 or m == 1:
return 1
if n == m:
return 1 + q(n,n-1)
if n > m :
return q(n,m-1) + q(n-m,m)
if n < m:
return q(n,n)
hanoi问题
下面是递归算法中最著名的汉诺塔问题,想必大家在最初学习递归算法的时候肯定被折磨过很多次,这里我们试着能不能像上面的问题一样,总结出一个递归公式,或者得到一个递归流程。
比如要将a上面的盘子转移到b上面,当a上面只有一个盘子的时候,直接a->b,如果a上面有两个盘子,步骤是a->c,a->b,c->b,那当我们变成三个盘子的时候,我们还像这样写出我们的步骤:a->b,a->c,b->c,a->b,c->a,c->b,a->b。
从上面我们的步骤中可以发现,我们要将a上面的n个盘子放到b上面,我们应该首先将n-1个盘子放到c上面,然后a->b,再将c上面的n-1个盘子,转移到b上。那么n-1个盘子放到c上面,首先应该将n-2个盘子放到b上面,然后a->c,再将b上面的n-2个盘子放到c上,接下来循环往复… 我们可以惊人的发现,这不就是递归的思想了吗,那整个hanoi的问题就可以简单的描述为:
可以很快的写出来hanoi的递归函数了:
#汉诺塔问题
def hanoi(n:int,a:str,b:str,c:str):
if n < 1:
print('input error!')
if n == 1:
print(a+'-->'+b)
else:
hanoi(n-1,a,c,b)
print(a+'-->'+b)
hanoi(n-1,c,b,a)
参考内容
计算机算法设计与分析(第四版) 王晓东著
递归算法总结:https://www.cnblogs.com/king-lps/p/10748535.html
递归算法:https://www.cnblogs.com/xiaoyunoo/p/3519577.html