【C语言(四)】

一、函数递归

1.1、什么是函数递归?

递归是学习C语⾔函数绕不开的⼀个话题,那什么是递归呢?
递归其实是⼀种解决问题的方法,在C语⾔中,递归就是函数自己调⽤自己。

但是递归需要注意,如果程序“无脑”递归的话,代码最终会陷入死递归,导致栈溢出(Stack overflow)

【C语言(四)】_第1张图片

递归的思想:

把⼀个大型复杂问题层层转化为⼀个与原问题相似,但规模较小的子问题来求解;直到子问题不能再被拆分,递归就结束了。所以递归的思考方式就是把大事化小的过程。

1.2、递归的限制条件

递归在书写的时候,有2个必要条件:
• 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
• 每次递归调用之后越来越接近这个限制条件。

1.3、递归举例

1.3.1、举例一:求n的阶乘

我们知道n!的公式:n! = n * (n-1)!

例如:5! = 5 * 4 * 3 * 2 * 1

        4! = 4 * 3 * 2 * 1

故:   5! = 5 * 4!

这个思路就是我们上述说过的把大事化小:把一个较大类型的问题,转化成一个与原问题相似,但规模较小的问题来求解。

n! ---> n * (n-1)!

(n-1)! ---> (n-1) * (n-2)!

...如此类推

直到n是1或者0时则不在拆解

再稍微分析⼀下,当 n<=1 的时候,n的阶乘是1,其余n的阶乘都是可以通过上述公式计算。
n的阶乘的递归公式如下:
【C语言(四)】_第2张图片

那我们就可以写出函数Fact求n的阶乘,假设Fact(n)就是求n的阶乘,那么Fact(n-1)就是求n-1的阶乘,函数如下(当然我们此处不考虑n过大的情况,因为n过大容易造成溢出):

int Fact(int n)
{
	int ret = 0;
	if (n == 0)
		return 1;
	else
		return n * Fact(n - 1);
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fact(n);
	printf("%d\n", ret);
	return 0;
}

我们可以画一幅图来直观的推演上述过程:

【C语言(四)】_第3张图片

1.3.2 、举例二:顺序打印一个数的每一位

输⼊⼀个整数m,打印这个按照顺序打印整数的每⼀位。
比如:
输⼊:1234 输出:1 2 3 4
输⼊:520   输出:5 2 0

这个题目,放在我们面前,首先想到的是,怎么得到这个数的每⼀位呢?
如果n是⼀位数,n的每⼀位就是n自己,n是超过1位数的话,就得拆分每⼀位,1234%10就能得到4,然后1234/10得到123,这就相当于去掉了4,然后继续对123%10,就得到了3,再除10去掉3,以此类推。不断的 %10 和 \10 操作,直到1234的每⼀位都得到;但是这里有个问题就是得到的数字顺序是倒着的。
但是我们有了灵感,我们发现其实⼀个数字的最低位是最容易得到的,通过%10就能得到,那我们假设想写⼀个函数Print来打印n的每⼀位,如下表示:

Print(n)
如果n是1234,那表⽰为
Print(1234) //打印1234的每⼀位

其中1234中的4可以通过%10得到,那么
Print(1234)就可以拆分为两步:
1. Print(1234/10) //打印123的每⼀位
2. printf(1234%10) //打印4
完成上述2步,那就完成了1234每⼀位的打印

那么Print(123)⼜可以拆分为Print(123/10) + printf(123%10)

以此类推下去,就有
   Print(1234)
==>Print(123) + printf(4)
==>Print(12) + printf(3)
==>Print(1) + printf(2)
==>printf(1)

直到被打印的数字变成⼀位数的时候,就不需要再拆分,递归结束。
那么代码完成也就⽐较清楚:
Print(int n)
{
	if (n > 9)
	{
		Print(n / 10);
	}
	printf("%d ", n % 10);
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	//输入一个整数n,按照顺序打印整数中的每一位
	Print(n);

	/*while (n)
	{
		printf("%d\n", n % 10);
		n / 10;
	}*/

	return 0;
}

在这个解题的过程中,我们就是使用了大事化小的思路
把Print(1234)打印1234每⼀位,拆解为首先Print(123)打印123的每⼀位,再打印得到的4
把Print(123)打印123每⼀位,拆解为首先Print(12)打印12的每⼀位,再打印得到的3
直到Print打印的是⼀位数,直接打印就行。

下面我们可以画一张图来推演一下过程:

【C语言(四)】_第4张图片

二、递归与迭代 

kkkkk举例:求第n个斐波那契数

我们也能举出更加极端的例子,就像计算第n个斐波那契数,是不适合使⽤递归求解的,但是斐波那契数的问题通过是使用递归的形式描述的,如下:
 【C语言(四)】_第5张图片

看到这公式,很容易诱导我们将代码写成递归的形式,如下所示:

int count = 0;
int Fib(int n)
{
	if (n == 3)
		count++;
	if(n <= 2)
		return 1;
	else
	{
		return Fib(n - 1) + Fib(n - 2);
	}
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("ret = %d\n", ret);
	printf("count = %d\n", count);
	return 0;
}

我们设置了一个变量count,来记录n = 3(统计第3个斐波那契数被计算的次数)的时候所需要计算的次数,当我们输入n = 40时我们可以看一下这个数的庞大:

