[算法系列] 深入递归本质+经典例题解析——如何逐步生成, 以此类推,步步为营

本文是递归系列的第三篇, 第一篇介绍了递归的形式以及递归设计方法(迭代改递归),;第二篇以递归为引子, 详细介绍了快排和归排以及堆排的核心思想; 本篇主要通过几个题, 从递推, 归纳法的角度, 深入了介绍了递归的本质和具体应用.

往期回顾:

  1. 搞懂递归, 看这篇就够了 !! 递归设计思路 + 经典例题层层递进
  2. 递归应用: 快速排序+归并排序算法及其核心思想与拓展 … 附赠 堆排序算法

回顾我们在第一篇文章讨论的递归中, 下面是我们能够看到现象形式:

f(n) -> f(n - 1) -> f(n-2) -> ... -> f(1)

但实际本质是: 为了解决/完成 f(n), 必先完成f(n- 1); 为解决f(n-1),必先解决f(n-2) … 那么最先要解决f(1)

f(1) -> f(2) -> f(3) -> ... ->f(n)

回顾以前学过的数学归纳法:

1. 证明当k=1时,条件成立
2. 假设k=n(n>=1)时,条件成立
3. 证明k=n+1,条件成立
得到结论: k取任意正整数时,条件成立

如果没记错的话这叫第一数学归纳法, 往往我们用来证明构造的某些式子在给定自然数集合(全体或局部)的正确性. 而数学归纳法本质是什么呢? 通俗来看, 就是首先证明了k=1时的正确性, 然后证明k = n 成立可以推导出k=n+1成立. 根据上述两个条件可以得出k=2也就成立了… 然后k=3也就成立… 本质是递推.

  • 递归解决的本质是先从f(1)->f(2)->…->f(n), 小问题解决了,再解决大问题
  • 数学归纳法式从k = 1 逐层证明, 或者说证明k=n和k=n+1的关系,然后递推
  • 递推, 就是按照前一个(或几个)的关系推理出下一个 …

recursion一词既可以翻译为递推,也可以翻译为递归, 这里的归应该是是规约的意思. 注意这里的递归和编程形式中的 递归调用 是有点区别的, 编程中谈到的形式化更多一些, 而数学本质还是和递归递推没有区别.

递归, 递推, 数学归纳法本质正是同一种东西.

好了,现在看来知道了这些似乎作用不大. 我们还是举个例子, 搞懂递归, 看这篇就够了 !! 递归设计思路 + 经典例题层层递进 中的青蛙上楼梯问题.

1. 再谈青蛙上楼梯

楼梯有n个台阶, 一个青蛙一次可以上1 , 2 或3 阶 , 实现一个方法, 计算该青蛙有多少种上完楼梯的方法

文中给出了递归的解法:

(回忆找重复,找变化,找出口)

假如青蛙上10 阶, 那么其实相当于要么 站在第9 阶向上走1步,要么 站在第8 阶向上走两步, 要么在第7阶向上走3步. 每个大于3阶楼梯的问题都可以看成几个子问题的堆叠

变化:令f(n) 为 青蛙上n阶的方法数. 则f(n) = f(n -1) +f(n - 2) + f(n -3) , 当n >= 3

出口: 当n = 0 时 ,青蛙不动 , f(0) = 0; n = 1时 ,有1种方法 , n = 2 时 有2 种方法

def f(n):if n == 0 :return 1   #站着不动也得返回1的, 因为实际上0种方法的是没意义if n == 1:return 1if n == 2:return 2return f(n - 1) +f(n - 2) +f(n -3)

显然这样的递归方法不是很直观的, 其实一开始拿到这题 , 普通地想, 应该是拿出张白纸来, 左边起名一列: 阶数 , 右边起名一列: 走法

阶数       走法
1           1       0->1
2           2       0->1->2           0->2
3           4       0->1->2->3     0->1->3       0->2->3       0->3
4           7       ...
...         ...

详细康康阶数为4时的走法:

注意我分成了三列写, 如果不看红色部分的话, 三列分别代表了上第1,2,3阶的方法. 现在带着红色的 ->4 一起看:

  • 第一列: 相当于先上到第1阶再一次上到4 (因为最大可以跨3阶嘛)
  • 第二列: 相当于先上到第2阶再一次上到4 (相当于最后一次跨2阶嘛)
  • 第三列:相当于先上到第3阶再一次上到4(最后一次跨1阶即可)

显然上到第四阶的方法刚好就是这三列的和了 …

