121-对C语言中递归的分析

递归

在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。
德罗斯特效应是递归的一种视觉形式。
递归通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,当问题的规模小到一定程度时问题的答案显而易见。
递归需要有边界条件、递归前进段和递归返回段。

递归函数的执行分为“递推”和“回归”两个过程,这两个过程由终止条件控制,即逐层递推,直到递归终止条件满足,终止递归,然后逐层回归
递归调用同普通函数一样,每当调用发生时,就要分配新的栈帧(新参数据,现场保护,局部变量),而与普通函数不同的是,由于递推的过程是一个逐层调用的过程,因此存在一个逐层连续的分配栈帧过程,直至遇到递归终止条件时,才开始回归,这时才逐层释放栈帧空间,返回到上一层,直至最后返回到主调函数。

例如:有 5 个学生坐在一起, 问第 5 个学生多少岁?他说比第 4 个学生大 2 岁,问第 4 个学生岁数,他说比第 3 个学生大 2 岁,问第 3 个学生,又说比第 2个学生大 2 岁,问第 2 个学生,说比第 1 个学生大 2 岁,最后问第 1 个学生,他说是 10 岁,请问第 5 个学生多大。

非递归解题方法如下:

//非递归求年龄
int Age(int n)
{
    
    
	int tmp = 10; //第一个人年龄
	for(int i=1;i<n;i++)
	{
    
    
		tmp += 2;//后面的人比前一个多 2 岁
	}
	return tmp;
}

那么递归该如何处理呢?
如果 Age 函数是用来求年龄的,那么
Age(1)表示第一个人的年龄;
Age(2)表示第二个人的年龄;

Age(n-1)表示第 n-1 个人的年龄;
Age(n)表示第 n 个人的年龄

//递归求年龄
int Age(int n)
{
    
    
	int tmp;//保存年龄

	if(n == 1)
		tmp = 10;
	else
		tmp = Age(n-1) + 2;//当前第n个比第n-1个年龄多 2

	return tmp;
}

在这里插入图片描述
上图中的红色表示函数的调用过程,在这个过程中每个函数都还没有执行完成,那么每个函数占用的内存空间都不能释放,函数的调用都需要占用一定的栈空间(一个栈帧),而栈的空间是非常小的(在动态内存章节讲过栈 1M),当递归次数非常多时有可能出现栈空间不足

在这里插入图片描述

//递归求年龄
int Age(int n)
{
    
    
	int tmp;//保存年龄

	if(n == 1)
		tmp = 10;
	else
		tmp = Age(n-1) + 2;//当前第 n 个比第 n-1 个多 2

	return tmp;
}
//递归调用次数太多,程序崩溃
int main()
{
    
    
	printf("%d\n",Age(5000));//windows 系统,程序崩溃

	return 0;
}

例:利用递归求阶乘 n!
在这里插入图片描述

//递归求 n 的阶乘
//Fac(0)表示 0 的阶乘
//Fac(1)表示 1 的阶乘
//Fac(2)表示 2 的阶乘
//......
//Fac(n-1)表示 n-1 的阶乘
//Fac(n)表示 n 的阶乘
#include<stdio.h>
int Fac(int n)
{
    
    
	if(n==0 || n==1)
		return 1;
	else
		return Fac(n-1)*n;
}
int main()
{
    
    
	for(int i=0;i<10;i++)
	{
    
    
		printf("%d!=%d\n",i,Fac(i));
	}

	return 0;
}

在这里插入图片描述

利用递归求 1+2+3+…+n。
在这里插入图片描述

//递归求和
//Sum(1)表示从 1 加到 1
//Sum(2)表示从 1 加到 2
//Sum(3)表示从 1 加到 3
//......
//Sum(n-1)表示从 1 加到 n-1
//Sum(n)表示从 1 加到 n
int Sum(int n)
{
    
    
	if(n < 0) return -1;
	if(n==0 || n==1)
		return n;
	else
		return Sum(n-1) + n;
}

例:利用递归求斐波那契数列
在这里插入图片描述
实际上斐波那契数列是最不适合递归的例子。

//非递归求斐波那契数列
#include<stdio.h>
int Fibon_for(int n)
{
    
    
	int f1 = 1;
	int f2 = 1;
	int f3 = 1;
	for(int i=2;i<n;i++)
	{
    
    
		f3 = f1 + f2;
		f1 = f2;
		f2 = f3;
	}
	return f3;
}

//递归求斐波那契数列
int Fibon(int n)
{
    
    
	if(n==1 || n==2)
		return 1;
	else
		return Fibon(n-1) + Fibon(n-2);
}

int main()
{
    
    
	printf("非递归结果:");
	for(int i=1;i<10;i++)
	{
    
    
		printf("%d ",Fibon_for(i));
	}
	printf("\n");
	printf("递归结果: ");
	for(int i=1;i<10;i++)
	{
    
    
		printf("%d ",Fibon(i));
	}

	return 0;
}