【C语言(四)】_第6张图片而当我们输入50时这个结果将会跑很长时间...

当我们n输⼊为50的时候,需要很长时间才能算出结果,这个计算所花费的时间,是我们很难接受的,这也说明递归的写法是非常低效的,那是为什么呢?
【C语言(四)】_第7张图片

其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计算,而且递归层次越深,冗余计算就会越多。

所以斐波那契数的计算,使⽤递归是⾮常不明智的,我们就得
想迭代的方式解决。我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从小到大计算就行了
 

//迭代的方式
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;
	while (n >= 3)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("ret = %d\n", ret);
	return 0;
}

迭代的⽅式去实现这个代码,效率就要⾼出很多了。
有时候,递归虽好,但是也会引入⼀些问题,所以我们⼀定不要迷恋递归,适可而止就好。

三、拓展学习

3.1、青蛙跳台阶问题

//青蛙跳台阶问题
//有一只青蛙一次可以跳一个台阶,也可以一次跳两个台阶
//问,跳到第n个台阶有多少种跳法? 

【C语言(四)】_第8张图片

我们以冷酷脸代替青蛙哈,借助此图分析一下跳台阶的过程:

①当青蛙跳到第一阶台阶:小青蛙只有一种跳法

   当青蛙跳到第二阶台阶:小青蛙有两种跳法

   当青蛙跳到第三阶台阶:小青蛙有三种跳法

   ...以此类推

我们发现这种情况有点像我们刚才讲过的斐波那契而数列

②前两级台阶总是固定的,不会有变化,而如果台阶数大于等于3,那么小青蛙每一次跳台阶的选择会不同,它可以选择跳一阶台阶,也可以选择跳两阶台阶,将这两种情况合并,将会得到我们要的跳法的总数(注意:我们这里要求的是跳法,而不是跳跃的台阶数量,n代表的的是第几级台阶)!

③假如台阶数为n,n的时候我们有两种跳法,一是可以从n - 1直接蹦上了,二是从n - 2蹦上来,则我们可以将其分解成(n - 1)和(n - 2)。同理(n - 1)同样也有两种跳法,这就利用到我们说的递归了。

代码如下:

int Jump(int n)
{
	if (n == 1)
		return 1;
	else if (n == 2)
		return 2;
	else
	{
		return Jump(n - 1) + Jump(n - 2);
	}
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	int sum = Jump(n);
	printf("%d\n", sum);
	return 0;
}

3.2、汉诺塔 

汉诺塔简介:

汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

【C语言(四)】_第9张图片

3.2.1、图解圆盘移动 

①一个圆盘的移动

【C语言(四)】_第10张图片 一个圆盘的移动非常简单:一次移动从A --> C

②两个圆盘的移动

【C语言(四)】_第11张图片

【C语言(四)】_第12张图片【C语言(四)】_第13张图片【C语言(四)】_第14张图片

两个圆盘的移动:移动两次A --> B | A --> C | B --> C

③三个圆盘的移动

【C语言(四)】_第15张图片【C语言(四)】_第16张图片【C语言(四)】_第17张图片【C语言(四)】_第18张图片【C语言(四)】_第19张图片【C语言(四)】_第20张图片【C语言(四)】_第21张图片【C语言(四)】_第22张图片

三个圆盘的移动:移动七次A --> C | A --> B | C --> B | A --> C | B --> A | B --> C | A --> C

3.2.2、思路和分析 

当我们考虑三个以上的圆盘转移时将会非常困难, 对比上述三类移动,我们可以归纳:每一次的移动均是把A上的n - 1个圆盘先经过C移动到B上,再将A上的第n个(也就是最后一个),直接移动到C上,最后再把B上的n-1个圆盘整体通过A移动到C上。

综上所述:我们可以发现递归的最后限制条件就是n = 1时,只有一个圆盘的情况下,只需要将其直接移动到目标位置即可。

从第二个圆盘开始,它们都必须经历的步骤:①将A上n-1个圆盘整体移动到B柱(中转柱)上。②再将A上第n个圆盘移动到C柱(目标柱)上。③再将n-1个圆盘从B柱上整体移动到C柱上。

上述操作中的①又可以分解成:①将A上n-2个圆盘通过①整体移动到C柱(中转柱)上。②将A上第n-1个圆盘直接移动到B柱(目标柱)上。③再将n-2个圆盘整体从C柱移动到B柱上。

上述操作中的③也可以分解成:①将B柱上n-2个圆盘通过①移动到A柱(中转柱)上。②将B柱上第n-1个圆盘直接移动到C柱(目标柱)上。③将A柱上n-2个圆盘整体移动到C柱上。

3.2.3、代码实现

void Move(char a ,int n , char b)
{
	printf("将编号为%d的圆盘从%c移动到%c\n", n, a, b);
}

void Hanoi(int n , char A , char B , char C)
{
	if (n == 1)
		Move(A, 1, C);
	else
	{
		Hanoi(n - 1, A, C, B);
		Move(A , n ,C);
		Hanoi(n - 1, B, A, C);
	}
}

int main()
{
	int n = 0;
	char A = 'A';
	char B = 'B';
	char C = 'C';
	scanf("%d", &n);
	Hanoi(n, A, B, C);
	return 0;
}

你可能感兴趣的:(c语言)