【问题标题】:How do I tell if a function is tail recursive C?如何判断一个函数是否是尾递归 C?
【发布时间】:2021-11-25 15:45:19
【问题描述】:

我的函数是尾递归吗?如果是这样,为什么/为什么不呢?我在理解这个概念时遇到了一些困难,因此非常感谢任何帮助。

struct link 
{
  int value; //data    
  link_t *next; //link
};

struct list
{
    link_t *first;
    link_t *last;
};


int list_size_count(link_t *first, int acc)
{
  if (first != NULL)
  {
    return (acc + list_size_count((first->next), 1));
  }
  return acc;
}

int linked_list_size(list_t *list)
{
  return list_size_count((list->first),0);
}

【问题讨论】:

  • 你怎么看?你得出什么结论?你为此付出了什么努力?
  • 一般来说,tail call是指一个函数的返回值是另一个函数调用的直接结果,返回值被直接 作为新的返回值。这增加了执行尾调用优化的机会,其中被调用函数直接返回到调用函数的调用者。这允许在调用函数之前回收当前堆栈帧。但这只有在您的编译器支持尾调用优化时才会发生。
  • 在你发布的代码中,唯一的尾调用在linked_list_size,它调用并返回list_size_count的结果。递归函数list_size_count 不包含尾调用(因此不是尾递归)。可以通过将acc的add折叠到函数本身转化为尾递归函数,这样返回值在返回前不需要在调用者中修改。
  • 更重要的是编译器是否真的可以优化递归。通常,编译器只有在尾调用递归时才能做到这一点。这意味着任何不是尾调用的递归都不应该在任何情况下使用,因为它非常慢,非常危险并且通常不可读。这绝对没有任何收获。
  • @TomKarzes 我该怎么做?

标签: c recursion linked-list tail-recursion tail


【解决方案1】:

对于尾递归函数,递归调用必须是它的最后一个操作。 在您的代码中,函数所做的最后一件事是添加两个值,因此 list_size_count() 不是尾递归。

int list_size_count(link_t *first, int acc)
{
  if (first != NULL)
  {
    return (acc + list_size_count((first->next), 1));
  }
  return acc;
}

下面是如何让它尾递归:

int list_size_count(link_t *first, int acc)
{
  if(first != NULL)
  {
    return list_size_count(first->next, acc + 1);
  }

  return acc;
}

【讨论】:

    【解决方案2】:

    我的函数在这里是尾递归吗?

    不,不是。

    这一行

    return (acc + list_size_count((first->next), 1));
    

    在递归调用返回后包括一个添加操作,即acc 的添加以及递归调用返回的任何内容。因此,它不是尾递归。

    但是很容易变成尾递归。只需将上面的行更改为:

    ++acc;
    return list_size_count((first->next), acc);
    

    这里acc首先递增并作为参数传递给递归函数,递归调用的结果直接在return语句中使用。

    现在递归调用后没有任何操作。因此它是尾递归。

    【讨论】:

      【解决方案3】:

      如果一个例程的最终动作是调用另一个例程,则称为尾调用。一个例子是一个例程,比如说一个名为g 的例程,它以return f(x); 结尾。编译此代码的直接方法是:

      • 通过将参数x 复制到指定位置来传递参数。
      • 执行子程序调用指令调用f
      • 清理堆栈帧(通过将堆栈指针恢复到该例程开始执行时的值)并执行返回指令。

      因为这是例程g 要做的最后一件事,所以我们不需要f 返回g。我们可以跳转到它,而不是调用它:

      • x 复制到传递参数的位置。
      • 执行分支指令跳转到f

      然后f会执行,清理我们当前所在的栈帧,并执行返回指令。这将返回到g 的调用者,就好像g 执行了一条返回指令。我们从这些指令中获得了与之前的指令相同的效果,但工作量更少。

      (这里没有展示更多细节。为了这个工作,管理堆栈帧的规则必须允许f清理g的堆栈帧,即使f也可以建立自己的堆栈上的上下文。)

      如果fg 是同一个例程,这称为尾递归

      请注意,在此示例中,fg 必须返回相同类型的数据。如果f 返回一个float 并且g 返回一个int,那么当g 包含return f(x); 时,调用f 实际上并不是g 所做的最后一件事。在return 语句中会自动将float 转换为int,因此这不会是尾调用。

      尾调用必须是例程的最后一个动作;它不必是例程中的最后一个源代码。如果return f(x); 出现在例程中的任何位置(并且类型匹配),那就是尾调用,它可以被分支替换(如果堆栈管理规则允许)。此外,如果 g 的返回类型为 void,则例程中任何位置的 f(x); return; 都是尾调用,而例程末尾的 f(x); 是尾调用。

      在您的例程linked_list_size 中,return list_size_count((list->first),0); 是尾调用,但不是尾递归。

      在您的例程list_size_count 中,没有尾调用,因为在return (acc + list_size_count((first->next), 1)); 中,+ 在调用list_size_count 之后执行。您可以通过将其重写为:

      return list_size_count(first->next, acc+1);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-01-16
        • 1970-01-01
        • 2011-08-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-02-08
        相关资源
        最近更新 更多