递归:程序调用自身的编程技巧称为递归。是一个过程或函数在定义过程中直接或间接调用自身的一种方法。常常用于将大型复杂问题层层化为与原问题相似的小问题来求解。
int main()
{
printf("hello\n");
main();
return 0;
}
如上就是一个简单的递归举例。但是我们在运行时会发现,这个程序是有问题的,该程序会不断调用main函数使得进入死循环,同时在一段时间后程序会崩溃。
死循环很好理解,因为函数一次又一次反复调用。
但是程序崩溃的原因就不太一样了。我们发现错误警报中说明是Stack overflow,这是递归中常见的一种错误——栈溢出。
我们曾提到过,当创建变量时会向内存申请一块空间,以供变量使用。其实内存一般分为三个区域:栈区、堆区、静态区,它们会分别储存不同的东西。 栈区储存局部变量、函数形参;堆区储存动态开辟的内存(如malloc、calloc);静态区储存全局变量、static修饰的变量。
而我们在函数递归的过程中在不断调用函数,这时就需要不断从栈区中申请空间,当长时间进行下去,则必然会导致栈区空间全部被占用,这个时候无空间可以再申请,所以程序就会崩溃,这种错误类型就叫做栈溢出。
为深入理解函数递归思想,我们列举几个例子来深入感受。
接受一个整型,按顺序打印每一位。(如:输入:1234,输出:1 2 3 4)
运用递归的思想,如果要打印n位,那可以打印n-1位数字的每一位和n位数字的第n位;也就是 打印这个n-2位数字的每一位和n-1位数字的第n-1位和n位数字的第n位。
即print(1234) --> print(123) 4 --> print(12) 3 4 --> print(1) 2 3 4
#include
void print(int n)
{
if(n>9)
{
print(n/10);
}
printf("%d\n"n%10);
]
int main()
{
unsigned int num = 0;
scanf("%d",&num);
print(num);
return 0;
}
利用该代码,当传递n后n去掉最后一位(n/10)继续进入这个函数,然后让每个函数都打印自己的最后一位(n%10)即可。
#include
int my_strlen(char* str)
{
int count = 0;
while(*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "world";
int len = my_strlen(arr);
printf("len = %d\n",len);
return 0;
}
上述代码将字符串数组传入到函数中进行操作,因为传入的是数组的首元素地址,所以用char*来接收。每完成一次判断后,计数变量(count)++,同时str这个地址也++(因为char为1字节,++恰好为下一个字符),以此得到字符串长度。
那么不采取创建临时变量(即不使用上述count这样的变量)是否可以完成任务呢?
#include
int my_strlen(char* str)
{
if(*str != '\0')
return 1+my_strlen(str+1);
else
return 0;
}
int main()
{
char arr[] = "world";
int len = my_strlen(arr);
printf("len = %d\n",len);
return 0;
}
如此操作,当字符不是终止字符\0时,则会继续调用函数并且每次调用函数,返回值都会多一次+1,直到\0时返回0,这样所有返回值加和即可得到字符串长度。
递归的两个条件
· 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
· 每次递归调用之后越来越接近这个限制条件。
#include
int Fac1(int n)
{
int i = 0;
int ret = 0;
for(i = 1;i<=n;i++)
{
ret *= i;
}
return ret;
}
int main()
{
int n = 0;
int ret = 0;
scanf("%d",&n);
ret = Fac1(n); //求阶乘函数
printf("%d\n",ret);
return 0;
}
这是利用循环的方式进行解决。
#include
int Fac2(int n)
{
if(n>1)
return n*Fac2(n-1);
else
return 1;
}
int main()
{
int n = 0;
int ret = 0;
scanf("%d",&n);
ret = Fac2(n); //求阶乘函数
printf("%d\n",ret);
return 0;
}
这是利用函数递归的方式。
处理这种问题时,函数递归思路可以十分明确。根据数学知识得到n!=n*(n-1)!所以我们可以很顺利的得出,如果将n!当作函数Fac2(n)输出的结果,那么自然(n-1)!就是Fac2(n-2)的输出结果,所以函数递归的表达式可以顺利得到。
需要注意的是为了打断递归,所以应有“回程车”——return值为非递归,这个程序就可以完美运行。
由上一个例子我们可以先写出斐波那契数的产生方式(以Fib作为计算斐波那契数的函数名):
那么通过这个关系函数,我们就可以顺利编写出我们的代码
#include
int count = 0;//全局变量
int Fib(int n)
{
if(n==3) //测试第三个斐波那契数计算次数
{
count++;
}
if(n>2)
return Fib(n-1)+Fib(n-2);
else
return 1;
}
int main()
{
int n = 0;
int ret = 0;
scanf("%d",&n);
ret = Fib(n);
printf("ret = %d\n",ret);
printf("count = %d\n",count);
return 0;
}
利用这个程序即可求出,但是存在缺陷。效率非常低,因为每个n>2的数字都会再一次递归中被分为两个计算,当n=40时,不难预测n==3时的计算会被进行2的指数倍次,代码中加入count变量就是为了统计这一事实。 所以很明显这个算法效率很低。
那么利用循环是否会有所优化呢?循环只需三个变量a,b,c。而三个变量恰好是斐波那契数列的三个连续的数字,那么只需要a=b;b=c;c=a+b即可使得程序进入下三个数,用代码实现:
#include
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;//如果n<=2则直接输出c,就是1
if(i=2;i
不难发现,这个算法明显对计算机压力更小。
递归是个很棒的思想,在处理某些问题时常常能发挥奇效。掌握其主要思想,保证实现大事化小的思想,好比高中数学数列之中的递推公式,把握住两层表达式(问题)之间的联系就是解决问题的关键,慢慢悟好好学!
本文为学习C语言心得与笔记记录,部分举例来源于B站C语言教学up主鹏哥