目录
- 前言
- 递归
- 定义
- 递归的两个必要条件
- 接受一个整型值(无符号),按照顺序打印它的每一位
- 使用函数不允许创建临时变量,求字符串“abcd”的长度
- 求n的阶乘
- 求第n个斐波那契数
- 迭代
- 总结
- 递归与迭代的主要区别
- 用法不同
- 结构不同
- 时间开销不同
- 两个经典问题
前言
从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事!故事是什么?“从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事!故事是什么?‘从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事!故事是什么?……’”
递归
定义
计算机科学中,递归是是指在函数的定义中使用函数自身的方法。它通常把一个大型的复杂问题层层转化为一个与原问题相似的规模较小的问题来求解,递归只需要少量的代码就可以描述出解题过程中的多次重复计算,大大减少了程序的代码量。
递归的主要思想是:大化小
递归的两个必要条件
1.存在限制条件,当满足这个限制条件时,递归停止。
2.每次递归调用之后越来越接近限制条件。
错误示例:
#include<stdio.h>
int main()
{
printf("hello world!\n");
main();
return 0;
}
画红线的地方,意思是栈溢出,从上面写的程序中发现,递归没有停止的限制条件,导致死递归。
接受一个整型值(无符号),按照顺序打印它的每一位
示例1:
问题描述:
接受一个整型值(无符号),按照顺序打印它的每一位。
样例输入:1234
样例输出:1 2 3 4
代码示范:
#include<stdio.h>
void Func(unsigned int x)
{
if (x > 9)
{
Func(x / 10);
}
printf("%d ", x % 10);
}
int main()
{
unsigned int num = 0;
scanf("%d", &num); //假设输入的是123
Func(num);
return 0;
}
到这里对递归应该有一个比较清晰的认识了,在图中红色过程表示的就是递归当中的“递”,蓝色过程表示的就是递归当中的“归”。
使用函数不允许创建临时变量,求字符串“abcd”的长度
示例2:
问题描述:
使用函数不允许创建临时变量,求字符串“abcd”的长度
分析: 直接求字符串“abcd”的长度,它是字符串,在前面文章中说过字符串的结束标志是 \0
代码展示:
#include<stdio.h>
#include<string.h>
int Length(char* l)
{
int count = 0;
while (*l != '\0')
{
count++;
l++;
}
return count;
}
int main()
{
char arr[] = "abcd";
int len = Length(arr);
printf("%d\n", len);
return 0;
}
这段代码完全没有问题,但是题目要求不允许创建临时变量,这里创建了临时变量count。
再分析:
所以我们可以将Length函数写成递归的形式:
//递归
int Length(char* length)
{
if (*length == '\0')
return 0;
else
return 1 + Length(length + 1);
}
我们再分析过程:
求n的阶乘
我们回顾一下n!怎么算:
#include<stdio.h>
int Func(int x)
{
int i = 0;
int j = 1;
for (i = 1; i <= x; i++)
{
j *= i;
}
return j;
}
int main()
{
int n = 0;
scanf("%d", &n);
int result = Func(n);
printf("%d\n", result);
return 0;
}
上述代码是非递归的形式,我们再来思考一下如何使用递归来写n!:
我们可以发现n!=n*(n-1)!
所以Func函数可以写成:
int Func(int x)
{
if (x <= 1)
return 1;
else
return x * Func(x - 1);
}
求第n个斐波那契数
斐波那契数列由0和1开始,之后的斐波那契数就是由之前的两数相加而得出。
前几个斐波那契数是:
1、 1、 2、 3、 5、 8、 13、 21、 34、 55、 89、 144……
也就是说从第三个数开始,后面的每一个斐波那契数都是前两个数的和。
到这里就可以直接写代码了:
#include<stdio.h>
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 result = Fib(n);
printf("%d\n", result);
return 0;
}
这时候我们信心满满,让计算机帮我求第50个斐波那契数,当我们执行程序后在键盘输入50,却发现,等了很久都没有发现它输出内容。
为什么?
我们要求第50个斐波那契数,就需要计算第49个和第48个数,计算第49个数又需要计算第48个数和第47个数,可以想一下上面这个图画到末端需要画多少,除了前两个数,要算2的48次方,而int型只占4个字节的内存,也就是32位,2的32次方都已经42亿多,可想而知计算量非常庞大。按照递归的方式要进行大量的重复计算。我们可以做一个计数,计算第40个斐波那契数中3被计算了多少次,你会发现3被计算了将近四千万次,效率非常低。所以不是因为计算机偷懒没算,它也在拼命地计算,只是量太大,它一会儿也算不出来。
那么应该如何改进呢?
迭代
在计算机科学中,迭代是程序中对一组指令(或一定步骤)的重复。它既可以被用作通用的术语(与“重复”同义),也可以用来描述一种特定形式的具有可变状态的重复。可以简单理解为普通循环。但与普通循环有所差别,迭代时,循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环计算的初始值
我们知道函数形参是被存放在栈区当中,递归每“递”一次,就要开辟一个变量的内存,那么当参数非常大的时候,栈区内存不够了,栈区放不下了,也就是说栈区空间已经被耗尽了,但是你的东西还没放完,这个时候就会出现栈溢出的现象。
那么我们回头再看求第n个斐波那契数,能否将递归转换成迭代的形式。
while (n >= 3)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
那么当n小于3的时候,也就是第1个或者第2个数,都是1,所以在给a,b,c初始化的时候,都赋值为1即可。
完整代码:
#include<stdio.h>
//迭代
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 result = Fib(n);
printf("%d\n", result);
return 0;
}
这个时候程序运行后输入50,尽管结果仍然是错误的,但是速度非常快。解决了大量重复计算的问题。
总结
当使用递归可能会导致栈溢出时,程序效率明显下降的时候,就不能够使用递归了。
如何解决?
1.可以使用迭代替换递归。
2.在递归函数设计中可以使用static限制变量,让变量申请一块内存后,在那一块内存折腾。不仅不再大量开辟栈区内存,从而导致栈溢出,并且static可以保存递归时的中间状态,并且为各个调用层所访问。
- 递归代码量少,迭代不易想到,递归比迭代更清晰。所以许多问题采用递归的方式解释。
- 迭代实现比递归实现的代码可读性差,但是效率高。
- 当问题复杂的时候,难以用迭代实现,此时使用递归会更加简洁。
递归与迭代的主要区别
用法不同
- 迭代是代码块的重复。虽然需要更多的代码,但时间复杂度通常小于递归的时间复杂度。
- 递归是多次调用自身,因此代码长度非常小。但是,当有非常非常多次的递归调用时,递归的时间复杂度可能会呈指数级增长。
结构不同
- 迭代是环结构,从初始状态开始,每次迭代都遍历这个环,并更新状态,多次迭代直到到达结束状态。
- 递归是树结构,从字面可以理解为重复“递”和“归”的过程,当“递”到达底部时就会开始“归”。
时间开销不同
- 与迭代相比,递归具有大量的开销。递归具有重复函数调用的开销,即由于重复调用同一函数,代码的时间复杂度增加了许多倍。
两个经典问题
- 汉诺塔问题
- 青蛙跳台阶问题