在C ++中减少递归函数中堆栈的使用

时间:2016-11-24 19:04:16

标签: c++ c function recursion tail-recursion

我有一个计算任何数字的阶乘的程序。当我尝试使用100,000这样的大数字时,它会在达到0之前停止。我猜这是一种防止出现问题的安全机制。

虽然这很好,但它可以防止程序计算大量数字。在我的程序中,在变量x达到0之后,它会停止递归函数。所以不需要这个“安全网”。

以下是我的参考代码:

#include <iostream>
#include <string>

int answer = 1;
int recursive(int x);
using std::cout;
using std::cin;
int main() {

    recursive( 100000 );

}


int recursive( int x ) {
    cout << x << "\n";
    answer = x * answer;
    x--;
    if ( x > 0 ) {
        recursive( x );
    }
    else {
        cout << "Answer: " << answer << "\n";
    }
}

有没有办法解决这个障碍?

3 个答案:

答案 0 :(得分:4)

正如其他人所提到的,你将无法将100,000的因子拟合成64位类型,因为它需要大约150万位才能表示它。 (这是一个最后有25000个零的数字。)

但是,假设我们将问题从[1..100000]更改为递归添加。你仍然会遇到堆栈问题。堆栈是有限的,递归使用堆栈,因此对您可以进行的调用数量有一个基本限制。

对于像递归这样简单的事情,您可以使用tail recursion

来消除堆栈的大量使用

然后需要将代码更改为:

#include <iostream>
#include <string>

int answer = 1;
int recursive(int multiplier, int x=1);
using std::cout;
using std::cin;

int main() {

    std::cout << "Recursion result = " << recursive(100000) << std::endl;

}


int recursive(int multiplier, int x) {
    if (multiplier == 1) {
        return x;
    }
    return recursive(multiplier - 1, multiplier * x); // Change the * to + for experimenting with large numbers that could overflow the stack
}

在上面的例子中,由于在递归之后没有其他操作,编译器将优化并且不会耗尽堆栈。

答案 1 :(得分:3)

也许我有点太晚了,但我还是会添加我的建议和解决方案。它可能会帮助你(和其他人)另一次 stackoverflow问题的最佳解决方案实际上根本不使用递归:

int fac(int n){
    int res=1;
    for(int i = 0; i <= n; ++i){
        res *= i;
    }
    return res;
}

由于时间(函数调用)和它消耗的资源(堆栈),在编程时实际上会导致递归失效。在许多情况下,如果需要保存&#34;当前位置,可以通过使用循环和具有简单弹出/推送操作的堆栈来避免递归。 (在c ++中,可以使用vector)。在阶乘的情况下,甚至不需要堆栈,但是如果你在tree datastructure上进行迭代,那么你需要一个堆栈(虽然取决于实现)。

现在您遇到的另一个问题是int的大小限制:如果使用32位整数而不是{{1},则不能超过fac(12)对于64位整数。这可以通过使用实现大数字操作的外部库来解决(例如GMP library或Boost.multiprecision,如SenselessCoder所提到的)。但是您也可以从Java创建自己的BigInteger类的类,并实现像我一样的基本操作。我在我的例子中只实现了乘法,但增加非常相似:

fac(20)

主要思想很简单,#include <iostream> #include <vector> #include <stdio.h> #include <string> using namespace std; class BigInt{ // Array with the parts of the big integer in little endian vector<int> value; int base; void add_most_significant(int); public: BigInt(int begin=0, int _base=100): value({begin}), base(_base){ }; ~BigInt(){ }; /*Multiply this BigInt with a simple int*/ void multiply(int); /*Print this BigInt in its decimal form*/ void print(); }; void BigInt::add_most_significant(int m){ int carry = m; while(carry){ value.push_back(carry % base); carry /= base; } } void BigInt::multiply(int m){ int product = 0, carry = 0; // the less significant part is at the beginning for(int i = 0; i < value.size(); i++){ product = (value[i] * m) + carry; value[i] = product % base; carry = product/base; } if (carry) add_most_significant(carry); } void BigInt::print(){ // string for the format depends on the "size" of the base (trailing 0 in format => fill with zeros if needed when printing) string format("%0" + to_string(to_string(base-1).length()) + "d"); // Begin with the most significant part: outside the loop because it doesn't need trailing zeros cout << value[value.size()-1]; for(int i = value.size() - 2; i >= 0; i-- ){ printf(format.c_str(), value[i]); } } 表示将little endian表示形式分割成一个大的十进制数。这些碎片的长度取决于您选择的基础。 仅当您的基数为10 时才会起作用:如果您选择10作为基数,则每件将代表一位数,如果您选择100(= 10 ^ 2)作为基数,则每件将代表两个从末尾开始的连续数字(见小端),如果选择1000作为基数(10 ^ 3),每个数字将代表三个连续数字,......等等。假设你有100,12765将是BigInt,1789将是[65, 27, 1],505将是[89, 17](= [05,5]),... 。基数1000:12765为[5, 5],1789为[765, 12],505为[789, 1]
然后乘法有点像我们在学校学到的纸上乘法:

  1. [505]
  2. 的最低部分开始
  3. 将其乘以乘数
  4. 该产品的最低部分(=产品模数,基础)成为最终结果的相应部分
  5. &#34;更大&#34;该产品的各个部分将添加到以下产品中
  6. 转到下一篇
  7. 的第2步
  8. 如果没有剩下的部分,请将BigInt的最后一部分产品的剩余较大部分添加到最终结果中
  9. 例如:

    BigInt

    如果我使用上面的类来计算1到30之间所有数字的阶乘,如下面的代码所示:

    9542 * 105 = [42, 95] * 105
        lowest piece = 42 --> 42 * 105 = 4410 = [10, 44]
                    ---> lowest piece of result = 10
                    ---> 44 will be added to the product of the following piece
        2nd piece = 95    --> (95*105) + 44 = 10019 = [19, 00, 1]
                    ---> 2nd piece of final result = 19
                    ---> [00, 1] = 100 will be added to product of following piece
        no piece left --> add pieces [0, 1] to final result
    ==> 3242 * 105 = [42, 32] * 105 = [10, 19, 0, 1] = 1 001 910
    

    它会得到以下结果:

     int main() {
        cout << endl << "Let's start the factorial loop:" << endl;
        BigInt* bigint = new BigInt(1);
        int fac = 30; 
        for(int i = 1; i <= fac; ++i){
            bigint->multiply(i);
            cout << "\t" << i << "! = ";
            bigint->print();
            cout << endl;
        }
        delete bigint;
        return 0;
    }
    

    我对长期答案的批评。我试图尽可能简短但仍然完整。欢迎提出问题 祝你好运!

答案 2 :(得分:2)

我可以为你遇到的一些问题提出一些建议。

你没有评估每个递归步骤的问题是因为你有一个堆栈溢出。当使用更多的堆栈空间而不是你想要的时候会发生这种情况。您可以通过保留以前计算的值表来避免这种情况。请注意,如果您立即想要计算100000的阶乘,它将无济于事,但如果您通过计算,例如10!,然后20来慢慢爬上去!等你不会有这个问题。另外一件事是增加堆栈大小。我看到了一些关于此的评论,所以我不会提到如何。

您将遇到的下一个问题是,您将无法表示因为阶乘而产生的数字。这是因为您的整数大小不足以表示这些数字。换句话说,你正在溢出整数。为此,以及上述观点,您可以看到:Boost.multiprecision。 Boost是一个非常好的库,你可以用它来做这样的事情。