希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!
目录
1.递归
1.1什么是递归
程序员用自身的编程的技巧为递归。
递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。
#include <stdio.h>
int main()
{
printf("hehe\n");
main();
return 0;
}
上述就是一个简单的递归的例子,主函数main在程序中调用了自己,但是这个程序缺少了递归的一个要素:结束递归的限制条件。没有结束的限制条件其结果必然就是无限递归,直到栈溢出,栈区无法再次新开辟main函数的栈帧,栈区使用完之后,程序无法继续运行,出现的结果如下图。
画图解释一下 如图(画的不好啊,将就看一下啊)。
在程序进行运行的时候,当读取程序每一次执行到main(),编译器就会为函数开辟新的栈帧(栈帧就是编译器经过计算为函数开辟栈区空间),当栈区堆满的时候,程序也就暂停了,就会弹出stack overflow的警告。也就是死循环,程序就崩掉了。因此我们需要加上限制条件。
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
最后通过了解程序的如何执行来完成递推和回归的:红色箭头是回归,黑色箭头就是递推了
了解到传递的数据的变化和递推的顺序按照下图就好理解了。
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) 等价关系式。
缺点暴露
当输入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;
}
运行结果如下
由于计算斐波那契函数采用递归的方式太占用空间,并且运行效率低,可以采用非递归循环的方式计算。
非递归的方式
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;
}
运行结果如下
仅仅调用了子函数一次。
总结
并不是所有的问题都适合递归,递归会调用新的函数,消耗栈区内存;在有迭代可以简单解决的情况下不用递归。