1、什么是递归?
程序调用自身的编程技巧称为递归(recursion)。
递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需要少量的程序就可以描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:把大事化小。
2、递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件
举例子来说明一下递归
main函数也是函数,在main函数里面调用main函数,main函数自己调用自己,这就是递归。
int main()
{
printf("hello\n");
main();
return 0;
}
运行结果是一直打印hello,特别像死循环,但是程序跑着跑着自己就关了。
1>------ 已启动生成: 项目: test0623, 配置: Debug x64 ------
1>Test0707.c
1>D:\2022_code\test0623\test0623\Test0707.c(109): warning C4717: “main”: 如递归所有控件路径,函数将导致运行时堆栈溢出
1>test0623.vcxproj -> D:\2022_code\test0623\x64\Debug\test0623.exe
1>已完成生成项目“test0623.vcxproj”的操作。
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0 个 ==========
这样的代码是错误的示范,但它确实是属于一种递归。按F10调试代码,让代码一步一步走,就会发现代码会报错,报错信息为:0x00007FFF47A241C4 (KernelBase.dll) (test0623.exe 中)处有未经处理的异常: 0xC00000FD: Stack overflow (参数: 0x0000000000000001, 0x0000002CCEA03FF8)。栈溢出
3、练习
练习一:接受一个整型值(无符号),按照顺序打印它的每一位。例如:输入1234,输出1 2 3 4
一千二百三十四怎么能得到它的个位、十位、百位以及千位呢?
1234%10 = 4
1234 / 10 = 123 123%10 = 3
123/10 = 12 12%10 = 2
12/10 = 1 1%10 = 1
这样就4,3,2,1就都拿到了。
得到每一位之后存到一个数组里面,然后倒着打印就可以得到1 2 3 4,但是这样的做法有些麻烦,用递归来写代码就会相对简单。
使用递归的方法来实现的话,具体是怎么做呢?在上面提到了递归的主要思考方式是大事化小
,如何实现大事化小呢?(与原问题相似,但是规模较小)
print(1234) 拆成
print(123) + 4 再拆成
print(12) + 3 + 4 再拆成
print(1) + 2 + 3 + 4 (如果数字小于等于9,就不需要再剥离了,因为就剩下它一位了)
每一次都会剥离下来一个数字,这个问题就会越来越小。根据上面的思路,可以写出如下代码:
首先要判断数字是否大于9
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u",&num);
//递归是自己调用自己,需要做一个函数print
//要让函数打印参数部分数字的每一位,就需要将参数num给传过去
print(num);
return 0;
}
测试代码的运行效果:
1234
1 2 3 4
假设数字n为123,来分析一下代码:
写递归代码的时候,为了防止出现“栈溢出”的情况:
不能死递归,要有跳出条件,而且每次递归要逼近跳出条件;
递归层次不能太深
练习二:编写函数不允许创建临时变量,求字符串的长度
这是用库的方式来实现的,代码的打印结果是5.
#include<string.h>
int main()
{
char arr[] = {"hello"};
//['h'] ['e'] ['l'] ['l'] ['o'] ['\0']
printf("%d\n",strlen(arr));
return 0;
}
现在要做的工作是模拟实现一个strlen函数
int my_strlen(char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = {"hello"};
//['h'] ['e'] ['l'] ['l'] ['o'] ['\0']
printf("%d\n",my_strlen(arr));
//
//模拟实现一个strlen函数
//数组名相当于是元素地址,数组名在传参的时候,传过去的是首元素的地址
//'h'是字符,所以传过去的是字符的地址,字符的地址应该放到一个字符指针变量里面,所以写my_strlen()函数的时候,应该写成指针的形式
return 0;
}
代码的运行结果是5.结果正确,但是题目要求是不允许创建临时变量,上面的代码中使用了临时变量count,所以不满足题目要求,那么要怎么做呢?
思路就是:
my_strlen(“hello”)
1+my_strlen(“ello”)
1+1+my_strlen(“llo”)
1+1+1+my_strlen(“lo”)
1+1+1+1+my_strlen(“o”)
1+1+1+1+1+my_strlen(" ")
1+1+1+1+1+0=5
int my_strlen(char* str)
{
if (*str != '\0')
return 1 + my_strlen(str + 1);
else
return 0;
}
int main()
{
char arr[] = { "hello" };
//['h'] ['e'] ['l'] ['l'] ['o'] ['\0']
printf("%d\n", my_strlen(arr));
return 0;
}
练习三:求n的阶乘(不考虑溢出)
用之前的方法写代码的话,是下面的样子
int main()
{
int n = 0;
scanf("%d",&n);
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret = ret * i;
}
printf("%d\n",ret);
return 0;
}
但是如果用到递归的方法的话,代码如下:
int jiecheng(int i)
{
if (i <= 1)
return 1;
else
return i * jiecheng(i-1);
}
int main()
{
int n = 4;
printf("%d\n", jiecheng(n) );
return 0;
}
练习四:求第n个斐波那契数
代公式就可以很好地写出来代码。
// 1 1 2 3 5 8 13 21 34 55 ....前两个数之和等于第三个数
// n<=2, 1
//Fib(n)=
// n>2, Fib(n-1)+Fib(n-2)
//
int Fib(int i)
{
if (i <= 2)
return 1;
else
return Fib(i - 1) + Fib(i - 2);
}
int main()
{
int n = 0;
scanf("%d",&n);
int ret = Fib(n);
printf("%d\n",ret);
return 0;
}
10
55
有一些功能可以用迭代的方式实现,也可以使用递归。
但是可以发现一个问题,在使用递归的方法编写好程序之后,如果要计算第49个斐波那契数的时候效率很低,非常耗费时间。
下面用循环的方法比用递归的方法的效率要高。
int Fib(int i)
{
int a = 1;
int b = 1;
int c = 0;
while (i > 2)
{
c = a + b;
a = b;
b = c;//永远是在计算前两个数的和
i--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}