到这里, 有兴趣的同学可以在写出阶数为5的走法. 但其实也会得到下面的结论:

  • 第一列: 相当于先上到第2阶再一次上到5 (因为最大可以跨3阶嘛)
  • 第二列: 相当于先上到第3阶再一次上到5 (相当于最后一次跨2阶嘛)
  • 第三列:相当于先上到第4阶再一次上到5(最后一次跨1阶即可)

显然上到第五阶的方法刚好就是这三列的和了 …

…想一想, 规律也就可以得出了

阶数为n的走法. 但其实也会得到下面的结论:

  • 或者先上到第n-3阶再一次上到n (因为最大可以跨3阶嘛)
  • 或者先上到第n-2阶再一次上到n (相当于最后一次跨2阶嘛)
  • 或者于先上到第n-1阶再一次上到n(最后一次跨1阶即可)

所以 f(n) = f(n -1) +f(n - 2) + f(n -3) 不是凭空产生, 而真是一步一步的像上面一样推出来 – 递归表达也是如此

下面就可以自然的得到递归法实现

def go_stairs(n):if n <= 1:return 1if n == 2 :return  2if n == 3 :return  4return go_stairs(n - 1) + go_stairs(n - 2) + go_stairs(n - 3)

写出了出口条件, 写出了递推式, 计算机不就帮我们像上面一样, 一步一步地推下去了么…

同样的, 我们也可以按照我们的推理演算的顺序, 用一个长度为3的数组, 保存每次得到的f(n -1) ,f(n - 2) ,f(n -3), 下一轮再更新…这就是我们递推的迭代法实现 :

def go_stairs_ite(n):#声明一个长度为4的数组保存每次计算得到值, 用于存储每次计算所需的三个值和一个结果值arr =[]if n <= 1:return 1if n == 2 :return  2if n == 3 :return  4arr[0] = 1arr[1] = 2arr[2] = 4              #1  2   4   ()for i in range(4, n+1):arr[3] = arr[0]  #1  2   4   1                       不断地空出来一个固定位置,存结果                arr[0] = arr[1]    #2  2   4   1       arr[1] = arr[2]    #2  3   4   1arr[2] = arr[3] + arr[0] + arr[1]   #2  4   7   1return  arr[2]

2. 机器人走方格 cc150 9.2

有一个 X*Y 的方格, 一个机器人只能走格点且只能向右或者向右走, 要从左上角走到左下角
请设计一个算法, 计算机器人有多少种走法
给定两个个正整数X , Y, 返回机器人走法的数目.

分析如下:

得到递推公式和出口条件就可以写出递归形式代码:

'''
递归形式
'''
def robot_go_grim(x, y):if (x == 1 or y == 1):return 1return  robot_go_grim(x - 1 , y ) +robot_go_grim(x , y - 1 )

想清楚了, 代码看上去是不是异常简洁呢?

现在考虑迭代形式: 我们知道,

  • 如果只有一个格子, 那么终点即为起点, 结果为1
  • n * 1 或 1 * m 的情况, 总是只有一种走法

在对应格子中填上从此处到右下角的走法, 目前可得到:

然后就可以填格子, 根据就是f(n,m) = f(n - 1,m) +f(n, m -1) .

这其实也就相当于:当前的方法数 = 自己下方格子处的方法数 + 右边格子处的方法数

  • 填到图中值为6 的格子处, 也就得到了f(3,3)的解
  • 填到图中值为5 的格子处, 也就得到了f(2,5)的解
'''
迭代形式
'''
def robot_go_grim_ite(x,y):dp = [[0 for i in range(0, y)] for j in range(0, x)]#出口条件(边界条件)for j in range(0 , y):dp[x - 1][j] = 1for i in range(0, x):dp[i][y - 1] = 1# print_matrix(dp)for i in range(x - 2 , -1 , -1):for j in range(y - 2 , -1 , -1):dp[i][j] = dp[i +1] [j] + dp[i][j +1]return dp[0][0]

3.输出合法括号cc9.6

编写一个方法,打印n对括号的全部有效组合(即左右括号正确匹配)
示例
输入:3
输出:()()(),((())),(())(),()(()),(()())

按照前两道的思路, 我们依然从最初开始逐步递推: 寻找每次大规模问题和其小一号问题的关系. 同时出口条件又是已知的

def proper_bracket(n):''':param n:   输入括号对数:return:'''#声明一个set用于存放结果sn = set()#出口条件if n == 1 :sn.add("()")return snsn_1 = proper_bracket(n-1)    #声明小一号规模的子问题,上一次求得的sn作为下一次的sn_1for e in  sn_1:              #以下全是归回来的副作用, 阐明子问题与父问题的关系sn.add("()"+e)         sn.add(e+"()")sn.add("("+e+")")return snprint(proper_bracket(3))

