一.递归的理解
1.递归的概念
在C语言中,递归就是函数自己调用自己的这个过程。
下面举一个错误的例子,仅供理解递归的概念:
cpp
#include <stdio.h>
int main()
{
printf("hehe\n");
main();//main函数中⼜调⽤了main函数
return 0;
}
上述代码的解释:先调用了printf函数用于打印hehe,然后在main函数内部调用main函数,这就是函数的递归。但是这里要注意的是这个程序运行的结果是:死循环的打印hehe,这个递归没有结束条件,所以是不正确的递归。(感觉递归就是类似于套娃的东西,自己体内有一个更小的自己)

上述是无限次递归会出现的问题:栈溢出,因为每次循环都会在栈区的内存中为调用的main函数开辟空间,在无限次的循环中导致栈区的空间耗尽,最终导致栈溢出现象Stack overflow
2.递归的作用
递归的作用是把一个大型的问题层层转换成与原问题相似但是规模更小的子问题来解决。直到子问题不可以继续被拆分。然后尽力解答与大问题相关的子问题,然后层层返回。就求解出了大问题的最终答案。所以说递归中的递就是递推的意思,递归中的归就是返回的意思。
3.递归的限制条件
递归是有限制条件的,因为没有限制条件的递归有可能会陷入无限递归的错误中,所以下面来说一下递归的限制条件:①递归存在限制条件,当满足该限制条件时,递归便会终止,不再继续进行。②每次递归调用后,都会越来越接近限制条件,最终达到递归终止的目的。防止无效的限制条件导致无限次数的递归。这两个限制条件必须同时满足,否则写出的递归程序就会导致一些问题的出现。
二.递归举例
1.举例一:阶乘的运算
(1)题目分析
一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。
(2)思路展示
首先一个数的阶乘就是1往后乘,一直乘到该数。假设这个数是5,所以我们可以得到:

因此5!等于5*4!。这样就将求一个数阶乘的问题转换成求该数-1的阶乘问题,直到该数减到0时,该数的阶乘就等于所有算出来的数相乘。下面列出递推公式图:

(3)代码展示
因此我们就可以写出相关递归程序的代码:
cpp
int Fact(int n)
{
if(n==0)
return 1;
else
return n*Fact(n-1);
}
上述代码的解释:当函数参数为1时,返回1的阶乘:1;当函数参数不为1时,函数返回值为该数乘形式参数为该数减一的函数返回值,一直减一直到该数为1时,返回1即可完成阶乘相关的递归程序。
下面进行函数代码的测试:
cpp
#include <stdio.h>
int Fact(int n)
{
if(n==0)
return 1;
else
return n*Fact(n-1);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fact(n);
printf("%d\n", ret);
return 0;
}

(4)画图推演:

上述图片的进一步解释:
当主函数的参数为5时,调用Fact函数,因为此时的n不等于0,所以执行5*Fact(4)这个语句。因为Fact(4)将4作为函数参数进行调用阶乘函数。所以进行第二次的函数调用;第二次函数调用时,参数是第一次函数调用传过来的4,不等于0,所以执行此语句:4*Fact(3)。这时候因为Fact(3)也需要调用阶乘函数进行求解,所以第二次函数调用将3作为参数继续进行第三次的函数调用;当进入第三次的函数调用时,此时的函数参数为3,不等于0,所以执行:3*Fact(2)这条语句,因为Fact(2)这个表达式以2为参数调用了阶乘函数,所以第三次函数调用了自己本身,开始了第四次函数调用;在第四次函数调用时参数为2,所以执行此语句:2*Fact(1).....以此类推;
当进行到1*Fact(0)时,此次函数调用的的参数为0,执行第一条语句返回1。将返回的1传给调用此次函数的地方:1*Fact(0)。传回返回值之后,将此表达式计算出来继续返回上一个调用此次函数的地方。以此类推....直到将所有的返回值返回到最初的函数调用处。
2.举例二:顺序打印数的每一位
(1)题目分析
题目就是给出一个数字,然后进行顺序打印出它的每一位上的数值。
(2)思路展示
当给出一个数,我们可以通过对10取余数从而得到该数的最后一位,进而打印该数。然后将该书逐次除以10.这样就可以得到该数每一位上的数字。但是这不符合题目要求。题目要求我们顺序打印,而不是从个位向最高位依次打印,所以我们需要改进思路:可以将逆序的每一位上的数字先存起来,然后到最后再逆序打印,这样我们就得到了顺序打印给出数字的每一位数值。但是这显然很麻烦,也不符合本章节的题目。所以我们要用递归来实现这个问题。
首先我们可以设置一个函数Print,专门用来实现题目的要求:
Print(n) 如果n是1234,那 Print(1234)就可以表示为实现打印1 2 3 4的函数。其中Print(1234)可以拆分成Print(123)和printf(4),这里自己调用了自己,让第二次函数调用来实现123的打印,留下的4交给printf函数来打印以此类推.......

