递归函数的通俗原理(解读递归算法原理和效率)

递归的定义
在数学与计算机科学中,递归(Recursion)是指在函数的定义中使用函数自身的方法。实际上,递归,顾名思义,其包含了两个意思:递和归,这正是递归思想的精华所在。
通俗点讲,我们可以把” 递归 “比喻成 “查字典 “,当你查一个词,发现这个词的解释中某个词仍然不懂,于是你开始查这第二个词。
可惜,第二个词里仍然有不懂的词,于是查第三个词,这样查下去,直到有一个词的解释是你完全能看懂的,那么递归走到了尽头,然后你开始后退,逐个明白之前查过的每一个词,最终,你明白了最开始那个词的意思。
递归的思想
递归就是有去(递去)有回(归来),如下图所示。“有去”是指:递归问题必须可以分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决,就像上面例子中的钥匙可以打开后面所有门上的锁一样;“有回”是指 : 这些问题的演化过程是一个从大到小,由近及远的过程,并且会有一个明确的终点(临界点),一旦到达了这个临界点,就不用再往更小、更远的地方走下去。最后,从这个临界点开始,原路返回到原点,原问题解决。

递归的三大要素
- 明确递归终止条件;
- 给出递归终止时的处理办法;
- 提取重复的逻辑,缩小问题规模;
明确递归终止条件
我们知道,递归就是有去有回,既然这样,那么必然应该有一个明确的临界点,程序一旦到达了这个临界点,就不用继续往下递去而是开始实实在在的归来。换句话说,该临界点就是一种简单情境,可以防止无限递归。
给出递归终止时的处理办法
我们刚刚说到,在递归的临界点存在一种简单情境,在这种简单情境下,我们应该直接给出问题的解决方案。一般地,在这种情境下,问题的解决方案是直观的、容易的。
提取重复的逻辑,缩小问题规模
我们在阐述递归思想内涵时谈到,递归问题必须可以分解为若干个规模较小、与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决。从程序实现的角度而言,我们需要抽象出一个干净利落的重复的逻辑,以便使用相同的方式解决子问题。
常见递归算法
下面总结一下常见的递归问题和实现算法。
斐波那契数列
斐波那契数列的排列是:0,1,1,2,3,5,8,13,21,34,55,89,144……依次类推下去,你会发现,它后一个数等于前面两个数的和。在这个数列中的数字,就被称为斐波那契数。
递归思想:一个数等于前两个数的和。
首先分析数列的递归表达式:

代码如下:
/** * 斐波那契数列的递归写法 * @param n * @return */ long F(int n){ if (n<=1) return n; return F(n-1)+F(n-2); }
可以看到,递归写法简单优美,省去考虑很多边界条件的时间。当然,递归算法会保存很多的临时数据,类似于堆栈的过程,如果栈深太深,就会造成内存用尽,程序崩溃的现象。
阶乘
递归思想:n! = n * (n-1)!
首先分析数列的递归表达式:

代码如下:
long factorial(int n){ if (n <=1) return 1; return j(n-1)*n; }
倒序输出一个正整数
例如给出正整数 n=12345,希望以各位数的逆序形式输出,即输出54321。
递归思想:首先输出这个数的个位数,然后再输出前面数字的个位数,直到之前没数字。
首先分析数列的递归表达式:

