一. 什么是递归?

  • 定义:递归(recursion):程序调用自身的编程技巧称为递归。递归做为⼀种算法在程序设计语⾔中广泛应⽤。是⼀个过程或函数在其定义或说明中有直接或间接调⽤⾃身的⼀种⽅法。
    递归的主要思考方式在于:把大事化小。它通常把⼀个⼤型复杂的问题层层转化为⼀个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
  • 递归的两个条件:
    (a)存在限制条件,当满⾜这个限制条件的时候,递归便不再继续。
    (b)每次递归调⽤之后越来越接近这个限制条件。

二.如何实现递归?

此处先给出一个递归模板:

public void recursion(参数0) {
    if (终止条件) {
        return;
    }
    recursion(参数1);
}

接下来我们通过“用递归的方法实现一个阶乘的计算”的例子来详细了解一下递归的思想。
代码设计思路:

第一步,我们需要明确我们所设计的函数的功能是计算一个数的阶乘。那么,首先进入我们脑海的事情应该是阶乘的数学表达公式(也可以是我们首先Google的内容),factorial(n)=1×2×3×……×(n-2)×(n-1)×n。
第二步,在这个式子中,我们会发现当n=1,2时,其阶乘结果就是n本身,我们都不需要往计算公式中带入就可以很快得到结果。
第三步,我们会发现如果当n的值取的比较大时,计算factorial(n)这个阶乘的范围也会变得很大,所以我们需要找到一个办法尽可能的缩小计算范围。通过分析数学计算公式,我们发现可以让 factorial(n) = n ×f(n-1)。这样,范围就由 n 变成了 (n-1)了,范围变小了,并且原函数f(n)的计算结果不变。说白了,就是我们找到原函数的一个等价关系式,factorial(n) 的等价关系式为 n×(n-1),即factorial(n) = n ×f(n-1)。

根据上面的思路,代码可以写如下所示:

public int factorial(int n) {
    if (n <= 2)
        return n;
return n * factorial(n - 1);}

这是一个简单的递归实现,通过代码我们不难看出,代码第2-3行是用来判断程序的的临界点,即终止条件,第4行是调用函数本身,实现递归。下图是当n取5的时候,函数递归调用的具体流程。

resampler C语言代码_编程语言


这种递归比较简单,我们求factorial(5)的时候,只需要求出factorial(4)即可,如果求factorial(4)我们要求出factorial(3)……,一层一层的调用,当n=1的时候,我们直接返回1,然后再一层一层的返回,直到返回factorial(5)为止。

根据上面的设计思路,对于如何把递归思想用程序表达出来,我确定了三个步骤:

第一步:确认函数的主要功能
对于递归,我觉得很重要的一个事就是,这个函数的功能是什么,他要完成什么样的一件事,而这个,是完全由我们自己来定义的。也就是说,我们先不管函数里面的具体得代码逻辑是什么,而是要先明白,你这个函数是要用来干什么?你希望通过这个函数最终返回的结果是什么?
第二步:寻找递归结束条件
所谓递归,就是在函数内部代码中调用这个函数本身,所以,我们必须要找出递归的结束条件,不然的话,函数会一直调用自己,从而进入一个死循环。也就是说,我们需要找出当参数是什么或者处于某种状态时,递归成立的条件无法满足而结束递归过程,之后直接把处于递归结束条件的函数结果返回,也就是说,这个时候我们必须能根据此刻函数参数的值,直接知道函数的结果是什么。
第三步:找出问题等价关系,进行逻辑传递
这一步骤中我们需要做的就是要不断缩小参数的范围,通过函数自身的功能,找出问题中可以循环的逻辑,将问题解决规模缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。

接下来,让我们通过几个代码实例来进一步了解递归以及遇到递归问题如何下手:

  1. 斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21…,即第一项 f(1) = 1,第二项 f(2) = 1…,第 n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。

解题思路:
第一步,先确定f(n) 的功能是求第 n 项的值。
第二步,显然,当 n = 1 或者 n = 2 ,我们可以轻易地知道结果f(1) = f(2) = 1。所以递归结束条件可以 为 n <= 2 时,f(n)= = 1。
第三步,题目已经把等价关系式给我们了,所以我们很容易就能够知道 f(n) = f(n-1) + f(n-2)。

int fib(int n)
{
	if (n <= 2)
		return  1;
	else
		return  fib(n - 1) + fib(n - 2);
}
int main()
{
	int n = 0;
	int ret = 0;
	printf("请输入你要查询的数字:");
	scanf("%d", &n);
	ret = fib(n);
	printf("第%d个斐波那契数字是%d:\n", n,ret);
	system("pause");
	return 0;
}

resampler C语言代码_经验分享_02

  1. 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

