C语言——递归(例题中有汉诺塔问题详解)

一、递归定义

程序调用自身的编程技巧称为递归(recursion)。

递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的。

void recursion()
{
   statements;
   recursion();
}

函数调用函数本身。

C 语言支持递归,即一个函数可以调用其自身。但在使用递归时,程序员需要注意定义一个从函数退出的条件,否则会进入死循环。

递归函数在解决许多数学问题上起了至关重要的作用,比如计算一个数的阶乘、生成斐波那契数列,等等。

二、递归的必要条件

存在限制条件,当满足这个限制条件的时候,递归便不再继续。

每次递归调用之后越来越接近这个条件。

例1:输入一个整数,按顺序打印每一位数字

#include <stdio.h>

void PrintDigit(int n)
{
	if (n > 9)
	{
		PrintDigit(n / 10);
	}
	printf("%d ",n % 10);
}

int main()
{
	int num = 0;
	scanf("%d",&num);
	PrintDigit(num);
	return 0;
}

打印结果是1 2 3 4 

就是相当于用这个函数一直除10,除到只有一位数的时候就打印。

先递后归,名为递归

先递:先除以10到个位,后归:再从后往前依次打印。

递归是嵌套的调用函数,进入函数的过程是递去,函数返回来的过程是归来;递进结束时开始回归。

递归应当存在限制条件,当逐渐接近并达到这个条件时函数就开始回归;如果没有这个限制条件,函数就会一直递下去,直至栈溢出(Stack overflow)才会结束。

每一次函数调用都要在栈区申请空间,无限递进就导致栈区空间被占满。

例2、写一个函数求字符串长度,要求使用递归

#include <stdio.h>

int StringLength(char* str)			 //传来的参数str是数组首元素的地址
{
	if (*str == '\0')//只要遇到\0就结束递去,开始归来
	{
		return 0;
	}
	return 1 + StringLength(str + 1);//只要不是\0返回值就加一
	//(str + 1)可以实现数组元素的遍历
}//此处不能使用str++,因为str++是先使用再运算,会导致无限递归
 //最好也不要使用++str,虽然++str可以实现功能,但它会导致str的值发生变化

int main()
{
	char ch[] = "abc";
	int len = StringLength(ch);
	printf("%d",len);
	return 0;
}

三、递归与迭代

迭代是函数内某段代码实现循环,迭代与普通循环的区别是,迭代中参与运算的变量同时是保存结果的变量。

引例:写一个函数计算斐波那契数列的某一项

递归

#include <stdio.h>

int Fibonacci(int n)
{
	if (n == 0)
	{
		return 0;
	}
	else if (n == 1)
	{
		return 1;
	}
	return Fibonacci(n - 1) + Fibonacci(n - 2);
}
int main()
{
	int i = 0;
    scanf("%d",&i);
	printf("%d\n", Fibonacci(i));
	
	return 0;
}

在要计算的项数较低时,没有太大问题,但是项数较大时,我们就会发现程序运行的时间很长。原因是这段代码的效率很低,时间复杂度是O(2^n)。

对于每一个非基本情况的输入n,函数Fibonacci(n)都会调用自身两次:Fibonacci(n-1)Fibonacci(n-2)。这意味着计算量随着n的增加呈现指数增长。

对于每个n值,以下是递归调用的情况:

  • Fibonacci(n) 调用 Fibonacci(n-1) 和 Fibonacci(n-2)
  • Fibonacci(n-1) 调用 Fibonacci(n-2) 和 Fibonacci(n-3)
  • Fibonacci(n-2) 调用 Fibonacci(n-3) 和 Fibonacci(n-4)
  • 以此类推...

可以注意到,对于大于等于3的n,Fibonacci(n-2)被计算了两次,其中一次是直接递归调用,另一次是通过Fibonacci(n-1)的调用。这种重复计算随着n的增长急剧增加,因此,这个函数的时间复杂度是指数级别的,具体来说是O(2^n)。

我们创建一个全局变量counter来计算第三项被计算了几次

#include <stdio.h>

int counter = 0;
int Fibonacci(int n)
{
	if (n == 3)
	{
		counter++;
	}
	if (n == 0)
	{
		return 0;
	}
	else if (n == 1)
	{
		return 1;
	}
	return Fibonacci(n - 1) + Fibonacci(n - 2);
}
int main()
{
	int i = 0;
	scanf("%d", &i);
	printf("%d\n", Fibonacci(i));
	printf("%d ",counter);
	return 0;
}

我们发现,要计算第四十项的值,要计算39088169次第三项,可见此代码效率很低下。

想要算40                                   项
就要算39                38               项
就要算38      37      37      36     项
就要算37 36 36 35 36 35 35 34项
就要算......

迭代

#include <stdio.h>

int Fibonacci(int n)
{
	int c = 1, a = 1, b = 1;
	for (int i = 1; i <= n - 2; i++)
	{
		c = a + b;
		a = b;
		b = c;
	}
	return c;
}
int main()
{
	int n = 0;
	scanf("%d",&n);
	int res = Fibonacci(n);
	printf("Fibonacci(%d) = %d",n,res);
	return 0;
}

