C语言程序设计——函数(递归)

四、函数——函数递归

递归:程序调用自身的编程技巧称为递归。是一个过程或函数在定义过程中直接或间接调用自身的一种方法。常常用于将大型复杂问题层层化为与原问题相似的小问题来求解。

int main()
{
    printf("hello\n");
    main();
    return 0;
}

如上就是一个简单的递归举例。但是我们在运行时会发现,这个程序是有问题的,该程序会不断调用main函数使得进入死循环,同时在一段时间后程序会崩溃。

C语言程序设计——函数(递归)_第1张图片

死循环很好理解,因为函数一次又一次反复调用。

但是程序崩溃的原因就不太一样了。我们发现错误警报中说明是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,这样所有返回值加和即可得到字符串长度。

递归的两个条件

·  存在限制条件,当满足这个限制条件的时候,递归便不再继续。

·  每次递归调用之后越来越接近这个限制条件。

求n的阶乘(不考虑溢出)

#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值为非递归,这个程序就可以完美运行。

求第n个斐波那契数

由上一个例子我们可以先写出斐波那契数的产生方式(以Fib作为计算斐波那契数的函数名):

Fib(n)=\left\{\begin{matrix} Fib(n-1)+Fib(n-2), n>2\\ \, \, \, \,\, \,\, \, \, \,\, \, \, \,\, \, \, \, \, \,\, \, \, \,\, \, \, \, \, \, \,\, \, \,1\, \, \, \, \, \, \,\,\, \, \, \,\, \, \, \,\,\, \, \, \, \, \,\ \,,n\leqslant2 \end{matrix}\right.

那么通过这个关系函数,我们就可以顺利编写出我们的代码

#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主鹏哥 

你可能感兴趣的:(算法,动态规划)