递归算法学习笔记

1 认识递归       

        函数内部调用的自身的编程技巧称为递归(Recursion),或者叫做俄罗斯套娃。

        递归递归,英文单词就一个recursion,这不好,不利于理解。中文的递归二字就好理解了,递归可拆分为递去和归来两个过程,其实单词fetch可能更加生动形象一些。

        递去就是将一个庞大的父问题转化为一个相似的子问题,此子问题又作为新的父问题,再一步一步进行转化。所谓的相似是指他们的解决方法是相似的,而且每递去一步后,大问题就在上一步的基础上更加细化为一个子问题;归来是指不当铁头怪,撞到南墙后(或者称为临界点)就选择回头,而且回头时一步解决一个子问题。

        在归来与递去之间究竟发生了什么呢?

        (1)在递去过程中,执行的语句在遇到同名函数前是正常执行的,但是新的同名函数之后的语句则保留在内存中,暂时不执行;

        (2)反复重复(1)过程,直到遇到终止条件,递去无法继续,开始由后往前归来,归来的过程其实就是继续执行原来保存在内存中的,递去过程中没有执行完成的语句;

        这里需要注意,递去和归来时函数的执行顺序是相反的。递去时没有每一个函数没有被执行的部分都需要等相邻下一个子过程完全结束后才继续执行。

        总结来说就是递去只执行新函数之前的语句,之后的存起来,执行顺序就是由外到里,宛如进入深渊;必须要有一个终止条件,不然只有递去没有归来,陷入死循环;归来只执行新函数语句后的语句,执行顺序是由里即外,犹如从深渊出来。 越是最前的函数语句,递归后越在最后执行。递去归来之间,随着子过程不断被执行完毕,最终程序完全走出深渊,最初的父问题也就被解决了。

2 应用递归

2.1 理解递归

        我认为,要理解递归的关键有两点,其一在于理解递归函数的用途是什么,其二就是搞清楚父问题及其下一步子问题之间的关系是什么(或者更加直白的,如何使用下一步子问题表示父问题)。

        需要强调的是,绝对禁止使用理解循环迭代的方法——按照程序的执行顺序一步一步走完,最终得到输出结果。这样真的会完犊子啦,套娃不了几步就会把你绕晕的。

        下面以Fibonacci数列为例说明如何理解递归:小朋友们都知道Fibonacci数列其实就是1、1、2、3、5、8、13、21、……的这样一组数列。要求这个数列的第n项值,不妨假设有一个函数fib(n),这个函数的用途是输入一个项数n后就可以求出第n项的数列值。好了,目前为止,理解递归的第一步已经完成了,找到了这个函数的功能;那么我们再来进行第二步:寻找父子问题之间的联系,这里不难发现,父子之间的关系显然为fib(n)= fib(n-1) + fib(n-2)。到此为止,理解递归的第二步就已经完成了,父子问题的关系已经找到。要求解fib(n),只需要求解fib(n-1) ,fib(n-2)就可以了,而求解fib(n-1) ,fib(n-2)就只需要简单地把fib(n)中的n换为n-1和n-2就可以了。再次强调,理解到第二步就可以了,禁止思考fib(n-1) ,fib(n-2)如何细分为更小的子问题。这对使用及理解递归没有任何帮助。

2.2 递归问题的灵魂三问

        在对递归有所理解后,下面来介绍递归的三大灵魂拷问。

        第一问:函数的功能是什么。要动手编写一个函数,必定已经对其功能有所了解了,有时候函数的功能其实就是题目本身或者老板的要求。比如2.1节的fib(n)就是函数的功能——给出n,求解数列第n项数值。

        第二问;父子问题之间的递推关系式(或者如何使用子问题通过一定的变换得到父问题)是什么。比如2.2节的fib(n)= fib(n-1) + fib(n-2)就是一个典型的二阶线性递推关系。此外,递推关系式的求取是递归算法的难点与重点,弄清楚了递推关系,递归算法就完成了十之八九。

        第三问:递归终止的条件(即第1节提到的临界点或者南墙)是什么。必须设置递归的临界点,不然递归函数就像是脱缰的野马一去不回。马儿要是不回来,当然答案也就不会被带回来啦。一般而言,递归的终止条件就是所能确定的最简单的一种或几种情况的函数值。比如,求取Fibonacci数列的第n项,其终止条件就是n=1以及n=2时的函数值(因为fib(n)的递推式为 fib(n-1) + fib(n-2)两项,故需要设置两个终止条件。这个其实是很好理解的,高中数列的知识就告诉了我们,一阶线性递推需要一个已知量a1来求取通项公式,而二阶线性递推需要两个已知量a1,a2才能得到通项公式)

        以后使用递归求解问题时,只要把这三个灵魂问题,尤其是第二个问题想明白了,递归也就不是事儿啦。这三个要素的思考顺序就是依次递进的:先想想编写的函数功能是什么;再思考父子问题之间的递推式是什么(数列问题的递推式就是数学上的递推关系,其他问题可能没有数学形式上的递推关系,但必然有实际意义上的递推关系);递推关系解决后,就可以根据递推关系来判断需要几个终止条件(一父几子一般就需要几个终止条件)。

