递归算法学习笔记

递归概念

任何可以用计算机求解的问题所需的计算时间都与其规模有关,问题的规模越小,解题所需的计算时间往往也越短,从而比较容易处理。直接或者间接调用自身的算法称为递归算法,用函数自身给出定义的函数称为递归函数。使用递归技术往往使函数的定义和算法的描述简捷且易于理解,有些数据结构,如二叉树等,由于其本身固有的递归特性,特别适合用递归的形式来描述。

当我们设计递归算法时,应满足三点:①符合递归的描述:需要解决的问题可以化为子问题求解,而子问题求解的方法与原问题相同,只是数量增大或减少;②递归调用的次数是有限的;③必须有递归结束的条件

其中第③点是我们在编写递归函数时必须要注意的,一定要有递归结束条件,即每个递归函数都必须有非递归定义的初始值,在函数递归结束条件下,没有再调用递归函数。

if (满足递归结束条件):
    这个程序块中没有调用递归函数的语句
函数递归的调用机制

递归函数调用同样遵守函数调用机制,当函数调用自己时也要将函数状态、返回地址、函数参数、局部变量压入栈中进行保存。

实际上函数被调用时执行的代码是函数的一个副本,与调用函数的代码无关。当一个函数被调用两次,则函数就会有两个副本在内存中运行,每个副本都有自己的栈空间且与调用函数的栈空间不同,因此不会相互影响。这种调用机制决定了函数是可以递归调用的。

当有多个算法构成嵌套调用的时候,按照后调用先返回的原则进行,算法之间的信息传递和控制转移必须通过栈来实现,即系统将整个程序运行时所需的数据空间安排在一个栈中,每调用一个算法,就为它在栈顶分配一个存储区,每退出一个算法,就释放它在栈顶的存储区,当前运行算法的数据一定是在栈顶的。

递归算法的优缺点

递归算法结构清晰,可读性强,并且容易用数学归纳证明算法的正确性,为设计算法和调试程序带来很大的方便。但是递归调用运行效率比较低,无论是计算时间还是占用的存储空间都比非递归算法要多。

有时希望在递归算法中消除递归调用,使其转化为一个非递归算法。通常采用的方法是,用一个用户的栈来模拟系统的递归调用工作栈,从而达到将递归算法改为非递归算法的目的。

算法实例
阶乘函数

我们首先通过阶乘函数来了解递归,我们可以很容易的将阶乘函数的递归公式写出来:

image

我们通过这个递归公式,可以很容易的写出计算阶乘的递归函数

#阶乘问题
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…,我们可以很容易的写出这个数列的递归公式表示

image

同样的,根据这个递归公式我们就可以方便的写出递归函数

#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)的递归公式如下:

image

既然总结出了全排列问题的递归公式,我们接下来就可以写出具体的递归函数

#组合全排列问题
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<=n1 的划分组成 ,即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)的递归公式如下

image

所以整数划分的递归函数可以写成下面这个样子

#整数划分问题
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的问题就可以简单的描述为:

image

可以很快的写出来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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值