勤时当勉励 岁月不待人
C/C++ 游戏开发
Hello,这里是君兮_,今天给大家带来一篇函数递归的文章,它在我们以后的数据结构中也非常重要。
递归算法与迭代算法
前言
不像加减乘除,我们求学期间就已经见识过多次了,而大多数初学者在此之前可能都从未了解接触过递归思想,这使得很难上手递归算法,今天我希望能尽我所能结合画图已经例题的方法把递归算法讲解的通俗易懂,帮助大家入门
- 废话不多说了,我们开始今天的内容
一.什么是递归?
- 程序调用自身的编程技巧称为递归( recursion)。
- 递归做为一种算法在程序设计语言中广泛应用。 递归算法通常指一个过程或函数在其定义或说明中直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
- 递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。(也就是说我们现在学的需要使用递归的场景大多数通过循环也能解决,在下面介绍的例子中会用循环也实现一遍)
- 递归的主要思考方式在于:把大事化小
- 当你在使用递归时遇到思考瓶颈,请牢记大事化小的思想!!
二.递归的两个必要条件
- 1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 2.每次递归调用之后越来越接近这个限制条件。
-
当你不满足这两个条件的任意一条时,程序会陷入死循环
当你的递归写出bug时,往往是没考虑上面这两个条件或者条件设置有误,调试时多想想递归条件最好能够画下图帮助自己理解。
三.配合实例讲解
打印整型数据的每一位
实例一:
接受一个整型值(无符号),按照顺序打印它的每一位。
例如:
输入:1234,输出 1 2 3 4
代码如下:
#include <stdio.h>
void print(int n)
{
if(n>9)//如果不大于9直接打印当前n的值即可
{
print(n/10);
}
printf("%d ", n%10);
}
int main()
{
int num = 1234;
print(num);
return 0;
}
- 我们想要把一个四位数甚至更多位的数每一位给剥离出来,首先要想到的是怎么得到每一位
- 我们知道,对10取余%就可以得到这个数的个位的数,而除以10就可以把这一位给消去,此时我们就可以这样实现(以下把n当作一个四位数举例)
- 先让n对10%得到此时这一位的数,然后将n/10来到下一位,再让n对10%得到这位数继续朝下进行直至n对10%等于0,说明此时n是个小于10的数只剩下它需要取了,取下它结束递归即可
画图解释
递归的递就是传递的意思,而归的意思是回归
这就是递归!- 解释完了递归在这段代码的应用,我们来讲讲这个代码中出现的问题
- 当我们中用户某天突发奇想输入了一个负数进去会发生什么呢?
- 我们这个程序设定的条件是只针对正数的,要想输入一个负数也能打印就得这样修改代码
void print(int n)
{
if (n > 9)//如果不大于9直接打印当前n的值即可
{
print(n / 10);
}
else if (n < -9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int num = -1234;
print(num);
return 0;
}
或者把我们传入的int改为一个无符号整型
void print(unsigned int n)
{
if (n > 9)//如果不大于9直接打印当前n的值即可
{
print(n / 10);
}
/*else if (n < -9)
{
print(n / 10);
}*/
printf("%d ", n % 10);
}
这里虽然都是非常简单的地方,但我想提醒大家的是,无论在递归或者任何其他程序的编写中我们都得尽可能考虑各方面的情况,在以后的程序猿工作中,我们要知道用户是不总是照着你的指示来使用程序的,我们得尽可能保证程序的避免上面这种bug。
循环实现
//判断输入数字位数
#include<math.h>
int Strlen(unsigned int n)
{
int count = 0;
while (n / 10)
{
count++;
n /= 10;
}
return count;
}
void print(unsigned int n)
{
int i = 0;
int ret = Strlen(n);
if (n > 9)
{
for (i = ret; i > 0; i--)
{
int m = pow(10, i);
printf("%d ", n / m);
n %= m;
}
}
printf("%d", n);
}
int main()
{
int num = 123456789;
print(num);
return 0;
}
-
我们先来看看效果
-
说实话,同样实现一个功能递归比循环简单多了,从代码的行数就能看出来。这就是我们使用递归算法的意义
求字符串长度
实例二
编写函数不允许创建临时变量,求字符串的长度。
#include <stdio.h>
int Strlen(const char*str)
{
if(*str == '\0')//是个空字符串
return 0;
else
return 1+Strlen(str+1);//每次递归让Strlen朝后移动一位直至遇到"\0"不满足递归条件
}
int main()
{
char *p = "abcde";
int len = Strlen(p);
printf("%d\n", len);
return 0;
}
- 咱们还是画个图理解吧
- 结合咱们这个图理解起来就比较简单啦,我希望以后你自己写的时候在有弄不清逻辑时也能画一个类似的图来理解。
循环实现
int Strlen(char* s)
{
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count;
}
int main()
{
char* p = "abcde";
int len = Strlen(p);
printf("%d\n", len);
return 0;
}
- 结合咱们这个图理解起来就比较简单啦,我希望以后你自己写的时候在有弄不清逻辑时也能画一个类似的图来理解。
四.递归与迭代
- 递归,就是在运行的过程中调用自己。
- 迭代法也称辗转法,是一种不断用变量的旧值递推新值的过程,跟迭代法相对应的是直接法(或者称为一次解法),即一次性解决问题。
- 迭代算法是用计算机解决问题的一种基本方法,一般用于数值计算。累加、累乘都是迭代算法的基础应用
斐波那契数列
1.递归求解
- 下边我们通过求斐波那契数列的例子来具体讲解一下。
- 在咱们开始之前我们先来讲讲什么是斐波那契数列
求第n个斐波那契数(不考虑栈溢出)
1 1 2 3 5 8 13 21 34 55 …
每前2个的数的和是第三个数,我们把拥有这种性质的数列称为斐波那契数列。
//求第n个斐波那契数
//1 1 2 3 5 8 13 21 34 55 ...
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 ", ret);
}
- 示例结果
- 这里的图解我希望大家能自己画一下,如果你理解了上面我讲的那两个例子这对你来说应该不算很难。
递归算法的不足
- 我们重点先讲讲上面这段代码中出现的问题
- 输一个很小的数我们这个程序能完美的运行,可输入一个很大的数呢?
我们来测试一下:
- 没有任何结果,这是为什么呢?
- 我们现在想测试一下此时该函数被执行了几次
int count = 0;//全局变量
int fib(int n)
{
if (n == 3)
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("n等于3执行了%d次 ", count);
}
- 当n=30时,仅仅是n=3这一种情况就被执行了30多万次,可见我们这个函数的时间复杂度有多大。
- 为什么会出现上述情况呢?
- 画图说明一下
- 图画的太丑辣,大家能理解我的意思就行
- 我们发现 fib 函数在调用的过程中很多计算其实在一直重复,这就导致我们的程序中出现的大量的重复没必要的计算浪费时间。
- 同时:
- 在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)这样的信息。
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出
2.使用迭代算法改进我们的代码
-
- 将递归改写成非递归。
-
- 使用
static
对象替代nonstatic
局部对象。
在递归函数设计中,可以使用static
对象替代nonstatic
局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic
对象的开销,而且static
对象还可以保存递归调用的中间状态,并且可为各个调用层所访问
- 使用
- 这里对关键字static不太了解的,可以看看下面链接中的博客,在操作符部分有具体讲static的用法:【C语言初阶】万字解析,带你0基础快速入门C语言(下)
- 改进代码如下:
//求第n个斐波那契数
int fib(int n)
{
int result;
int j;
int i;
result = j = 1;
while (n > 2)
{
n -= 1;
//实现每一次n减小时新的n-1与n-2并赋值给n
i = j;//把n-1的值给n-2
j = result;//把此时n的值给n-1
result = i + j;//n的值等于此时的(n-1)+(n-2)
}
return result;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("%d\n ", ret);
}
-
看看现在的结果
-
这里由于int型能存放的数不够肯定栈溢出了,但是先别管结果对不对,你就说快不快吧,是不是能给你个结果?这意味着咱们把代码重复冗杂的问题解决了!
提示:
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销 -
这里还有几个经典的问题比如汉诺塔和青蛙跳台阶,后面都会有具体博客去讲的。
五.扫雷游戏中的递归
- 另外,在改进扫雷游戏中我们也使用了递归算法,但是那个比较复杂,我也在扫雷游戏那篇博客里具体讲了,感兴趣的可以去看看,链接就放在这里啦!
【C语言】万字教学,带你分步实现扫雷游戏(内含递归函数解析),剑指扫雷,一篇足矣
总结
- 以上就是今天的所有内容了,今天我们具体分析的递归算法和迭代算法,同时对比了他们之间的区别与优缺点。
- 如果你有任何疑问欢迎在评论区指出或者私信我,我看到后会第一时间回复的哦!
新人博主创作不易,如果感觉文章内容对你有所帮助的话不妨三连一下这个新人博主再走呗。你们的支持就是我更新的动力!!!
(可莉请求你们三连支持一下博主!!!点击下方评论点赞收藏帮帮可莉吧)