2.3 递归应用实例

        递归的应用主要有三种类型:

  1. 其一是问题的定义是按递归定义的,比如阶乘,Fibonacci数列的第n项,字符串全排列,二分法查找;
  2. 其二是问题解法按递归算法实现的,比如HTP;
  3. 其三是数据的结构是按照递归定义的,比如二叉树遍历。

(1)Fibonacci数列的第n项

        这个应该算是最简单的递归应用了,应该不需要展开说。

def fib(n):
    '''求Fibonacci数列的第n项
    1、1、2、3、5、8、13、21、……
    '''
    if (n <= 2):  # 设定终止条件
        return 1
    return fib(n - 1) + fib(n - 2)  # 设定递推公式

(2)字符串全排列

        全排列问题比Fibonacci数列稍微难一点点,毕竟不是一个纯粹的数学递推问题,没有数学形式意义上的推推关系式子可以用,但是严格按照三大问题去思考也不难啦。

  1. 第一问:函数的功能是什么。很显然,我们要设计一个str_sort(s)函数,其功能是对字符串s进行一个全排列;
  2. 第二问:父子问题之间的转化关系是什么。假设有一个长度为n的字符串的需要全排列,其子问题就是从这个父问题中抽取长度为n-1的子串进行全排列。父子之间关系就是“父=没有抽取到的一个字符+子问题”故递推关系为str_sort(s(n))= n+str_sort(s(n-1))。还有一个问题就是长度为n-1的子串如何选取,学过排列组合都会啦,不就是C_{n}^{n-1}=C_{n}^{1} 嘛。
  3. 第三问:终止条件是什么。一阶递推,一个终止条件就够啦,想想最简单的情况,输入长度为1的字符,原样输出就好啦。
def str_sort(s):
    '''
    str_sort(s)作用是对字符串s全排列
    '''
    if len(s) <= 1:
        return s  # 此为递归的终止条件(输入一个字符时返回本身即可)
    str_list = []
    for i in range(len(s)):# 让每一个字符打头作为固定元素
        for j in str_sort(s[0:i] + s[(i + 1):]):# 剩下的第n-1个字符全排列,表达式为str_sort(s(n-1))
            str_list.append(s[i] + j) # 此行与上一行一起构成递推公式,千万不要尝试代入按照运算顺序推导,只需要知道递推逻辑是固定一个首字符加上剩下的n-1个字符全排列即可
    return str_list 

(3)二分法查找

        这个在(2)的基础上难度又大了一点点,二分法的子问题是需要分情况讨论滴——子问题究竟取左还是取右,这个用if-else分支结构就好了。

  1. 第一问:函数功能是什么。设计一个函数find(n,list),从有序列表list中查找元素n;
  2. 第二问:父子之间的关系是什么。二分法嘛,先在list中间切一刀分两半,由于是列表的有序性,比一比就知道n可能在左边还是右边啦。这个递推关系不好写为数学表达式,意思到了就行。
  3. 第三问:终止条件是什么。当n不左不右,正好是个中间派的话就意味着找到了n,程序就可以终止了,或者列表不能再使用二分法细分了(子问题列表为空后)就意味着n不在list中,程序也应该终止。
