递归调用详解,分析递归调用的详细过程

本文深入浅出地介绍了栈的概念及其在递归函数中的应用,通过实例详细解释了递归函数的设计思路及常见问题。

一、栈
     在说函数递归的时候,顺便说一下栈的概念。
     栈是一个后进先出的压入(push)和弹出(pop)式数据结构。在程序运行时,系统每次向栈中压入一个对象,然后栈指针向下移动一个位置。当系统从栈中弹出一个对象时,最近进栈的对象将被弹出。然后栈指针向上移动一个位置。程序员经常利用栈这种数据结构来处理那些最适合用后进先出逻辑来描述的编程问题。这里讨论的程序中的栈在每个程序中都是存在的,它不需要程序员编写代码去维护,而是由运行是系统自动处理。所谓的系统自动维护,实际上就是编译器所产生的程序代码。尽管在源代码中看不到它们,但程序员应该对此有所了解。
     再来看看程序中的栈是如何工作的。当一个函数(调用者)调用另一个函数(被调用者)时,运行时系统将把调用者的所有实参和返回地址压入到栈中,栈指针将移到合适的位置来容纳这些数据。最后进栈的是调用者的返回地址。当被调用者开始执行时,系统把被调用者的自变量压入到栈中,并把栈指针再向下移,以保证有足够的空间存储被调用者声明的所有自变量。当调用者把实参压入栈后,被调用者就在栈中以自变量的形式建立了形参。被调用者内部的其他自变量也是存放在栈中的。由于这些进栈操作,栈指针已经移动所有这些局部变量之下。但是被调用者记录了它刚开始执行时的初始栈指针,以他为参考,用正或负的偏移值来访问栈中的变量。当被调用者准备返回时,系统弹出栈中所有的自变量,这时栈指针移动了被调用者刚开始执行时的位置。接着被调用者返回,系统从栈中弹出返回地址,调用者就可以继续执行了。当调用者继续执行时,系统还将从栈中弹出调用者的实参,于是栈指针回到了调用发生前的位置。
     可能刚开始学的人看不太懂上面的讲解,栈涉及到指针问题,具体可以看看一些数据结构的书。要想学好编程语言,数据结构是一定要学的。

