递归解题思维
作者:互联网
开篇
网上关于如何用递归解决问题方法的文章很多了,看了一些,我也来谈谈我的看法。
很多文章都介绍了递归解决方法,比如三步走。实际上我觉得那些只是用递归解题时候表现出来的的特征,而递归解题没有特别好的、通用的办法,只能靠多尝试,就有点类似做数学题一样,只不过遇到一个类型的题目的时候,你能快速借鉴案例。
用递归解题的意义
如果没有特别好的通用的方法,那么递归解题技术学来有什么用吗?我总结了三个意义:
1. 改写程序:理论上所有循环都可以写成递归,不是所有编程语言都支持循环。因此可能在特殊编程语言上你只能用递归来改写。
2. 锻炼思维:用来寻找问题表示的最极简形态,算极限的函数式编程方式之一。当然这种最极简形态不一定对所有问题适用,也可能更丑陋。
3. 比赛或适用特殊问题:有些问题合用递归表达。
所以对递归的使用能锻炼思维,类似于用多种解法来应对问题。
递归思维与数学归纳法
网上有谈论递归与数学归纳法的关系了,再谈谈我的理解。我们高中学的数学归纳法都只是当做个工具来用,相当于给了一把瑞士军刀,让你应对不同场景。其中最重要的一步是“归纳递推”也就是假设 N - 1 成立,那么对于N成立。假设要证明等式:
1+2+3+...+n = n(n+1)/2
对于上述等式,等号左边是问题的原始形态,有等号就表明这个问题有个超越循环方式的简化的表达方式态。解题时候就是要完美凑出和等式一样的形态,也就是N-1时候右侧的乘式:(n-1)n/2+n 要在N时候+n以后 凑出 n(n+1)/2 。这步叫“归纳递推” 。
所以让你用数学归纳法证明这个等式时候,等价于告诉你原始循环形态就存在一个归纳递推的形态,说明这种表达具有同构性,需要你完成具体的N-1到N的变化联系。而递归是反过来,不知道是否存在“归纳递推”可能,你需要假设问题具有同构性,设计出N与N-1 的联系,使得那个表达能自我繁殖扩展出问题原始形态。
递归解题的类型
从解题角度,我觉得一般分为两类,
一类是问题原始形态是比较明确的,那么只要压缩出那个N与N-1 的联系的方案。
另一类是问题搜索空间不明确,你只能想象N到N-1 的联系具有同构性,通过尝试微小的变化来看它能否扩展到问题原始形态。就好比盗梦空间那个把两面镜子合在一起构建出一条路一样。
一般能拿出来讲解的题都是第一类,或者能转成第一类的。锻炼思维时候已经把问题简单化,让你关注怎么压缩而已。
递归结题步骤
一般总结递归都是三步:
一、明确函数功能(参数是什么,问题要返回的结果是什么)
二、寻找递归出口、或停止条件(不能无限递归下去,得有个边界)
三、寻找递归结构、或叫递推关系(一层递归做什么事)
虽是三步,看起来按次序解开即可得到问题答案,但这三步其实是糅合在一起设计的,针对题目单独列步骤几乎都无法找到答案,那些例子就是恰好结构十分简单到可以套用进来作为例子讲解而已。真正解一般题没有万能步骤,比如前面说的第二类,有些循环改出递归方案甚至能拿圈内大奖(Apriori和FP-growth的关系)。
其中第1第3步又最难,也就是寻找根据函数设计同构性。其中第3步则包含对问题拆分出规模更小的子问题和由子问题结果组装两个部分的设计,而且拆装都涉及规模和结构。
递归思想是为了把大问题切分成子问题,层层解决,那么问题能不能以“层层”方式拆分就是个问号(也就是形参的n到更小规模的n-1)。只不过一般的标量、线性结构、树形结构等都是可拆的。拆有不同拆法,这决定了递归写法就不唯一。然而拆了以后,子问题结构如何装起来也是个问号,这是解题的关键,所以说递归求解没有通用办法。下面用几个例子套一下。
递归例题
1. 求N阶乘
N!= N*(N-1)*(N-2)*...*2*1
这个题问题形态比较简单,就是一个循环N次的线性空间。首先设计一个函数来表示N阶乘的结果。先尝试设计成
int f(int n)
再考虑一层递归要做什么,也就是f函数要怎么反复用。如果有递推关系那就直接写出递归代码,如果没有就需要自己分析。N阶乘的递推关系是f(n) = n * f(n-1) 。 我们可以事后诸葛亮地看看这个递推关系已经包含了对问题拆分成子问题以及子问题如何组装:“拆”是每次n的规模问题拆成一个n-1规模子问题以及一个数n (n是一个数,拆成更小的数用减法, n-k =n - k );“装”是这个数 与 子问题相乘。
最后需要确定一个停止条件,也就是f(1) = 1 。那么把f(n)两种情况写出来的python代码就是:
def f(n):
if n == 1:
return 1
else:
return n * f(n-1)
2. 找出数组中的最小值
数组求最小值也是一个循环N次的线性空间,这里需要用到一个不使用循环的最小值函数,也就是min(int a,int b)。先尝试一下写个函数:
int findmin(int[] arr)
那么 首先需要对形参arr进行拆分,以及f()返回值的组装。arr是一个数组,“拆”出更小的子问题可以有两种选择:1. 拆成一个数和一个小一点的数组,2. 拆成两个数组。 “装”从min(a,b)角度思考一下,a、b都是int, 可以是一个数,也可以是findmin的结果。 所以一层递归有两种写法,min(一个数, 子问题结果) 或者 min(子问题结果1, 子问题结果2)。最后停止条件也容易找到,即arr数组只有一个元素时候,返回唯一的数作为结果。选择1的python代码如下:
def findmin(arr):
if len(arr) == 1:
return arr[0]
else:
return min(arr[0], findmin(arr[1:]))
选2的python代码如下:
def findmin(arr):
if len(arr) == 1:
return arr[0]
else:
mid = len(arr) // 2
return min(findmin(arr[:mid]), findmin(arr[mid:]))
3. 字符串翻转
字符串是字符数组,同样是线性搜索空间,先来设计函数:
string reverse_s( string s)
同样关注字符串拆和构两点,字符串的拆就如同数组一样切成多个部分,“拆”同样可以像上一题有两种方案:1. 拆成一个字符和剩下的字符串,2. 拆成两个字符串。“装”的时候因为需要翻转,所以每次拼接需要把头接到新字符串的尾。最后停止条件也容易找到,当字符串长度是1,则取出唯一的字符。方案2的python代码实现如下:
def reverse_s(s):
if len(s) == 1:
return s
else:
mid = len(s) // 2
return reverse_s(s[mid:]) +reverse_s(s[:mid])
小结
这三个题都是天然适合用递归表达,虽然完美套进三个步骤中,但你会发现看完即便看懂,这点思路也不足让你解其他题。因为几个步骤是糅合影响设计过程的,在所有案例讲解中都难以体现。如果非要细究一下,那么拆和装是最有值得回味的地方,三个题分别列出几种拆和装的方式:
三个题拆的地方有:1. 一个数拆成更小的数;2. 一个数组拆成一个元素+一个更小的数组; 3. 一个数组拆成两个数组。 拆既要考虑拆规模也要考虑拆结构、其中结构拆分是与步骤一紧密相关。
三个题装的地方有:求乘积、求最小值、字符拼接。组装时候还需要考虑顺序,例如求乘积和求最小值不需要关心顺序,而拼接需要;还要考虑结构问题,这也与步骤一紧密相关。
总结一下其实解题中拆规模不是最大问题,因为规模都是可以拆的,关键是地方的拆出来的结构是否能套进步骤一设计的函数中。也许这才是寻找递归方案的核心吧,后续我还会用一些案例来分析一下我的想法是否足够正确。
标签:拆成,思维,arr,递归,问题,解题,数组 来源: https://blog.csdn.net/lgnlgn/article/details/122020839