【问题标题】:Reducing usage of stack in recursive function in C++减少 C++ 递归函数中堆栈的使用
【发布时间】:2017-04-09 03:42:56
【问题描述】:

我有一个程序可以计算任何数字的阶乘。当我尝试对一个大数字(例如 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";
    }
}

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

【问题讨论】:

  • 您有一个更基本的问题,即 32 位或 64 位 int 只能表示相对较小的阶乘(12!对于 32 位 int,20!对于 64 位 int )。
  • @PaulR 我知道。但我还是想知道如何解决这个问题。
  • OK - 假设问题是堆栈溢出,您需要make the stack size bigger
  • 1) 当尝试多次调用递归函数时 - 您遇到了以该站点命名的问题 - stackoverflow,因为您的堆栈空间已用完。 2)您的代码具有 UB(未定义行为),因为您没有从函数返回任何内容,该函数被定义为返回 int
  • 函数返回一个int,但是没有return语句。

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


【解决方案1】:

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

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

由于时间(函数调用)和资源(堆栈)消耗,递归实际上在编程时被取消了。在许多情况下,如果需要保存“当前位置”(在 c++ 中可以使用vector),可以使用循环和带有简单弹出/推送操作的堆栈来避免递归。在阶乘的情况下,甚至不需要堆栈,但如果您正在迭代 tree datastructure,例如,您将需要一个堆栈(不过取决于实现)。

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

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

主要思想很简单,BigInt 通过将其little endian 表示切割成碎片来表示一个大十进制数。这些碎片的长度取决于您选择的底座。 只有当你的基数是 10 的幂时才有效:如果你选择 10 作为基数,每块代表一个数字,如果你选择 100 (= 10^2) 作为基数,每块代表两个从末尾开始的连续数字(请参阅小端),如果您选择 1000 作为基数(10^3),则每块将代表三个连续数字,......等等。假设您的基数为 100,那么 12765 将是 [65, 27, 1],1789 将是 [89, 17],505 将是 [5, 5] (= [05,5]),...基数为 1000:12765 将是 @987654337 @,1789 将是 [789, 1],505 将是 [505]
那么乘法就有点像我们在学校学过的纸上乘法:

  1. BigInt 的最低部分开始
  2. 乘以乘数
  3. 该乘积的最低部分(= 基数的乘积模数)成为最终结果的对应部分
  4. 该产品的“较大”部分将添加到以下部分的产品中
  5. 下一块进入第 2 步
  6. 如果没有剩余部分,则将BigInt 的最后一部分的剩余较大部分添加到最终结果中

例如:

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

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

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

它会给出以下结果:

Let's start the factorial loop:
    1! = 1
    2! = 2
    3! = 6
    4! = 24
    5! = 120
    6! = 720
    7! = 5040
    8! = 40320
    9! = 362880
    10! = 3628800
    11! = 39916800
    12! = 479001600
    13! = 6227020800
    14! = 87178291200
    15! = 1307674368000
    16! = 20922789888000
    17! = 355687428096000
    18! = 6402373705728000
    19! = 121645100408832000
    20! = 2432902008176640000
    21! = 51090942171709440000
    22! = 1124000727777607680000
    23! = 25852016738884976640000
    24! = 620448401733239439360000
    25! = 15511210043330985984000000
    26! = 403291461126605635584000000
    27! = 10888869450418352160768000000
    28! = 304888344611713860501504000000
    29! = 8841761993739701954543616000000
    30! = 265252859812191058636308480000000

对于冗长的答案,我深表歉意。我试图尽可能简短,但仍要完整。随时欢迎提问
祝你好运!

【讨论】:

    【解决方案2】:

    正如其他人所提到的,您将无法将 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
    }
    

    上述情况,由于递归后没有其他操作,编译器会进行优化,不会用完栈。

    【讨论】:

    • 137,000 位对应 100,000!?似乎太少了——即使不计算尾随的零。
    • 你是对的。我乘以 .3010 而不是除以它。将更新答案。它更像是 150 万位。
    • 你的函数不是尾递归的。该函数必须保留 x 直到实例执行,然后进行乘法运算。
    • 啊,是的,会改变 :( 刚刚写了反身实现。
    • 这是我的荣幸。明天将是充满成功的一天,别担心;)此外,只要不是rm -R ~ 之类的错误,一切都会好起来的
    【解决方案3】:

    对于您遇到的一些问题,我可以提出一些建议。

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

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

    【讨论】:

      猜你喜欢
      • 2015-04-14
      • 2012-10-20
      • 2018-10-03
      • 2023-03-27
      • 2015-09-12
      • 2017-09-12
      • 2018-06-19
      • 2015-01-28
      • 2018-05-28
      相关资源
      最近更新 更多