二、递归
     递归,是函数实现的一个很重要的环节,很多程序中都或多或少的使用了递归函数。递归的意思就是函数自己调用自己本身,或者在自己函数调用的下级函数中调用自己。
     递归之所以能实现,是因为函数的每个执行过程都在栈中有自己的形参和局部变量的拷贝,这些拷贝和函数的其他执行过程毫不相干。这种机制是当代大多数程序设计语言实现子程序结构的基础,是使得递归成为可能。假定某个调用函数调用了一个被调用函数,再假定被调用函数又反过来调用了调用函数。这第二个调用就被称为调用函数的递归,因为它发生在调用函数的当前执行过程运行完毕之前。而且,因为这个原先的调用函数、现在的被调用函数在栈中较低的位置有它独立的一组参数和自变量,原先的参数和变量将不受影响,所以递归能正常工作。程序遍历执行这些函数的过程就被称为递归下降。
     程序员需保证递归函数不会随意改变静态变量和全局变量的值,以避免在递归下降过程中的上层函数出错。程序员还必须确保有一个终止条件来结束递归下降过程,并且返回到顶层。
     例如这样的程序就是递归:

         void a(int);

         main()
         {
             int num=5;
             a(num);
         }

         void a(int num)
         {
             if(num==0) return;
             printf("%d",num);
             a(--num);
         }
        
     在函数a()里面又调用了自己,也就是自己调用本身,这样就是递归。那么有些人可能要想,这不是死循环吗?所以在递归函数中,一定要有return语句,没有return语句的递归函数是死循环。
     我们分析上面的例子,先调用a(5),然后输出5,再在函数中调用本身a(4),接着回到函数起点,输出4,……,一直到调用a(0),这时发现已经满足if条件,不在调用而是返回了,所以这个递归一共进行了5次。如果没有这个return,肯定是死循环的。
     虽然递归不难理解,但是很多在在使用递归函数的时候,问题多多。这里面一般有两个原因:一是如何往下递归,也就是不知道怎么取一个变量递归下去;二是不知道怎么终止递归,经常弄个死循环出来。
     下面看几个例子:
     1.求1+2+……+100的和
         先分析一下。第一递归变量的问题,从题目上看应该取1,2,……,100这些变量的值作为递归的条件;第二就是如何终止的问题,从题目上看应该是当数为100的时候就不能往下加了。那么我们试着写一下程序。

         int add(int);

         main()
         {
             int num=1,sn;
             sn=add(num);
             printf("%d/n",sn);
             getch();
         }

         int add(int num)
         {
             static int sn;
             sn+=num;
             if(num==100) return sn;
             add(++num);
         }

     分析一下程序:前调用add(1),然后在子函数中把这个1加到sn上面。接着调用add(2),再把sn加2上来。这样一直到100,到了100的时候,先加上来,然后发现满足了if条件,这时返回sn的值,也就是1+2+……+100的值了。
     这里有一个问题一定要注意,就是static int sn;
     有些人就不明白,为什么要使用static类型修饰符,为什么不使用int sn=0;?如果使用int sn=0;这样的语句,在每次调用函数add()的时候,sn的值都是赋值为0,也就是第一步虽然加了1上来,可是第二次调用的时候,sn又回到了0。我们前面说了,static能保证本次初始化的值是上次执行后的值,这样也就保证了前面想加的结果不会丢失。如果你修改为int sn=0,最后结果一定是最后的100这个值而不是5050。

     2.求数列s(n)=s(n-1)+s(n-2)的第n项。其中s(1)=s(2)=1。
         可以看出,终止条件一定是s(1)=s(2)=1。递归下降的参数一定是n。

         int a(int);

         main()
         {
             int n,s;
             scanf("%d",&n);
             s=a(n);
             printf("%d/n",s);
             getch();
         }

         int a(int n)
         {
             if(n<3) return 1;
             return a(n-1)+a(n-2);
         }
        
     这个题目主要说明的是,在函数中,不一定只有一个return语句,可以有很多,但是每次对归的时候只有一个起作用。题目不难理解,这儿不分析了。
     说了这些递归,其实它和函数的调用没有大的区别,主要就是一个终止条件要选好。递归函数很多时候都能用循环来处理。

         main()
         {
             int n=20,array[20];
             int i;
             for(i=0;i<n;i++)
             {
                 if(i<2) array=1;
                 else array=array+array;
             }
             printf("%d/n",array[19]);
             getch();
         }

     上面的程序就是实现一模一样的功能的。但是它有一个缺陷,就是n的值不是通过键盘输入来得到。如果想通过键盘来得到n,可以这样:

         main()
         {
             int n,i;
             int s1=1,s2=1,temp
             scanf("%d",&n);
             for(i=3;i<=n;i++)
             {
                 temp=s2;
                 s2+=s1;
                 s1=temp;
             }
             printf("%d/n",s2);
             getch();
         }

     但是在某些场合,使用递归比使用循环要简单的多。而且有些题目,一看就知道应该使用递归而不是循环来处理。

