将递归函数转换为尾递归函数

Convert a recursive function to a tail recursive function

本文关键字:递归函数 转换      更新时间:2023-10-16

关于这个问题:

我在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)
}

我想知道如何将这个函数转换成某种迭代或尾循环(或任何停止分段错误的东西),但更重要的是,我需要知道如何实际自己做。


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

NOTE2:有类似的问题(像这个或这个),但我发现没有一个解决我的函数每次迭代调用自己两次的事实。

记忆可以是限制计算所需递归调用数量的有效方法。试着对一些简单的输入求值,比如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()所做的那样。

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

首先,因为所有的递归都是求和的,以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((x-a)/a)的位置来选择1。因此,在这种情况下,有1 ((x-a)/a)条可能的路径。在下一次迭代中,我们希望选择步骤1两次。有n*(n-1)/2组合,其中n=floor((x-1-a)/a), n*(n-1)/2是二项式系数binom{n,2}。下一步有三个1有binom{n,3}组合其中n现在是=floor((x-2-a)/a)等

如果你预先计算二项式系数,算法是O(x),因此它可能也可以计算g(sqrt(10000019),10000019)。然而,最大的原生c++整数类型(unsigned long long)已经在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;
}

我想只是作为参考,这里是实现无递归。我不确定它是否会完成给定的输入:

#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;
}