这一章节比较简单,就归纳总结一下递归的思想。
0 定义及内涵
这一思想应该是简单而重要的一种算法思想,所以我们也最先介绍一下其思想和应用。可以参考博客的介绍:https://blog.csdn.net/sinat_38052999/article/details/73303111
还有知乎一篇:https://www.zhihu.com/question/20507130/answer/15551917
递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。
英文的Recursion从词源上分析只是"re- (again)" + "curs- (come, happen)" 也就是重复发生,再次重现的意思。 而对应的中文翻译 ”递归“ 却表达了两个意思:”递“+”归“。 这两个意思,正是递归思想的精华所在。从这层次上来看,中文翻译反而更达意。
首先不得不提的是递归与循环的区别:
递归是静中有动,有去有回。
循环是动静如一,有去无回。
循环,在编程语言中就是while/for循环那些,可以想象就是,每一次动作都是相似的过程,可能每次都一样,也可能每次发生相同的变化,最后达到循环停止的条件(如满足一定的次数,或者某个变量达到限制条件/不定次数)。
递归呢?
还是要体会,有去有回区别于循环;循环也可能是从第一扇门走到最后一扇门,但是你往往拿着最后结果就从最后一扇门溜了?;递归一定是遇到了最后一扇门,终于拿到了一个结果,然后我们循这旧路走回来继续计算,走到第一扇门得到最后结果。
递归的基本思想是把规模大的问题转化为规模小的相似的子问题来解决。在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。另外这个解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况了。
1 递归的应用场景
在我们实际学习工作中,递归算法一般用于解决三类问题:
(1). 问题的定义是按递归定义的(Fibonacci函数,阶乘,…);
(2). 问题的解法是递归的(有些问题只能使用递归方法来解决,例如,汉诺塔问题,…);
(3). 数据结构是递归的(链表、树等的操作,包括树的遍历,树的深度,…)。
上面的博客中也很详细的列出了各种应用的代码解析,就不再搬运了。虽然不是python语言的,相信不耽误理解这一思想,这些问题往往也不难,就不改写python版本了。
需要说明两点。
1)每一个递归程序都可以把它改写为非递归版本。我们只需利用栈,通过入栈和出栈两个操作就可以模拟递归的过程,二叉树的遍历无疑是这方面的代表。再横插一句,递归的实现原理就是内存栈。非递归也可以解决问题。
简洁性:但是并不是每个递归程序都是那么容易被改写为非递归的。某些递归程序比较复杂,其入栈和出栈非常繁琐,给编码带来了很大难度,而且易读性极差,所以条件允许的情况下,推荐使用递归。用递归来解决这些问题,往往几行代码就搞定了一些看起来相当”吓人“的问题。
性能考虑:当然,递归的性能问题是另一回事,栈的分配,函数调用代价都是在具体工程实践中要考虑的。 由于不断递归,如果该问题的递归调用深度极大,那么递归的程序运行时恐怕就要爆炸了。此时,还是建议使用非递归来实现。
2)谈到数据结构是递归的,最典型的就是树了,我们随后即将介绍树的所有知识,届时会深刻体会递归思想。还有二分查找算法,是我们下一章节的内容,也是典型的递归思想。因此,本章也只是递归算法应用的打头阵。而递归的重要性也可见一斑了。
2 递归算法设计
递归的基本思想就是把规模大的问题转化为规模小的相似的子问题来解决。特别地,在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况,这也正是递归的定义所在。格外重要的是,这个解决问题的函数必须有明确的结束条件,否则就会导致无限递归的情况。
因此,若要设计一个递归的算法程序,需要认知递归三要素:
1、将一个问题化简到更小的规模,类似于数学归纳法的步骤;
2、父问题与子问题不能有重叠的部分,通过子问题的解可以解决父问题;
3、确定递归终止的条件,比如树到达叶子节点,二分查找时left等于right
我们在阐述递归思想内涵时谈到,递归问题必须可以分解为若干个规模较小、与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决。从程序实现的角度而言,我们需要抽象出一个干净利落的重复的逻辑,以便使用相同的方式解决子问题。