<think> 我们被提供了三段引用,但用户的问题是关于C语言递归求和函数的代码完善、调试、原理及应用。 根据引用[1]:递归有两个必要条件:递归函数和递归出口。 引用[2]提供了一个求1到n和(实际上是1到100的和)的递归函数例子,但是注意,这个例子中递归出口是当n为0或1时返回1,这实际上不是标准的1到n求和。 因为: 当n=0时,返回1(应该是0) 当n=1时,返回1(正确) 当n>1时,返回Sum(n-1)+n 但是,这个函数对于n=0的情况是不正确的,因为0的求和应该是0。不过,在题目中调用的是Sum(100),所以结果正确(因为递归终止条件在n=1时返回1,然后加上2,3,...,100,所以得到5050+1? 不对,我们分析一下: 实际上,这个函数计算的是:Sum(0)=1, Sum(1)=1, Sum(2)=1+2=3, Sum(3)=1+2+3=6,...,Sum(100)=1+2+...+100=5050? 但是注意,递归终止条件当n=0时返回1是不对的,因为0的求和应该是0,但这里当n=0返回1,所以如果计算Sum(0)会得到1(错误)。 不过,在main函数中调用的是Sum(100),所以递归过程是: Sum(100) = Sum(99)+100 Sum(99) = Sum(98)+99 ... Sum(1)=1 所以,实际上从1到100的累加,最后加上1(Sum(1)返回1)是正确的,因为1就是第一项。但是,如果我们计算Sum(0)就会得到1,这不符合0的求和。 因此,我们可以完善这个递归求和函数,使其更健壮。 用户要求:完善代码、调试、原理及应用。 步骤: 1. 原理:递归求和的基本思想是将大问题分解为小问题,直到达到基本情况(递归出口)。对于求前n项和,可以分解为:前n项和 = 前n-1项和 + n。递归出口是当n减小到0时,返回0(或者当n为1时返回1,但这样不能处理0的情况,所以通常用0作为出口)。 2. 完善代码:我们可以修改递归出口,使其在n==0时返回0,在n>=1时递归。但是也可以用一个出口:当n==0时返回0,否则返回n+Sum(n-1)。这样,对于非负整数n,都可以正确计算。 3. 调试:我们可以通过打印递归调用来调试,但要注意递归深度,避免栈溢出。 4. 应用:递归求和虽然简洁,但在实际应用中,对于大的n(比如1000000)可能会导致栈溢出,因此通常使用迭代(循环)来求和。递归求和主要用于教学递归思想。 因此,我们给出一个改进的递归求和函数,并解释原理,以及讨论其应用和局限性。 </think>### C语言递归求和函数解析 #### 递归求和原理 递归求和的核心是**将问题分解为更小的同类问题**,其实现需要满足两个必要条件: 1. **递归函数**:函数自身调用自身 2. **递归出口**:终止递归的条件(基础情形) 数学表达式为: $$S(n) = \begin{cases} 0 & \text{if } n = 0 \\ n + S(n-1) & \text{if } n > 0 \end{cases}$$ #### 代码实现与调试 引用[2]中的代码可优化为更严谨的版本: ```c #include <stdio.h> // 递归求和函数 int recursive_sum(int n) { // 递归出口 if (n <= 0) return 0; // 递归调用 return n + recursive_sum(n-1); } int main() { int num = 100; printf("1+2+...+%d = %d\n", num, recursive_sum(num)); return 0; } ``` **调试要点**: 1. **递归深度限制**:当`n > 10000`时可能引发栈溢出(需改用迭代或尾递归优化) 2. **边界测试**:验证`recursive_sum(0)`返回0,`recursive_sum(1)`返回1 3. **负数处理**:当前版本对负数会返回0(可根据需求调整) #### 执行过程分析 以`recursive_sum(3)`为例: ```mermaid graph TD A[recursive_sum3] --> B[3 + recursive_sum2] B --> C[2 + recursive_sum1] C --> D[1 + recursive_sum0] D --> E[返回0] E --> F[返回1+0=1] F --> G[返回2+1=3] G --> H[返回3+3=6] ``` #### 应用场景 1. **数学级数计算**:如等差数列求和 2. **树形结构遍历**:文件系统遍历、DOM树操作 3. **分治算法基础**:归并排序、快速排序 4. **动态规划**:斐波那契数列计算(配合记忆化) #### 优化建议 1. **尾递归优化**(减少栈空间): ```c int tail_recursive_sum(int n, int acc) { if (n <= 0) return acc; return tail_recursive_sum(n-1, acc+n); } // 调用:tail_recursive_sum(100, 0) ``` 2. **迭代替代**(避免栈溢出): ```c int iterative_sum(int n) { int sum = 0; for(int i=1; i<=n; i++) sum += i; return sum; } ``` 3. **公式优化**(O1复杂度): ```c int formula_sum(int n) { return n*(n+1)/2; // 等差数列求和公式 } ``` > **重要提示**:实际开发中递归应谨慎使用。当`n > 1000`时,迭代法或数学公式更安全高效[^1]。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值