四、栈溢出

引例:写一个函数计算阶乘

递归

#include <stdio.h>

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

int main()
{
	int num = 0;
	scanf("%d", &num);
	int res = Factorial(num);
	printf("%d",res);
	return 0;
}

在调试Factorial函数的时候,如果你的参数比较大,就会报错:stack overflow(栈溢出)这样的信息。

系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终导致栈空间耗尽的情况,这样的现象我们称为栈溢出。

那如何解决这个问题呢

1、将递归改写成非递归。

2、使用static对象代替nonstatic局部对象。在递归函数设计中,可以使用static对象代替nonstatic局部变量(即栈对象,因为局部变量是保存在栈区的),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

迭代

#include <stdio.h>

int Factorial(int n)
{
	int mul = 1;
	for (int i = 1; i <= n; i++)
	{
		mul = mul * i;//mul既参加运算又保存结果
	}
	return mul;
}

int main()
{
	int n = 1;
	scanf("%d", &n);
	int res = Factorial(n);
	printf("%d", res);
	return 0;
}

五、例题

汉诺塔

例1、汉诺塔问题,输入有几个盘,输出最优步数为几次

汉诺游戏规则如下:

1、有三根相邻的柱子,标号为A,B,C。

2、A柱子上从下到上按金字塔状叠放着n个不同大小的圆盘。

3、现在把所有盘子一个一个移动到柱子B上,并且每次移动同一根柱子上都不能出现大盘子在小盘子上方。

为了方便理解我们将柱子重新命名,以下用柱子的新名称

1、如果只有一个盘

我们用了一步

2、如果有两个盘

我们用了三步

其实我们可以把这个2个盘的情况转换成较少盘的情况,在这里是转化成1个盘的情况

可以把2个盘的情况转化成三次1个盘的情况

第一次1个盘

把甲盘移到辅助柱

第二次1个盘

把乙盘移到目的柱

第三次1个盘

把甲盘移到目的柱

把2个盘的情况转化成三次1个盘的情况,而1个盘的情况是需要一步的,所以2个盘的情况需要三步。

在这里我们发现,可以把较多盘的情况转换成较少盘的情况

下面再来看三个盘的情况

3、如果有三个盘

我们发现3个盘的情况需要七步

3个盘的情况可以转化为两次2个盘的情况+一次1个盘的情况

第一次2个盘的情况

这里的甲乙盘移动到辅助柱的中间步骤暂时省略

一次1个盘的情况

把丙盘移到目的柱

第二次2个盘的情况

这里的甲乙盘移动到目标柱的中间步骤暂时省略

在前面我们知道了2个盘的情况要三步,1个盘的情况需要一步

所以3个盘的情况就需要 2 * (2个盘的情况的步数) + 1个盘的情况的步数 = 2 * 3 + 1 = 7

所以3个盘的情况需要七步

这里,我们就大概发现了一些规律:

n个盘的情况就可以转化为两个(n - 1)个盘的情况 + 一个1个盘的情况

3个盘的情况

转换为(2个盘的情况+ 2个盘的情况) + 1个盘的情况

转化为(1个盘的情况 + 1个盘的情况 + 1个盘的情况 )+ (1个盘的情况 + 1个盘的情况 + 1个盘的情况) + 1个盘的情况

到最后都转化为了1个盘的情况,而1一个盘的情况只需要一步,所以就可以算出步数。

代码实现:

#include <stdio.h>

int Hanoi(int n)
{
	if (n == 1)
	{
		return 1;
	}
	return 2 * Hanoi(n - 1) + 1;
}

int main()
{
	int n = 0;
	scanf("%d",&n);
	int res = Hanoi(n);
	printf("%d",res);
	return 0;
}

小知识

汉诺塔最优步数的公式是pow(2,n) - 1,也就是2^n - 1

汉诺塔原题是出自印度传说,原题是有64个圆盘。

64个圆盘最优步数为2^64 - 1 = 18446744073709551615,这是个非常大的数

大约1.8E19,也就是1.8 * 10^19

我们知道unsigned long long的最大值为18446744073709551615

所以以下代码

#include <stdio.h>

unsigned long long Hanoi(int n)
{
	if (n == 1)
	{
		return 1;
	}
	return 2 * Hanoi(n - 1) + 1;
}

int main()
{
	int n = 0;
	scanf("%d",&n);
	unsigned long long res = Hanoi(n);
	printf("%llu",res);
	return 0;
}

运行结果是

也可以

#include <stdio.h>

double Hanoi(int n)
{
	if (n == 1)
	{
		return 1;
	}
	return 2 * Hanoi(n - 1) + 1;
}

int main()
{
	int n = 0;
	scanf("%d",&n);
	double res = Hanoi(n);
	printf("%e",res);
	return 0;
}

运行结果是