在这里插入图片描述
当数值较大时,这两种执行的效率(时间)差别非常大
用递归处理。时间复杂度为O(2^n)即2的n次方
用非递归处理。时间复杂度为O(n)

在这里插入图片描述
介绍了最不适合使用递归的斐波那契数列,下面介绍一个非常适合使用递归的例子:汉诺塔
汉诺塔问题。古代有一个梵塔,塔内有 3 个座 A、B、C,开始时A座上
有若干个盘子,盘子大小不等,大的在下,小的在上。有一个老和尚想把这些盘子从A座移到C座,但规定每次只允许移动一个盘,且在移动过程中在 3 个座上都始终保持大盘在下,小盘在上。在移动过程中可以利用 B 座。要求编程序输出移动一盘子的步骤

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面是 2 个盘子的情况,非常的简单。如果是 3 个盘子呢?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
仔细分析图 2和图 5,只有到达这种情况,才能把最下面的一个盘子从 A 搬 到 C,然后再将其它小盘子搬到 C。

#include<stdio.h>
//模拟从 x 搬运到 y 的过程
void Move(char x,char y)
{
    
    
	printf("%c -> %c\n",x,y);
}

//将 n 个盘子的汉诺塔从 a 通过 b 搬到 c
void Hanoi(int n,char a,char b,char c)
{
    
    
	if(n == 1)//只有一个盘子,直接搬
	{
    
    
		Move(a,c);
	}
	else //先将上面的n-1个搬到b上,然后搬最下边的一个到c,再把n-1个从b搬到c
	{
    
    
		Hanoi(n-1,a,c,b);//上面 n-1 个从 a 通过 c 搬到 b
		Move(a,c);//只剩最后一个,直接搬
		Hanoi(n-1,b,a,c);//把上面搬到 b 上的 n-1 个盘子有从 b 通过 a 搬到 c
	}
}

int main()
{
    
    
	Hanoi(2,'A','B','C');
	return 0;
}

运行结果如下
在这里插入图片描述
图解递归过程:
在这里插入图片描述
在这里插入图片描述
总结:函数被调用,不管是自己调用自己,还是被其他函数调用,都将会给被调函数分配栈帧。
不存在无穷递归。
即递归函数必须要有一个是递归结束的出口(要有递归中终止的条件语句)
问题的规模不要过大,递归过深,引起栈溢出

看下面这道题:
输入一个整数(无符号整型),用递归算法将整数倒序输出
在这里插入图片描述
上面这两种写法的输出结果各是多少?
左:4 3 2 1
每次都是先执行printf("%d ",n%10);然后再递推

#include<stdio.h>
void backward(int n)
{
    
    
	if(n>0)
	{
    
    
		printf("%d ",n%10);
		backward(n/10);
	}
}
int main()
{
    
    
	int n=0;
	scanf("%d",&n);
	printf("原整数:%d\n",n);
	printf("反向数:");
	backward(n);
	printf("\n");
	return 0;
}

在这里插入图片描述

右:1 2 3 4
一直递推到遇到终止条件,然后一一回归打印

#include<stdio.h>
void backward(int n)
{
    
    
	if(n>0)
	{
    
    
		backward(n/10);
		printf("%d ",n%10);
		
	}
}
int main()
{
    
    
	int n=0;
	scanf("%d",&n);
	printf("原整数:%d\n",n);
	printf("反向数:");
	backward(n);
	printf("\n");
	return 0;
}

在这里插入图片描述
在这里插入图片描述
递归的终止条件非常重要,否则将会无休止地递归下去,陷入死循环状态,最终会导致栈空间被耗尽,报StackOverflow错误

注意事项:
1、限制条件:在设计一个递归过程时,必须至少有一个可以终止递归的条件,并且还必须对在合理的递归调用次数内未满足此类条件的情况进行处理。如果没有一个在正常情况下可以满足的条件,则过程将陷入执行无限循环的高度危险之中
2、内存使用:应用程序的局部变量所使用的空间有限。过程在每次调用它自身时,都会占用更多的内存空间以保存其局部变量的附加副本。如果这个进程无限持续下去,最终会导致StackOverflowException错误
3、效率:几乎在任何情况下都可以循环替代递归。循环不会产生传递变量,初始化附加存储空间和返回值所需的开销,因此使用循环相当于使用递归调用可以大幅提高性能
4、相互递归:如果两个过程相互调用,可能会使性能变差,甚至产生无限递归。此类设计所产生的问题与单个递归过程所产生的问题相同,但更难检测和调试
5、调用时使用括号:当Function过程以递归方式调用它自身时,必须在过程名称后加上括号(即使不存在参数列表)。否则,函数名就会被视为表示函数的返回值
6、测试:在编写递归过程时,应非常仔细信心地进行测试,以确保它总是能满足某些限制条件而终止递归过程,还应确保保护因为过多的递归调用而耗尽内存

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/LINZEYU666/article/details/111852228