稍微解释下上述代码,

  • n=1时为出口条件,答案明确
  • n>1时依次调用n-1, 因此首先求得的是n=2时, sn_1="()",针对它的每一项进行加左,加右,加外三个操作得到sn, 再逐次返回

下面的迭代形式正是递推过程的正向体现

'''
迭代形式
'''
def proper_bracket_ite(n):sn = set()sn.add("()")if n ==1 :return snfor i in range(2 , n+1 ):sn_new = set()               #从n=2开始每次创建一个新集合set_new, 从sn推出set_newfor e in sn:sn_new.add("()" +e)sn_new.add(e + "()")sn_new.add("(" + e + ")")sn = sn_new                  #set_new变sn,周而复始return  sn

4.集合的所有子集cc9.4

编写一个方法,返回int集合的所有子集
# 例如:
# 输入: [1,2,3]
# 输出: [],[1],[1,2],[1,2,3],[2,3],[3],[1,3],[2]

此题我们同样按照小规模往大规模进行推理,

  • 当只有一个元素时, 只用考虑有这个元素(子集1),或者没有这个元素(子集2),

  • 当有两个元素时,可以这样考虑:

    • 加入第一个元素 =>形成子集1
    • 加入第二个元素=>形成子集2
    • 弹出第二个元素
    • 弹出第一个元素
    • 加入第二个元素=>形成子集3
  • 当有多个元素时, 对于每个元素,都有试探放入或者不放人集合中两个选择:

    • 选择该元素放入,递归地进行后续元素的选择,完成放入该元素后续所有元素的试探;

    • 之后将其拿出

    • 再进行一次选择不放入该元素,递归地进行后续元素的选择,完成不放入该元素时对后续元素的试探

设arr传入的数组, item为每一个子集, res为最终的结果集, i 表示当前arr的下标

下图演示递归求解的调用思路:


代码如下

def get_subset(arr):item = list()res = list(list())generate(0 , arr, item, res)print(res)def generate(i , arr , item, res):''':param i:       表示当前操作的arr下标:param arr:     初始传入的int集合:param item:    存放每个子集的set:param res:     存放最终结果的set:return:'''if(i >= len(arr)):returnitem.append(arr[i])temp_item=list(item)          #这里不能直接res.append(item),否则下一次更新res中的item会跟着变化,这里只需要其元素res.append(temp_item)#重点generate(i +1, arr, item ,res)item.pop()                         #将当前元素拿出generate(i + 1, arr, item ,res)        #递归调用不考虑当前元素的情况

对于这种放或不放的01事件,还可以用二进制表示的方法。。具体来看,就是就是可能性的组合问题.以原始集合{1,2,3}为例,下图可以很好的表示子集的所有可能性:

因此,我们可以用当前位置上1或0表示选或不选当前位置上的元素, 数组长度即为二进制数的位数, 即可用一个3位二进制数保存{A,B.C}的所有可能性.

而在寻找这种可能时, 可从0遍历到2^(len(arr))-1, 其中的每一个二进制数,刚好表达的是一种可能性. 比如:110,即为{A,B}.

def get_subset_ite(arr):res= list()     #最终结果集for i in range(2**len(arr) - 1 ,-1 , - 1):item = list()   #d当前子集for j in range(len(arr) - 1 , -1 ,- 1):  #j是遍历每一位,当该为为1,则对应的元素加入if (i>>j) & 1 == 1:                 #若该二进制位为1,则加入itemitem.append(arr[j])res.append(item)                         return(res)

5.全排列cc9.5

写一个方法,返回一个字符串数组的全排列
例如
输入:"ABC"
返回:"ABC","ACB","BAC","BCA","CAB","CBA"

这个问题和刚刚的那个子集问题结合起来看

  • 子集问题是:针对某一位上的元素,选还是不选这个元素的问题(0或1). 对每一位来说均有两种可能, 总计为2^n个情况(子集)
  • 全排列问题是: 每个位置都要选,但是是选n个当中哪一个的问题. 其次,当前选定一个了,下一个可选情况就少1了.因此情况个数为n!

那么如何用递归思考方式着手解决呢? 还从小规模逐渐推吧

  1. 当串长度为1时:“a”, 只有一种情况
  2. 当串长度为2时,比如"ab": 初始"a", 加一个"b":
    • “b"可以放在"a"的前面形成"ab”
    • 也可以放在"a"后面形成"ba"
  3. 当串长度为3时,“ab"或"ba”, 加一个"c":
    • 对于"ab",有a左,ab中间,b右三个位置可加入, 分别形成三个串
    • 对于"ba",同样有三个为加如c,同样形成三个新串

