C 如何系统地遵循递归?

C 如何系统地遵循递归?,c,recursion,C,Recursion,嗯,我有很多C语言测试,涉及到找到给定函数的输出,而且,我需要精确地解释它的用途。其中一些是递归函数。当我遇到递归时,我总是费劲地寻找如何以图解方式遵循它,即使我成功了,有时我可能不明白递归函数的目的是什么 以下是两段代码: Main #include <stdio.h> #include <conio.h> int f2(int *a, int n, int x); int main() { int a[6] = {4, 3, 4, 2}; printf(

嗯,我有很多C语言测试,涉及到找到给定函数的输出,而且,我需要精确地解释它的用途。其中一些是递归函数。当我遇到递归时,我总是费劲地寻找如何以图解方式遵循它,即使我成功了,有时我可能不明白递归函数的目的是什么

以下是两段代码:

Main

#include <stdio.h>
#include <conio.h>

int f2(int *a, int n, int x);
int main()
{
  int a[6] = {4, 3, 4, 2};  
  printf("%d\n", f2(a, 4, 5)); 
  getch();
}
这个函数的“用途”是检查数组中是否有一组数字,它们的和会给出x的值。(在此特定示例中为x=5)。在本例中,它将返回true,因为2,3在数组中,2+3=5

我的问题是:我如何在纸面上按照它的示意图来理解它的目的。或者,你们将如何处理这类问题?
非常感谢您的帮助

遵循递归函数的最佳方法是使用堆栈。让我们以阶乘函数为例

long fact(int n)
{
    if (n <= 1)
        return 1;
    return n * fact(n - 1);
}
长事实(int n)
{

如果(n我们在学校也必须这样做,考试时花了很长时间才写出来,但这有点强迫理解

基本上,您最终会得到一个执行堆栈,就像编译器和运行时在后台实际执行代码一样:您从main开始,使用一组特定的变量。这是堆栈的顶部。然后main调用
f2
,将该调用推到堆栈上。这个新堆栈框架具有不同的局部变量。写下然后
f2
调用自身,将另一个帧推到堆栈上(这是同一个函数,但使用不同的参数对其进行不同的调用)。再次写下值

当函数返回时,将其从堆栈中弹出,并记下返回值

使用缩进来指示当前堆栈深度(或者如果有空间,只需写出整个堆栈)会有所帮助。通常,整个程序调用只涉及几个变量,因此将它们放入表中是有意义的(这样更容易了解发生了什么)

举个简单的例子:

Stack        |    a    |  n  |  x  | ret
-----------------------------------------
mn               4342     4     5
mn f2            4342     4     5
mn f2 f2          342     3     1
mn f2 f2 f2        42     2    -2
mn f2 f2 f2 f2      2     1    -6
mn f2 f2 f2 f2 f2         0    -8     0
mn f2 f2 f2 f2     (2     1    -6)
mn f2 f2 f2 f2 f2         0    -6     0
mn f2 f2 f2 f2     (2     1    -6)    0
mn f2 f2 f2       (42     2    -2)
...

理解递归函数的最佳途径是快速学习演绎推理,对一些简单的问题使用分而治之的策略。这并不能涵盖所有递归问题,但它涵盖了人们使用递归的80%以上的时间

基本上,您需要发现递归解决方案的三个要素: 1.将问题简化为较小问题的演绎规则(或一组规则)。 2.一套终止规则,旨在确保您能够达到这些规则。 3.存放中间结果的位置(通常是调用堆栈,有时是堆上的堆栈)

为了提供一个示例,我将使用我能想到的最愚蠢的示例,让我们尝试字符串长度—通常您会使用while循环计算字符串长度(假设您不使用系统库等)

递归策略类似于

(pseudo logic)

The length of a string is one more than the length of a string with one less character
The length of the "" string is zero

(pseudo java code)
int recursive_length(String s) {
   if (s.equals("")) {
     return 0;
   }
   return 1 + recursive_length(s.substring(1))
}
对于递归长度的调用,调用堆栈的增长如下

recursive_length("hello");
评估结果是

{1 + recursive_length("ello")}
{1 + {1 + {1 + recursive_length("lo")} } }
{1 + {1 + {1 + {1 + {1 + {0}}}}}}
{1 + {1 + {1 + {1 + {1 + 0}}}}}
评估结果是

{1 + {1 + recursive_length("llo")} }
评估结果是

{1 + recursive_length("ello")}
{1 + {1 + {1 + recursive_length("lo")} } }
{1 + {1 + {1 + {1 + {1 + {0}}}}}}
{1 + {1 + {1 + {1 + {1 + 0}}}}}
哪一种评估是正确的

{1 + {1 + {1 + {1 + recursive_length("o")} } } }
哪个回避

{1 + {1 + {1 + {1 + {1 + recursive_length("")} } } } }
现在,由于显式终止规则的计算结果为

{1 + recursive_length("ello")}
{1 + {1 + {1 + recursive_length("lo")} } }
{1 + {1 + {1 + {1 + {1 + {0}}}}}}
{1 + {1 + {1 + {1 + {1 + 0}}}}}
当我们从最里面的调用返回时,其计算结果为

{1 + recursive_length("ello")}
{1 + {1 + {1 + recursive_length("lo")} } }
{1 + {1 + {1 + {1 + {1 + {0}}}}}}
{1 + {1 + {1 + {1 + {1 + 0}}}}}
然后加法发生(最后)

然后我们从电话里回来

{1 + {1 + {1 + {1 + {1 + 1}}}}
还有一个补充

{1 + {1 + {1 + {1 + {2}}}}}
等等

{1 + {1 + {1 + {1 + 2}}}}
{1 + {1 + {1 + {3}}}}
{1 + {1 + {1 + 3}}}
{1 + {1 + {4}}}
{1 + {1 + 4}}
{1 + {5}}
{1 + 5}
{6}
6
那么,是什么保存了所有这些中间的
1+…
s呢?它们在调用堆栈中,当我们评估下一个递归调用时,调用堆栈将被退出。当内部调用返回时,代码将向上移动调用堆栈,积累答案

由于递归非常面向堆栈,因此它自然适合某些类型的数据结构。唯一的问题是,如果你把算法搞砸了,你永远不会达到终止状态,你的堆栈也会永远增长

为了解决这个问题,执行环境监视调用堆栈的进度,当它感觉调用堆栈太深时,它会以
StackOverflow
错误中断程序

在这个例子中,我在函数中调用函数的事实表明我是递归的。硬编码测试的早期退出条件是一个明显的终止规则。返回的结果显然是归纳推理,它将问题分解为一个较小的问题

这意味着验证递归函数通常是一个逻辑问题,而不是编程问题


通过one跟踪执行的最重要的部分是记录各个堆栈中的状态。在本例中,我使用了括号,但只要您有一个一致的方法来跟踪它们,那么使用什么符号都无关紧要。

我不会添加到我之前的堆栈示例中;它们可能是ta肯:我自己的演讲材料

我想谈谈@Edwin给你的三件东西:它们是你的关键工具。我通常会颠倒前两件。适用于你的具体问题:

终止:只要n为正,并且x不是0,我们就继续。当这些检查失败时,我们返回x==0(将返回值解释为false/true)

返回的结果:请注意,此布尔值也是唯一的返回结果

递归:我们尝试用一个较小的问题调用函数:

  • 从数组a中切掉第一个元素
  • x中减去该值
  • 减量n
{注意我们到目前为止所学到的:n是一个计数器;x是一个连续的和,其中