C语言笔记(函数篇二)函数递归

希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!

目录

1.递归

1.1什么是递归

1.2递归的思想

1.3递归的必要条件

2.递归举例

2.1求n的阶乘

2.2 使用递归的方式代替strlen函数

2.3字符串的逆序 

2.4总结

3.迭代

3.1 什么是迭代

3.2递推的缺点

3.3求斐波那契数


1.递归

1.1什么是递归

        程序员用自身的编程的技巧为递归。

        递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。

#include <stdio.h>
int main()
{
    printf("hehe\n");
    main();
    return 0;
}

        上述就是一个简单的递归的例子,主函数main在程序中调用了自己,但是这个程序缺少了递归的一个要素:结束递归的限制条件。没有结束的限制条件其结果必然就是无限递归,直到栈溢出,栈区无法再次新开辟main函数的栈帧,栈区使用完之后,程序无法继续运行,出现的结果如下图。

fb4f54cb872d4ab8b0de61a6a8d1459f.png

   画图解释一下 如图(画的不好啊,将就看一下啊)。

        在程序进行运行的时候,当读取程序每一次执行到main(),编译器就会为函数开辟新的栈帧(栈帧就是编译器经过计算为函数开辟栈区空间),当栈区堆满的时候,程序也就暂停了,就会弹出stack  overflow的警告。也就是死循环,程序就崩掉了。因此我们需要加上限制条件。

41c3586ffcfb48e8a20cade8baff71e1.png

1.2递归的思想

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

        解决大问题的方法和解决小问题的方法往往是同一个方法

1.3递归的必要条件

• 递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续。
• 每次递归调⽤之后越来越接近这个限制条件。
 

下面举例来更好的理解递归的思想和递归的必要条件

输入一个整型的数据,按照顺序打印它的每一位 (个 十 百 千 万)。
例如:
输入:1234,输出 1 2 3 4 

思路:

整体的程序先奉上

void  Print( unsigned int n)
{
	if (n > 9)            //进入递归的条件(限制条件)也是出递归的相反条件
	{
		Print(n / 10);   //每一次传递的参数也就是递归,参数的值必须接近限制条件
	}
	printf("%d ",n % 10);
}

int main()
{
	unsigned int n;
	scanf("%d", &n);
	Print(n);
	return 0;
}

我们顺着递归的思想将来分析上述的问题,

设函数的名字是Print,将问题进行拆分成小问题

小问题开始是先打印一位数字,没有可以限制的条件,

两位数字的时候,限制条件就有了,当输入的数字大于等于10的时候就需要有一次的递推,因此找到了限制条件;我么将十位数转变成个位数,要保留的是十位数上的数,我们采用取模的办法来保留(等价关系式)n/10。

输入一位数 (这里只是子函数)

int  Print( unsigned int n)
{
	printf("%d",n);
}

输入二位数

int  Print( unsigned int n)
{
	if (n > 9)             //限制条件
	{
		Print(n / 10);     //在子函数中再次进入了Print函数 传递的参数进行了处理
	}
	printf("%d ",n%10);
}

         输入一个三位数和上述的结果也是一样的,这样我们就归纳出了解决问题的思路,通过小的问题利用运行的规律解决大的问题,找出等价关系式。

输入四位数

 (1)Print("1234")

 (2)Print("123")4

 (3)Print("12") 3  4

 (4)Print("1") 2  3  4

        最后通过了解程序的如何执行来完成递推和回归的红色箭头是回归,黑色箭头就是递推了

了解到传递的数据的变化和递推的顺序按照下图就好理解了。

ad92dcd4dc8e4daa9963866c6f0e0dfc.png

2.递归举例

2.1求n的阶乘

问题:用递归的方法求n的阶乘(不考虑溢出问题)
 

代码献上

int Fac(int n)  //Factorial是阶乘的意思,命名为Fac
{
	if (n < 2)         //限制条件 输入数字不为1的情况下出现了新的规律啊
	{
		return 1;
	}
	else
		return n * Fac(n - 1);      
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fac(n);
	printf("%d ", ret);
	return 0;
}

思路  

n=1的时候阶乘就是1

n=2的时候 2!=2*1

n=3的时候 3!=3*2*1

n=n的时候 n!=n*(n-1)!(等价关系式)

我们可以看出限制条件和总结的数学规律,当n=2的时候规律改变了,限制条件也就是1;