由此推而广之到n时:

令  S(n-1) = {前n-1的子串全排列集合}, 则S(n)与S(n-1)关系为:for each item in S(n-1):for each empty between str[i] and  str[i+1]:item.append(str[n])res.append(item)

上面的方法, 其实并不是我们平时所一下想到的, 那么我们平时是怎么想的呢?

  • 先以a开头, b开头 , c开头写…abcd
  • 调换最后两位顺序…
  • 逐渐从后面向前面调换顺序, 写完所有a打头的item
  • 接下来交换a和b, 以b打头, a第二个写… 写完为止
  • 然后依然b打头, c第二个写…
  • 接下来交换a和c,c打头,a第二个…

看文字感觉不好表述, 那么还是看图好了:

蓝色数字为调用回溯顺序

代码:

res = list()    #全局: 最终结果list
def get_all_array(str):arr = list(str)arr.sort()      #先排好序generate(arr, 0)return resdef generate(arr ,k ):#递归走到底了 表示排好了if k == len(arr):item = "".join(arr)res.append(item)#从第k位开始的每个字符都尝试放在新排列的第k个位置for i in range(k , len(arr)):swap(arr , k ,i)   #交换, 形成新的顺序, 比如 bc=>cbgenerate(arr , k+1)  #递归调用swap(arr , k ,i)   #这是返回时的副作用, 再次交换, 复原 == >回溯# 辅助函数swap
def swap(arr , i ,j):if i <0 or  j < 0 or i > len(arr) or j > len(arr):return "i or j is out of indedx"tem = arr[i]arr[i] = arr[j]arr[j] = tem
['abc', 'acb', 'bac', 'bca', 'cba', 'cab']

上面这个交换-回溯法很简洁, 但是并不能按照字典序打印, 下面这个方法就可以将其字典序打印了

伪代码如下:

res = list()            #存放最终结果
generate("" , str)       #初始时前缀为空generate(prefix, str) :if prefix.length == str.length:res.add(prefix)   #结果集中放入prefixreturnfor each ch in str:# 这个字符可用: 在pre中出现的次数 < 在字符集中出现的次数 (这是关键)if prefix.count(ch) < str.count(ch)generate(prefix + ch ,str)  #将ch加入prefix中,继续递归调用

好了, 本次介绍就到这里, 下面来小结一下:

  • 本文是递归系列的第三篇, 第一篇介绍了递归的形式以及递归设计方法(迭代改递归),;第二篇以递归为引子, 详细介绍了快排和归排以及堆排的核心思想; 本篇主要通过几个题, 从递推, 归纳法的角度, 深入了介绍了递归的本质和具体应用.

  • 本文所谈递归的"本质",是数学角度上的,且并未继续深入(比如所谓的封闭式计算方法,直接求通项等). 同时,关于计算机中的递归(比如栈开辟,函数存储等问题)并未涉及, 待以后补充学习后一定补上.

  • 前两个题是数值类问题, 后三个题为非数值型问题. 他们的核心在这里都是: 逐步生成, 以此类推 .

  • 递归设计的方法依然还是 搞懂递归, 看这篇就够了 !! 递归设计思路 + 经典例题层层递进 中详细介绍的:

    • 找出口条件 ==> 边界, 最小规模的问题 ==> 初始情况
    • 找不变 ==> 解决问题的方法不应变化, f(n) 与 f(n-m) 才能表述成父问题与子问题的关系(回忆前面的汉诺塔问题)
    • 找变化 ==> n规模的问题与n-m规模问题之间的关系(考虑走格子, 全排列问题) ==> 递推公式中的n
  • 本篇介绍的一些东西将在后续对回溯, dfs ,动态规划的介绍中有所体现. 这里主要强调的是:

    如何观察问题 ==> 从小规模开始递推 ==> 找出本质(递推公式) ==> 按照方法,设计算法(递归, 迭代)
    

接下来的文章将对递归的一些应用: dfs, dp等进行介绍
下一篇:[算法系列] 搞懂DFS——设计思路+经典例题(数独游戏, 部分和, 水洼数目)图文详解