代码如下:
/** * 倒序输出正整数的各位数 * @param n */ void printDigit(int n){ System.out.print(n%10); if (n > 10){ printDigit(n/10); } }
汉诺塔
数学描述就是:
有三根杆子X,Y,Z。X杆上有N个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至Y杆:
- 每次只能移动一个圆盘;
- 大盘不能叠在小盘上面。
递归思想:
- 将X杆上的n-1个圆盘都移到空闲的Z杆上,并且满足上面的所有条件
- 将X杆上的第n个圆盘移到Y上
- 剩下问题就是将Z杆上的n-1个圆盘移动到Y上了
公式描述有点麻烦,用语言描述下吧:
- 以Y杆为中介,将前n-1个圆盘从X杆挪到Z杆上(本身就是一个n-1的汉诺塔问题了!)
- 将第n个圆盘移动到Y杆上
- 以X杆为中介,将Z杆上的n-1个圆盘移到Y杆上(本身就是一个n-1的汉诺塔问题了!)
代码如下:
/** * 汉诺塔 * 有柱子 x z y,最终将x上的n个圆盘借助z移动到y上 * 递归思想: * 1.将x上的n-1个放入到z上,借助y * 2.将x上的n圆盘放到y上 * 3.将z上的n-1个圆盘放入y * @param n * @param from * @param tmp * @param to */ void hanoi(int n,char from,char tmp,char to){ if (n>0) { hanoi(n - 1, from, to, tmp); System.out.println("take " + n + " from " + from + " to " + to); hanoi(n - 1, tmp, from, to); } }
递归的效率
还是拿斐波那契数列来做例子:
long Fib(int n){ if (n<=1) return n; return Fib(n-1)+Fib(n-2); }
这段代码应该算是短小精悍(执行代码只有一行),直观清晰,而且非常符合许多程序员的代码美学,是如果用这段代码试试计算Fib(1000)我想就再也爽不起来了,它的运行时间也许会让你抓狂。
看来好看的代码未必中用,如果程序在效率不能接受那美观神马的就都是浮云了。如果简单分析一下程序的执行流,就会发现问题在哪,以计算Fibonacci(5)为例:

从上图可以看出,在计算Fib(5)的过程中,Fib(1)计算了两次、Fib(2)计算了3次,Fib(3)计算了两次,本来只需要5次计算就可以完成的任务却计算了9次。这个问题随着规模的增加会愈发凸显,以至于Fib(1000)已经无法再可接受的时间内算出。
我们当时使用的是简单的用定义来求 fib(n),也就是使用公式 fib(n) = fib(n-1) + fib(n-2)。这样的想法是很容易想到的,可是仔细分析一下我们发现,当调用fib(n-1)的时候,还要调用fib(n-2),也就是说fib(n-2)调用了两次,同样的道理,调用f(n-2)时f(n-3)也调用了两次,而这些冗余的调用是完全没有必要的。可以计算这个算法的复杂度是指数级的。
由以上分析我们可以看到,递归在处理问题时要反复调用函数,这增大了它的空间和时间开销,所以在使用迭代可以很容易解决的问题中,使用递归虽然可以简化思维过程,但效率上并不合算。效率和开销问题是递归最大的缺点。
虽然有这样的缺点,但是递归的力量仍然是巨大而不可忽视的,因为有些问题使用迭代算法是很难甚至无法解决的。这时递归的作用就显示出来了。
茜之塔又名绿塔,属景天科、青锁龙属,多年生肉质草本植物,因叶片肥厚、叶形奇特、四季常绿、叶片排列紧密,并从下向上逐渐变小,整株就像是由一个个小小的宝塔山,看起来非常的精致美观,室内栽培可用直径14至20cm的泥质、陶质、瓷质花盆栽培,为与家庭装潢相协调,也可用工艺瓷质花盆栽培,可放置于有光照的卧室窗
近年来,伴随着我国的经济发展,出口贸易已经成为我国经济发展的重要支柱性产业,2022年,我国外贸进出口总值达到42.07万亿元,连续10年占据贸易总额世界第一的位置。但与此同时,全球经济的衰退趋势和逆全球化现象不断加重,导致贸易保护主义思潮在各个国家蔓延,各国纷纷通过政策制度、关税壁垒等手段限制他国
一、头虱问题真的可怕吗?头虱,一种生活在人类头皮和头发上的小寄生虫,确实不会直接导致严重的健康问题。它们的主要食物来源是人类的血液,而它们以头皮作为栖息地,来繁殖并继续他们的生命周期。头虱并不无害。他们的存在可能会引起一系列的不适和症状。头虱会导致瘙痒感。当头虱咬人时,他们会注入唾液以抑制血液凝固。
【一】一唱一和、一呼百应、一干二净、一举两得一落千丈、一模一样、一曝十寒、一心一意一败涂地、一本正经、一臂之力、一目了然一窍不通、一视同仁、一泻千里、一草一木一成不变、一帆风顺、一鼓作气、一哄而散一如既往、一丝不苟、一言难尽、一衣带水一朝一夕、一针见血、一知半解、一表人才一波三折、一步登天、一筹莫展