(3)代码展示
cpp
void Print(int n)
{
if(n>9)
{
Print(n/10);
}
printf("%d ", n%10);
}
int main()
{
int m = 0;
scanf("%d", &m);
Print(m);
return 0;
}
上述代码的解释:我们使用了大事化小的思路,把Print(1234)打印1234每一位,拆解为首先Print(123)打印123的每一位,再打印得到的4;把Print(123)打印123每一位,拆解为首先Print(12)打印12的每一位,再打印得到的3;直到Print打印的是一位数,直接打印就行。
(4)画图推演

上述图片的具体解释:首先在main函数第一次调用print函数,参数为1234,然后再进入paint函数内部时,根据条件自己调用自己将123当作参数进行第二次的print函数的调用;当进入第二次print函数的调用时,根据条件又进行了一次函数的调用,参数是12;当进入第三次print函数调用时,依然根据条件将1当作参数自己调用自己;当进入第四次print函数时,参数根据限制条件,打印了数字1。将其作为返回值开始进行一层一层函数的返回。
当返回完第五次函数调用,此时屏幕上打印出了1;然后将1作为返回值开始进行第四次print函数的返回,打印出了2便进行完了第四次print函数的返回。以此类推.......最终就在屏幕上打印出了1 2 3 4。
三.迭代与递归
迭代常常具被认为就是循环,但其实循环只是递归的一个子集;递归是一种较为容易的逻辑思维的方法,但是因为递归的缺陷,它往往会被误用。
1.递归的缺陷
在C语言中,函数调用是最为常见且C语言分不开的操作。C语言进行函数调用时,会首先给函数分配内存空间(函数栈帧),用于存放函数内部定义的变量,和一些代码操作。在函数返回之后就会将这个空间归还给系统。通过上面的学习:我们了解到函数递归是自己调用自己,当调用的函数返回后,自己才可以返回。这样就会产生一个问题。每次函数自己调用自己,系统就会在栈区为此次函数的调用分配空间。我们也应该直到系统栈区的空间一定是有限的,如果函数的递归调用层数过大,就会消耗完系统的所有栈空间,进而导致栈溢出。
2.举例说明
(1)举例一
比如递归题目的第一题阶乘的实现,我们可以用迭代的方法进行程序的书写:
cpp
int Fact(int n)
{
int i = 0;
int ret = 1;
for(i=1; i<=n; i++)
{
ret *= i;
}
return ret;
}
上述代码是能够完成任务,并且效率是比递归的方式更好的。 事实上,我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加容易理解, 但是这些问题的迭代实现往往比递归实现效率更高。 当一个问题非常复杂,难以使用迭代的方式实现且用递归实现不会导致栈溢出时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。此时使用递归可谓是利大于弊。
(2)举例二
题目描述:求第n个斐波那契数
思路展示:根据逻辑思维的推导和数学上斐波那契数列的定义,我们可以容易写出下方的公式和代码:

cpp
int Fib(int n)
{
if(n<=2)
return 1;
else
return Fib(n-1)+Fib(n-2);
}
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
可以看到上述便是我们实现求第n个斐波那契数的代码,我们用了递归的方法,当我们输入40时,会得到相应的斐波那契数;但是当我们输入50时,程序却迟迟不给出答案。这是为什么呢?下来进行画图演示:

根据上方图片的内容可以知道:递归程序不断的展开,在展开的过程中,会有重复计 算,而且递归层次越深,冗余计算就会越多。所以我们可以得知当输入50时,程序依然在努力的运算,只不过电脑冗余计算太多,导致得出答案会很慢。我们可以进行代码测试:
cpp
#include <stdio.h>
int count = 0;
int Fib(int n)
{
if(n == 3)
count++;//统计第3个斐波那契数被计算的次数
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("\ncount = %d\n", count);
return 0;
}

这里我们看到,在计算第40个斐波那契数的时候,使用递归方式,第3个斐波那契数就被重复计算了 39088169次,这些计算是非常冗余的。所以斐波那契数的计算,使用递归是非常不明智的,所以应该用迭代的方式解决。 我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从小到大计 算就行了。 这样就有下面的代码:
cpp
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while(n>2)
{
c = a+b;
a = b;
b = c;
n--;
}
return c;
}