2.2 使用递归的方式代替strlen函数

用递归的方法模拟实现strlen函数的功能
例如:
输入:abcedf,输出 6

代码献上

int my_strlen(char* str)    //  数组的名字也就是数组首元素的地址
{

	if (*str != '\0')
	{
		return 1 + my_strlen(str + 1);   //这里指针变量+1 地址就会变成数组下一个元素的地址 
	}
	return 0;
}
int main()
{
	char arr[] = "abcefg";     //这样初始化的数组,默认在最后加上'\0'作为结束标志
	int ret = my_strlen(arr);
	printf("%d", ret);
	return 0;
}

对代码有疑问的话可以看看代码里面的注释

思路

        字符串的结束标志是'\0',找到了限制条件,我们可以直接用字符串中的字符按照顺序和结束标志做对比。采用循环的方式,逐个数组元素进行比较。

等价关系式  1+my_strlen(str + 1)

另外这里还有非递推的方式(这个可以思考一下)

int my_strlen(char arr1[])
{
	int count = 0;
	int a = 0;
	while (1)
	{
		if (arr1[a] != '\0')
		{
			count++;
			a++;
		}
		else
		{
			break;
		}
	}
	printf("%d", count);
}
int main()
{
	char arr[] = "abcd";
    my_strlen(arr);
	return 0;
}
2.3字符串的逆序 

和青蛙跳台阶问题和汉诺塔问题放在其他文章中讨论。

2.4总结

        在进行递推的情况下,我们有两种的思路可以走,一种是大问题来推小问题,一种是小问题逐渐推到大,但是不同情况下的限制条件比较灵活。

按照帅地大佬的思路

1.明确函数的要干啥

2.寻找限制条件,限制条件往往会在出现变化的地方

3.通过归纳,总结出等价关系式。

3.迭代

3.1 什么是迭代

          递归是⼀种很好的编程技巧,但是和很多技巧⼀样,也是可能被误⽤的

迭代就是不断重复做某项事情,也是非递归的方式,基本上是采用循环的方式来实现得

     举例   

        如2.1中可以采用循环的方式解决问题

int Fact(int n)
{
int i = 0;
int ret = 1;
for(i=1; i<=n; i++)
{
ret *= i;
}
return ret;
}

        Fac函数是可以产⽣正确的结果,但是在递归函数调⽤的过程中涉及栈区内存的消耗。

3.2递推的缺点

        每⼀次函数调⽤,都需要为本次函数调⽤在内存的栈区,申请⼀块内存空间来保存函数调⽤期间的各种局部变量的值,这块空间被称为运⾏时堆栈,或者函数栈帧。
        函数不返回,函数对应的栈帧空间就⼀直占⽤,所以如果函数调⽤中存在递归调⽤的话,每⼀次递归函数调⽤都会开辟属于⾃⼰的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。所以如果采⽤函数递归的⽅式完成代码,递归层次太深(多次调用子函数),就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题。

3.3求斐波那契数

   此例子可以说明上述递归的缺点

递推的方式

int Fib(int n)
{
	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("%d\n", ret);
	return 0;
}

思路

斐波那契数列 1 1 2 3 5 8 13 21 34 55 89 ..........

看数列   在第三个数的时候规律发生了变化,因此限制条件和2有关。

后面的数总结的规律就是  n=(n-1)+(n-2) 等价关系式。

缺点暴露

440fa532f5c14baa93000127210f138f.png

当输入50的时候,程序的运行时间太长,极大的消耗掉了内存,我的电脑数不出来。

在上述的子函数中加入全局变量来计算Fib函数调用的次数,可以测出当n=3的情况下,函数调用的次数

int count = 0;            //加入
int Fib(int n)
{
	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("%d\n", ret);
	printf("%d\n", count);    //加入
	return 0;
}

运行结果如下

522c966b6e9f4f6981d0217cc2754dd3.png

        由于计算斐波那契函数采用递归的方式太占用空间,并且运行效率低,可以采用非递归循环的方式计算。

非递归的方式

int Fib(int n)
{
	count++;
	int a = 1;
	int b = 1;
	int c = 1;
	while (n > 2)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}

运行结果如下

0c7827a894d3421ca9e55e8a0f7a4cc6.png

仅仅调用了子函数一次。

总结

        并不是所有的问题都适合递归,递归会调用新的函数,消耗栈区内存;在有迭代可以简单解决的情况下不用递归。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值