在这里我们稍微计算一下

假设一个人玩这个64阶的汉诺塔,假设他一秒只能移动好一个盘,而且每一步都是最优解,则它需要的时间是

是不是很令人震惊?

例2、汉诺塔问题,输入有几个盘,输出最优步数的具体操作流程

例如有三个盘就输出

初始柱->目的柱,

初始柱->辅助柱,

目的柱->辅助柱,

初始柱->目的柱,

辅助柱->初始柱,

辅助柱->目的柱,

初始柱->目的柱

这样就完成了游戏

这里的某个柱->另个柱是指把某个柱上第一个盘移动到另个柱的最上面

代码实现:

#include <stdio.h>

void MovePrint(char position1[], char position2[])
{   //控制打印位置1到位置2
    //这里的位置1和位置2都是不确定的
    //取决于传来的参数
    //只有打印的作用,使每一步具象化

	printf("%s->%s\n", position1, position2);
}												  
		   
void Hanoi(int n, char position1[], char position2[], char position3[])
{
//我们发现Hanoi函数在递归的归来过程
//中,只能实现position1->position3的
//操作,原因是下面的if(n == 1)的代码
//块中的MovePrint函数中的参数只有两个
//所以下面再次调用Hanoi函数时,参数的
//位置改变了,这样使传给MovePrint函数的
//参数也改变了
//函数有三个char[]参数的原因是,这三个参数
//表示三个柱子,这三个柱子都被需要

	if (n == 1)					//如果n是1,就直接初始柱->目的柱
	{
		MovePrint(position1, position3);
	}
	else				//如果n不是1,按照我们之前的理论,
	{					//直接把n个盘的情况转化成 
						//两次(n - 1)盘的情况 +1个盘的情况
						//依据我们之前画的图,这几个情况的顺序是
						//(n - 1)个盘的情况
						//1个盘的情况
						//(n - 1)个盘的情况
						//由此得下列代码

		Hanoi(n - 1, position1, position3, position2);
		//这一步可以理解为通过目的柱
		//把初始柱上的(n - 1)个盘放到辅助柱
		//中间的过程暂时忽略
		//其实中间的过程又转化为了
		//两次(n - 2)个盘的
		//情况 + 一次1个盘的情况
		//然后就这样一直转化,直
		//到最后全部转化为1个盘的
		//情况,递归就可以回归了

		MovePrint(position1, position3);
		//这一步可以理解为
		//上一步把初始柱上的(n - 1)个盘放到辅助柱后
		//初始柱上只有一个盘,
		//这一步就是把初始柱上剩余的一个盘移动到
		//目标柱

		Hanoi(n - 1, position2, position1, position3);
		//这一步可以理解为通过初始柱
		//把辅助柱上的(n - 1)个盘放到目标柱
		//中间的过程暂时忽略
		//其实中间的过程又转化为了
		//两次(n - 2)个盘的
		//情况 + 一次1个盘的情况
		//然后就这样一直转化,直
		//到最后全部转化为1个盘的
		//情况,递归就可以回归了
	}
}

int main()
{
	char initial[] = "初始柱";
	char auxiliary[] = "辅助柱";
	char goal[] = "目的柱";
	int n = 0;
	scanf("%d",&n);
	Hanoi(n,initial,auxiliary,goal);
	return 0;
}

无注释版:

#include <stdio.h>

void MovePrint(char position1[], char position2[])
{
	printf("%s->%s\n", position1, position2);
}												  
		   
void Hanoi(int n, char position1[], char position2[], char position3[])
{
	if (n == 1)					
	{
		MovePrint(position1, position3);
	}
	else				
	{					
		Hanoi(n - 1, position1, position3, position2);
		MovePrint(position1, position3);
		Hanoi(n - 1, position2, position1, position3);
	}
}

int main()
{
	char initial[] = "初始柱";
	char auxiliary[] = "辅助柱";
	char goal[] = "目的柱";
	int n = 0;
	scanf("%d",&n);
	Hanoi(n,initial,auxiliary,goal);
	return 0;
}

运行结果:

青蛙跳台阶

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

台阶数12345678910
需要步数123581321345589

我们发现这与斐波那契数列非常像

所以得到下列代码

递归

#include <stdio.h>

int frogJump(int n)
{
	if (n == 0)
	{
		return 1;
	}
	if (n == 1)
	{
		return 1;
	}
	return frogJump(n - 1) + frogJump(n - 2);
}

int main()
{
	int n = 0;
	scanf("%d",&n);
	int step = frogJump(n);
	printf("%d",step);
	return 0;
}

但是这段代码很低效,可以改用迭代

迭代

#include <stdio.h>

int frogJump(int n)
{
	int c = 1, a = 1, b = 1;
	for (int i = 1; i <= n - 1; i++)
	{
		c = a + b;
		a = b;
		b = c;
	}
	return c;
}

int main()
{
	int n = 0;
	scanf("%d",&n);
	int step = frogJump(n);
	printf("%d",step);
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值