[算法系列] 深入递归本质+经典例题解析——如何逐步生成, 以此类推,步步为营相关推荐

  1. 【算法】递归(recursion)+经典例题个人分析

    定义(个人理解) 1.自己调用比自己小一个规模的自己. 2.有结束条件. 3.对问题的细化. ps: 大家可以通过这个效应感性的感受一下递归. 德罗斯特效应: ******************** ...

  2. 分治算法详细讲解(含经典例题分析)

    分治法思路: 将整个问题分解成若干小问题后再分而治之.如果分解得到的子问题相对来说还是太大,则可反复使用分治策略将这些子问题分成更小的同类型子问题,直至产生方便求解的子问题,必要时逐步合并这些子问题的 ...

  3. Oracle:经典例题解析

    1.环境准备 下面所有SQL语句都是建立在employees表和departments表上,具体表结构如下所示: departments表结构如下所示: 字段名 数据类型 长度 约束 说明 depar ...

  4. C语言递归及经典例题详解

    什么是递归? 什么时候使用递归 例题1 顺序打印问题 例题2 求n的阶乘 例题3 求第n个斐波那契数 经典 汉诺塔问题 经典 青蛙跳台阶问题 什么是递归? 递归就是程序调用自身的编程技巧.递归通常把一 ...

  5. 总结 贪心算法_这几道经典例题帮你轻松搞透贪心算法

    贪心算法概念叙述 运用贪心算法求解问题时,会将问题分为若干个子问题,可以将其想象成俄罗斯套娃,利用贪心的原则从内向外依次求出当前子问题的最优解,也就是该算法不会直接从整体考虑问题,而是想要达到局部最优 ...

  6. Hive经典例题解析

    1.第一题: sid name gender age academy dt chinese math english 95001 李勇 男 20 CS 2017-08-31 56 28 62 9500 ...

  7. 动态规划经典例题解析

    一.不同路径问题 题目描述 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为"Start" ). 机器人每次只能向下或者向右移动一步.机器人试图达到网格的右下角 ...

  8. 非主流自然语言处理——遗忘算法系列(二):大规模语料词库生成

     一.前言 写这篇文时,突然想到一个问题,大家的词库都是从哪来的? 之所以会这么有些意外的问,是因为从没把词库当成个事儿:平时处理微博,就用程序跑一下微博语料获得微博词库:处理新闻,程序跑一下新闻 ...

  9. 网络安全系列-二十七: 基于pkts.io解析pcap,生成五元组及payload

    pkts.io 是什么? pkts.io是一个用于读写pcaps的纯Java库.它的主要目的是操作/分析现有的pcaps,允许您围绕pcaps构建各种工具. 源码地址 目前使用JAVA解析Pcap的第 ...

最新文章

  1. 阿里内部不显示 P 序列职级,“高 P”光环成过去式?网友:这下可以装大佬了...
  2. linux查看命令类型,查看linux命令类型
  3. c语言获取dll文件路径,C语言URLDownloadToFile获取文件下载进度
  4. media player 控件播放音乐与视频 0130 winform
  5. 李沐:工作五年反思!
  6. Kafka和其他消息队列
  7. python3九九乘法表儿歌_python3: 简单4步骤输出九九乘法表
  8. ajaxsubmit方法的一种实现
  9. 班级网站java_基于jsp的班级网站-JavaEE实现班级网站 - java项目源码
  10. SDN学习笔记(一)
  11. 谷歌浏览器被2345主页强制绑定
  12. 【报告分享】2021中国住宿业市场网络口碑报告-中国饭店协会众荟(附下载)
  13. Win10 NVIDIA Container占用CPU高的处理方法
  14. 赋能建筑建材企业物流网络内外联通,B2B交易管理系统打造行业智慧供应链
  15. 小程序中如何实现即时通信聊天功能?
  16. 【STM32学习】(11)STM32 Mifare_One(S50) M1S50的使用(读、写、密钥修改,控制位解读)
  17. 3-wireshark网络安全分析——ARP欺骗攻击
  18. X3D - 一次编写、处处、时时都可渲染的三维图形格式
  19. 更新apple系统可以用哪些服务器,iPhone 是否能够升级至指定的 iOS 系统版本?
  20. Ubuntu 12.04 / Linux Mint 17.1 下安装TinyOS-2.1.1全过程

热门文章

  1. 【c++一本通】【堆】鱼塘钓鱼
  2. linux将时钟放在桌面上的,Linux 下的桌面指针时钟
  3. 程序员伪造邮件钓鱼,从入门到入土!
  4. 数据结构之图的基本概念
  5. 字节序:Big Endian 和 Little Endian
  6. 腾讯云云直播CSS产品概述和快速入门
  7. 高斯函数和C++简单实现
  8. CCSP2021 分赛区
  9. IPv6/ICMPv6-原理介绍+报文分析+配置示例
  10. 汽车零部件制造业ERP需求分析(转)