def find(n, list):
    '''
    有序排列的列表使用二分法查找元素
    在列表list中查找n
    '''
    start = 0
    end = len(list) - 1
    if list != []: 
        mid = (start + end) // 2
        if n > list[mid]:
            find(n, list[mid + 1:]) # 递推到后半段查找
        elif n < list[mid]:
            find(n, list[:mid]) # 递推到前半段查找
        else:# 终止条件1,找到n
            print('存在')
            return 
    else:# 终止条件2,遍历结束,未找到n
        print('不存在')
        return

(4)Hanoi Tower Problem

        HTP问题比前几个问题又难了一点点,涉及到多次递归的问题。下面首先介绍什么是HTP。

        HTP源于印度一个古老传说。相传大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上按照从下往上由大到小的顺序摞着n片黄金圆盘。大梵天命令婆罗门把圆盘按照原来的顺序重新摆放在另一根柱子上。并且规定在任何时候小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作? 

        我寻思这大梵天和婆罗门还挺闲,印度宗教竞争这么激烈,一天天地不好好想着怎么增加信徒,整这玩意儿让婆罗门跟当代程序员刨活儿。

        HTP问题是一个典型的递归问题:有a,b及c三根柱子,原来从上到下由小到大叠放的n个圆盘在a柱上,现在要将这n个盘移到c柱上,而且任何时候小盘上面都不能放大盘。如果感觉黄金盘子比较抽象,家里有条件的搞几个菜盘子试试就知道了。

        饭要一口一口吃,盘要一个一个移。毫无疑问,要实现c柱最下面放最大盘,那么必须将较小的n-1个盘先移到b柱上,最后再将a柱上的最大盘n移到c柱就好了。完成这个动作后,柱子上的布局就是a柱没盘,b柱从上到下由小到大叠放着n-1个圆盘,c柱有第n个最大盘。子问题已经出现了:将b柱的n-1个盘通过a柱的辅助统统移动到c柱上。这可不就是一个父子局的递归问题嘛,下面就是简单的灵魂三问了;

  1. 第一问:函数的功能是什么。设计一个函数hanoi(n,a,b,c),其功能是将n个盘子从原来的a柱经b柱的辅助一个一个地统统移动到c柱,且遵循大盘从不压小盘的规则;
  2. 第二问:父子问题的关系是什么。经过前面的分析,完成一个父问题可以拆分为三步,首先将a柱最上面的n-1个盘统统移到b柱,再将a柱剩下的第n个盘子移动到c柱,最后将b柱的n-1个盘移动到c柱就好了。第三步就是子问题了,前两步就是父问题和子问题的关系。
  3. 第三问:终止条件是什么。思考一下最简单的情况,a柱就一个盘子时,一步到位直接把它移动到c柱就好啦。
def hanoi(n, a, b, c):
    if (n == 1):
        # 如果a上只有一个盘子,直接把a移动到c
        print(a, '-->', c)  # 终止条件
        '''下面三个hanoi的调用完成了递推过程'''
    else:
        hanoi(n - 1, a, c, b)  # 先把a上的n-1个盘子移动到b
        hanoi(1, a, b, c)  # 再把a上最后一个盘子移动到c
        hanoi(n - 1, b, a, c)  # 最后把b上所有盘子(n-1个)移动到c

4 告别递归

在解决一些带有递归性质的问题时,递归虽然显示出了独特的优越,但是仍然存在一些缺点:

  1. 递归疯狂调用自己的时候是存在时间和空间消耗的,这导致计算效率低下;
  2. 递归运算有时存在大量重复计算,这也造成了其计算效率低下;
  3. 递归调用自身次数太多的话会导致存储返回值、临时变量等的空间不足(栈溢出现象);
  4. 递归算法不如循环迭代容易理解,与一般人的思维方式不同。

        虽然递归存在的以上问题可以被一些优化方法在一定程度上解决,但是能不用就不要用吧,就算要用也小心谨慎的用吧,千万别整得溢出了。另外,我们喜闻乐见的循坏迭代也是能实现任何递归功能的,只是有时候要复杂一点点啦。

5 参考资料

算法设计方法:递归的内涵与经典应用_Rico's Blogs-CSDN博客_递归的应用

python查找算法的实现-二分法 - yupeng - 博客园 (cnblogs.com)

Python解决汉诺塔问题_Lemon的博客-CSDN博客_python汉诺塔

递归算法原理及应用_ae479655315的博客-CSDN博客

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

syphomn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值