解题思路:
第一步,先确定f(n) 的功能是求青蛙跳上一个n级的台阶总共有多少种跳法。
第二步,求递归结束的条件,直接把 n压缩到很小很小就行了,因为 n 越小,我们就越容易直观着算出 f(n) 的多少,所以当 n = 1时,我们能直观地看出 f(1) =1。
第三步,我们要找出函数等价关系,每次跳的时候,小青蛙可以跳1个台阶,也可以跳2个台阶,也就是说,每次跳的时候,小青蛙有2种跳法。 第一种跳法:第1次我跳了1个台阶,那么还剩下n-1个台阶还没跳,剩下的n-1个台阶的跳法有f(n-1)种。第二种跳法:第一次跳了2个台阶,那么还剩下n-2个台阶还没,剩下的n-2个台阶的跳法有f(n-2)种。所以,小青蛙的全部跳法就是这两种跳法之和了,即 f(n) = f(n-1) + f(n-2)。至此,等价关系式就求出来了。

int f(int n) {
	if (n <= 2) {
		return n;
	}
	else {
		return f(n - 1) + f(n - 2);
	}
}
int main() {
	int n = 4;
	printf("请输入台阶数:");
	scanf("%d", &n);
	f(n);
	printf("青蛙一共可以有%d种跳法!\n", f(n));
	system("pause");
	return 0;
}

resampler C语言代码_编程语言_03


接下来,我们在介绍几个递归思想经典的例题!

3.猴子第一天摘下N个桃子,当时就吃了一半,还不过瘾,就又多吃了一个。第二天又将剩下的桃子吃掉一半,又多吃了一个。以后每天都吃前一天剩下的一半零一个。到第10天在想吃的时候就剩一个桃子了.问第一天共摘下来多少个桃子?并反向打印每天所剩桃子数。

int getnum(int n) {
	int num;
	if (n == 10) {
		return 1;
	}
	else {
		num = ((getnum(n + 1) + 1) * 2);
		printf("%d天,猴子还剩%d个桃子!\n", n, num);
	}
	return num;
}
int main() {
	int n = 1;
	int num = getnum(n);
	printf("猴子第1天一共摘了%d只桃子!\n",num);
	system("pause");
	return 0;
}

resampler C语言代码_resampler C语言代码_04


4.如下图所示,从左到右有A、B、C三根柱子,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上面,求移动的步骤。

resampler C语言代码_c语言_05

void hanoi(char start, char tmp, char end, int n)
{
	if (1 == n)
	{
		printf("%d 从%c 柱到%c 柱\n ", n, start, end);
		return;
	}
	hanoi(start, end, tmp, n - 1);
	printf("%d 从%c 柱到%c 柱\n", n, start, end);
	hanoi(tmp, start, end, n - 1);
}
int main()
{
	int n = 0;
	printf("请输入A柱上盘子的数量:\n");
	scanf("%d", &n);
	hanoi('A', 'B', 'C', n);
	system("pause");
	return 0;
}

resampler C语言代码_c语言_06

三.递归的优缺点比较

优点:

1.简洁明了。
2.在树的前序,中序,后序遍历算法中,递归的实现明显要比循环简单得多。

缺点:

1.递归由于是函数调用自身,而函数调用是有时间和空间的消耗的:每一次函数调用,都需要在内存栈中分配空间以保存参数、返回地址以及临时变量,而往栈中压入数据和弹出数据都需要时间。->效率
2.递归中很多计算都是重复的,由于其本质是把一个问题分解成两个或者多个小问题,多个小问题存在相互重叠的部分,则存在重复计算,如斐波那契数列的递归实现。->效率
3.调用栈可能会溢出,其实每一次函数调用会在内存栈中分配空间,而每个进程的栈的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致栈溢出。->性能

四.递归和循环的比较

那么递归和循环之间有哪些区别呢?我在知乎上发现了下面两个例子,对比了递归和循环。

递归:你打开面前这扇门,看到屋里面还有一扇门(这门可能跟前面打开的门一样大小(静),也可能门小了些(动)),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开,若干次之后,你打开面前一扇门,发现只有一间屋子,没有门了。你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这钥匙开了几扇门。

循环:你打开面前这扇门,看到屋里面还有一扇门,(这门可能跟前面打开的门一样大小(静)也可能门小了些(动)),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,(前面门如果一样,这门也是一样,第二扇门如果相比第一扇门变小了,这扇门也比第二扇门变小了(动静如一,要么没有变化,要么同样的变化)),你继续打开这扇门。。。,一直这样走下去。入口处的人始终等不到你回去告诉他答案。

因此,不难发现递归与循环的相同点和不同点在于:

相同点:
(1)都是通过控制一个变量的边界(或者多个),来改变多个变量为了得到所需要的值,而反复而执行的;
(2)都是按照预先设计好的推断实现某一个值求取;(请注意,在这里循环要更注重过程,而递归偏结果一点)。

不同点:
(1)递归通常是逆向思维居多,“递”和“归”不一定容易发现(比较难以理解);而循环从开始条件到结束条件,包括中间循环变量,都需要表达出来(比较简洁明了)。

简单的来说就是:
用循环能实现的,递归一般可以实现,但是能用递归实现的,循环不一定能。
因为有些题目①只注重循环的结束条件和循环过程,而往往这个结束条件不易表达(也就是说用循环并不好写);②只注重循环的次数而不注重循环的开始条件和结束条件(这个循环更加无从下手了)。