【问题标题】:Convert a recursive function to a tail recursive function将递归函数转换为尾递归函数
【发布时间】:2016-01-06 09:15:07
【问题描述】:

继续this问题:

我在 c++ 中有一个函数,它一遍又一遍地调用自己。就是这样:

#include <iostream>
#include <math.h>
#include <stdio.h>
#include <cmath>
using namespace std;
double g( double a, double x){
    if (x>=a) return (g(a,x-1)+g(a,x-a));
    else if (x<a) return 1;
    return 0; //Never Reached
}
int main(){
    cout << (unsigned long)g(sqrt(90),90) <<endl; // outputs 7564511
    cout << (unsigned long)g(sqrt(10000019),10000019)<<endl; // Signal: SIGSEGV (Segmentation fault)
}

我想知道如何将此函数转换为某种迭代或尾循环(或任何停止段错误的方法),但更重要的是我需要知道如何自己实际执行。


注意:如果这是一个微不足道的问题,我提前道歉。

注意 2: 有类似的问题(如 thisthis),但我发现的问题都没有解决我的函数调用自身两次的事实每次迭代。

【问题讨论】:

  • 这可能会对您有所帮助:codeproject.com/Articles/418776/…
  • 当一个递归函数在尾调用中两次调用自己时,我认为不能转换为尾递归函数。
  • 就像@RSahu 说的,这是不可能的。
  • 不是真的。取决于函数的性质,例如你让斐波那契尾递归stackoverflow.com/questions/22111252/tail-recursion-fibonacci
  • @RSahu 绝对一切都可以转换为尾递归函数,谷歌“继续传递风格”。它是否对任何人都有帮助是完全不同的问题。

标签: c++ loops recursion tail-recursion


【解决方案1】:

正如许多人已经说过的,这不能直接转换为尾递归或迭代函数,因为浮点函数参数使得迭代构建结果变得困难。但是,稍微考虑一下,这个函数可以很有效地计算出来。

首先,因为所有的递归都相加并以1结尾,所以该函数基本上是计算到递归结束的路径数。例如,对于 g(5,2),一条路径是 g(2,5)-> g(2,3) -> g(2,1)(此处返回 1),另一条路径是 g(5, 2)-> g(4,2) -> g(3,2) -> g(2,2) -> g(0,2)。因此,要计算 g,我们只需要计算可能路径的数量。

让我们从总是从 x 中减去 a 的路径开始。显然,我们只有一个这样的路径。接下来,考虑这样一种情况,一旦我们选择减去 1 的路径,而其他时候减去 a,我们有 floor((xa)/a) 位置来选择 1。因此,有 floor((xa)/a ) 在这种情况下可能的路径。在下一次迭代中,我们要选择步骤 1 两次。有 n*(n-1)/2 组合,其中 n=floor((x-1-a)/a) 和 n*(n-1)/2 是二项式系数 \binom{n,2} 。下一步三个有 \binom{n,3} 组合,其中 n 现在是 = floor((x-2-a)/a) 等。

如果你预先计算二项式系数,算法是 O(x),因此它可能也可以计算 g(sqrt(10000019),10000019)。但是,最大的原生 c++ 整数类型(无符号长长整数)已经在 g(sqrt(500),500) 附近溢出。您可以使用 long double 来获得稍大输入的近似值。或者,您可以使用 boost Multiprecision Library 来获得更多数字,但我假设您在得到 g(sqrt(10000019),10000019) 的答案之前会耗尽内存。

带有溢出检查来计算g()的源代码

#include <iostream>
#include <vector>
#include <limits>
#include <algorithm>
#include <cstdlib>

unsigned long long binomial(unsigned int n, unsigned int m) {
  if (n - m < m) m = n - m;
  std::vector<unsigned long long>bc(m+1, 0);
  bc[0] = 1;
  for (unsigned int i = 0;i <= n;++i) {
    for (unsigned int j = std::min(i, m);j > 0;j--) {
      if (std::numeric_limits<unsigned long long>::max()-bc[j-1] < bc[j]) {
        std::cout << "Error: too large binomial coefficient(" << n << "," << m << ")" << std::endl;
        exit(1);
      }
      bc[j] += bc[j - 1];
    }   
  }
  return bc[m];
}

unsigned long long g(double a, double x) {
  unsigned long long r = 1;
  int n = 0;
  for (int i = static_cast<int>(x);i >= a;--i) {
    ++n;
    int m = static_cast<int>((i - a) / a);
    unsigned long long b = binomial(m + n, n);
    if (std::numeric_limits<unsigned long long>::max() - b < r) {
      std::cout << "Error: too large sum " << b << "+" << r << std::endl;
      exit(1);
    }
    r += b;
  }
  return r;
}

int main(){
  std::cout << g(sqrt(90), 90) << std::endl;
  std::cout << g(sqrt(10000019), 10000019) << std::endl;
}

【讨论】:

  • 我想我应该考虑更多的学校教育。这有点过头了。
【解决方案2】:

我想仅供参考,这里是没有递归的实现。我不确定它是否会完成给定的输入:

#include <stack>
double gnr(double a, double x) {
   std::stack<double> stack;
   double result = 0;
   stack.push(x);
   while (!stack.empty()) {
      x = stack.top();
      stack.pop();
      //cout << stack.size() << " " << x << endl;
      if (x < a) {
         result++;
      } else {
         stack.push(x - 1);
         stack.push(x - a);
      }
   }
   return result;
}

【讨论】:

  • 哦,找到了。是#import &lt;stack&gt;
【解决方案3】:

Memorization 可以有效地限制计算所需的递归调用次数。尝试为一些简单的输入评估函数,例如g(2, 8),你会发现你最终会一遍又一遍地为相同的值评估函数。通过在第一次计算时缓存每组输入的结果,可以缩短递归并显着减小问题的规模。

要使函数迭代而不是递归,您可以使用的一种策略是尝试翻转定义并自下而上进行迭代。考虑斐波那契函数:

fib(n) = fib(n-1) + fib(n-2)

为了迭代计算 fib(n),我们从基本情况 fib(1) + fib(0) 开始,一直迭代到 fib(n)。这使您可以在进行过程中累积值,而不必在计算中间值时(一遍又一遍地)记住您的位置。所以fib() 的迭代定义如下:

fib(n) {
    a = 1;
    b = 0;
    fib = 0;
    i = 1;
    while (i < n) {
        fib = a + b;
        b = a;
        a = fib;
        i++;
    }
    return fib;
}

你应该可以用你的g() 函数做类似的事情。我没有时间玩它,但我敢打赌,如果您尝试手动评估几个 a, x 对,您会注意到一种模式,它可以让您以迭代形式重写函数,就像我一样为fib() 完成了上述操作。

【讨论】:

  • 是的,这东西求记忆,但这不是他要的。
  • @MK。我提到 memoization 是因为 OP 要求“任何阻止 segfault 的东西”,而 memoization 在这方面是朝着正确方向迈出的一大步。
  • 好吧,天真的记忆不会削减它——它仍然会在第 1 步中从大输入变为小输入。我认为我们需要颠倒顺序从小到大并记忆.
  • 实际上我试过了,我对记忆的初步评估是错误的。因为数字是小数,所以命中数不足以使其合理。
猜你喜欢
  • 1970-01-01
  • 2019-09-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-16
相关资源
最近更新 更多