【问题标题】:Calculating factorial of large numbers in C在C中计算大数的阶乘
【发布时间】:2010-11-25 22:20:24
【问题描述】:

在我的 C 代码中,我想计算 1 到 100 范围内数字的阶乘。对于较小的数字,该函数有效,但对于较大的数字(例如 100!),它返回不正确的结果。有什么方法可以在 C 中处理大数的阶乘?

我使用的编译器是 gcc v4.3.3。 我的代码如下:

#include <stdio.h>
#include <math.h>

double print_solution(int);

int main(void)
{
        int no_of_inputs, n;
        int ctr = 1;

        scanf("%d",&no_of_inputs); //Read no of inputs

        do
        {
                scanf("%d",&n); //Read the input
                printf("%.0f\n", print_solution(n));
                ctr++;  
        } while(ctr <= no_of_inputs);

        return 0;       
}

double print_solution(int n)
{
        if(n == 0 || n == 1)
                return 1;
        else
                return n*print_solution(n-1);
}

【问题讨论】:

  • 为什么 print_solution 声明返回双精度?
  • 因为 double 的范围比 int 大,不是吗? :)

标签: c algorithm


【解决方案1】:

没有标准的 C 数据类型可以准确处理 100 大的数字!如果使用 arbitrary precision integer arithmetic,您唯一的选择是通过库或自己完成。

如果这只是一些爱好项目,我建议您自己尝试一下。这是一种有趣的运动。如果这与工作相关,请使用预先存在的库。

您通常会得到的最大 C 数据类型是 64 位整数。 100!是10157的数量级,至少需要525位才能准确存储为整数。

【讨论】:

  • +1 在有趣的部分。我们在大学就这样做了。这至少是说明性的。如果您出于专业目的这样做,请使用现有库。
  • 如何在不使用预先存在的库的情况下在 C 中实现任意精度整数运算?
  • @alexBrand 商店编号是数组。这可以是数字(字符)数组,将两个数字打包成一个字节(高和低 4 位,每个使用 0-9)或许多其他方案之一。数组可以与内存允许的一样大。然后,您需要根据这些数据结构实现算术运算。
【解决方案2】:

100 阶乘很大,准确地说是 93326215443944152681699238856266700490715968264381621468592963895217 59999322991560894146397615651828625369792082722375825118521091686400 00000000000000000000。

也许你应该使用像 GMP 这样的 bignum 库。它有很好的文档、相当一致的界面、速度,如果你在 Linux 上,你的发行版可能有一个包(我认为我的默认安装它)

【讨论】:

  • 如果您愿意跳过100! 循环,您甚至可以在 OS X 上获取 GMP 库。
【解决方案3】:

要近似计算大数的阶乘,您可以这样:

嗯! = n * (n-1)! 所以 log(n!) = log(n) + log(n-1!)

现在您可以使用动态规划来计算 log(n!) 和计算
嗯!作为(基数)^(对数值)

【讨论】:

  • 不是整数,而是获得双精度近似值的好方法
  • 举个例子here in Javacript
【解决方案4】:

如果您不想使用 bigint 库,最好使用 stdlib 中的 long double 和 tgammal() 来自 math.h:

long double fact(unsigned n)
{
    return tgammal(n + 1);
}

这将为您提供100!,在 x86 上精度为 18 位小数(即 80 位 long double)。

确切的实现也没有那么复杂:

#include <math.h>
#include <stdio.h>
#include <string.h>

void multd(char * s, size_t len, unsigned n)
{
    unsigned values[len];
    memset(values, 0, sizeof(unsigned) * len);
    for(size_t i = len; i--; )
    {
        unsigned x = values[i] + (s[i] - '0') * n;
        s[i] = '0' + x % 10;
        if(i) values[i - 1] += x / 10;
    }
}

void factd(char * s, size_t len, unsigned n)
{
    memset(s, '0', len - 1);
    s[len - 1] = '1';
    for(; n > 1; --n) multd(s, len, n);
}

int main(void)
{
    unsigned n = 100;
    size_t len = ceill(log10l(tgammal(n + 1)));
    char dstr[len + 1];
    dstr[len] = 0;
    factd(dstr, len, n);
    puts(dstr);
}

【讨论】:

  • 这是您在此处使用的“不复杂”的有趣定义。对我来说,“不复杂”的意思是 type_t fact(type_t x) { return x ? x * fact(x - 1) : 1; }
  • 我没有说'不复杂' ;) - 它没有那么复杂:如果你知道如何手动乘法,那么实现很简单......
  • 我喜欢这个解决方案,但我对一件事感到困惑:tgamma(n) = factorial(n-1)。 IE。这不是先计算阶乘,将其长度作为字符,然后再次手动计算阶乘吗?
  • @Ross:确实如此。不过,这不是算法的重要部分:您可以让您的穷人的 bigint 增长,或者使用另一个近似值 log_10(n!)。对于给定的玩具问题(从 1 到 100 的阶乘),这只是让它工作的最方便的方法
【解决方案5】:

每个人都在告诉你正确的答案,但还有几点。

  1. 您最初使用双精度来获得更大范围的想法行不通,因为双精度无法精确存储此数据。它可以进行计算,但需要进行大量舍入。这就是 bigint 库存在的原因。

  2. 我知道这可能是来自教程或示例站点的示例,但执行无限递归会在某些时候对您不利。对于本质上是一个迭代过程,您有一个递归解决方案。当您尝试使用更大的值(尝试 10000)运行您的程序时,您将理解为什么该站点被命名为原来的名称。

一个简单的迭代方法如下

  int answer, idx;

  for (answer = 1, idx = 1; idx <= no_of_inputs; idx++ ) {
    answer = answer * idx;
  }
  printf("Factorial of %3d =  %d\n", no_of_inputs, answer);

【讨论】:

  • @Steve Weet - 您可以记住该函数,这将允许它逐步计算更大的阶乘。我发现在 C 语言中性能提升通常很小,但这有助于缓解递归问题。
  • @Chris:我认为您正在考虑递归计算斐波那契数——递归阶乘函数仍应仅产生 N(或 N+1,取决于您的基数是 1 还是 0)调用计算事实(N)。问题是您正在生成 N 或 N+1 堆栈帧。一种解决方案是迭代地编写它或利用尾递归(gcc 至少会使用 -O2 优化尾调用)。这需要添加一个显式的累加器参数。
  • 你会在堆栈溢出之前打破double。
【解决方案6】:

这是我几年前用来解决谷歌谜题的,它使用 GMP 库 http://gmplib.org/:

#include <stdio.h>
#include "gmp.h"

void fact(mpz_t r,int n){
    unsigned int i;
    mpz_t temp;
    mpz_init(temp);
    mpz_set_ui(r,1);
    for(i=1;i<=n;i++){
        mpz_set_ui(temp,i);
        mpz_mul(r,r,temp);
    }
    mpz_clear(temp);
}
int main(void) {
    mpz_t r;
    mpz_init(r);
    fact(r,188315);
    /* fact(r,100); */
    gmp_printf("%Zd\n",r);
    mpz_clear(r);
    return(0);
}

gcc -lgmp -o fact fact.c

./事实

【讨论】:

    【解决方案7】:

    您可以尝试使用“unsigned long long”类型,但这是您可以使用内置类型获得的最大值。 我建议(正如 cletus 已经提到的)要么使用已知的大数字实现,要么自己编写一个。 “这是一个很好的练习”x 2。

    【讨论】:

    • 这在 ISO C90 中是不允许的。但是,我同意你回答的后半部分。 :)
    【解决方案8】:

    如果您只想使用标准数据类型并且不需要确切的答案,请计算 n 的对数!而不是 n!本身。 n 的对数!很容易适合double(除非 n 很大)。

    【讨论】:

      【解决方案9】:

      在 C 中处理大数阶乘的任何方法?

      由于阶乘可以迅速超出标准固定宽度整数的范围,甚至像 double 这样的浮点类型,代码应该考虑允许无限整数精度的用户类型精确答案。

      存在各种宽整数精度库,但如果代码需要简单的解决方案,请考虑使用 字符串。

      下面的内容并不快,也不注意数组的边界,但说明了这个想法。将'0'-'9' 转换为0-9 非常浪费,但这确实可以轻松进行逐步调试。

      #include <stdlib.h>
      #include <string.h>
      #include <stdio.h>
      
      static char *strfact_mult(char *s, unsigned x) {
        unsigned sum = 0;
        size_t len = strlen(s);
        size_t i = len;
        while (i > 0) {
          sum += (s[--i] - '0') * x;
          s[i] = sum % 10 + '0';
          sum /= 10;
        }
        while (sum) {
          len++;
          memmove(&s[1], s, len);
          s[i] = sum % 10 + '0';
          sum /= 10;
        }
        return s;
      }
      
      char *str_fact(char *dest, unsigned n) {
        strcpy(dest, "1");
        while (n > 1) {
          strfact_mult(dest, n--);
        }
        return dest;
      }
      
      void test_fact(unsigned n) { 
        char s[1000];
        printf("%3u %s\n", n, str_fact(s, n));
      }
      
      int main(void) {
        test_fact(0);
        test_fact(4);
        test_fact(54);
        test_fact(100);
      }
      

      输出

        0 1
        4 24
       54 230843697339241380472092742683027581083278564571807941132288000000000000
      100 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
      

      【讨论】:

        【解决方案10】:

        以下是您问题的解决方案:

        #include <stdio.h>
        void factorial(int b){
            int temp = 0, r, size = 0, x;
            int arr[200] = {0};
            int l_b = b-1;
            while(b>0){
                r = b%10;
                arr[size++] = r;
                b = b/10;
            }
            while(l_b >= 2){
                int i=0;
                while(size>0){
                 x = arr[i]*l_b+temp ;
                 arr[i++] = x%10;
                 temp = x/10;
                 size--;
                }
               while(temp>0){
                  arr[i++] = temp%10;
                  temp = temp/10;
               }
              size = i;  --l_b;
            }
            for(int k=size-1;k>=0;k--)
                printf("%d",arr[k]);//ok i'm taking space here
            printf("\n");
        }
        int main(void) {
            // your code goes here
            int fact;
        
            scanf("%d\n",&fact);
            factorial(fact); 
        
            return 0;
        }
        

        【讨论】:

          【解决方案11】:

          因子高达 12!适合 32 位整数。高达20的阶乘!适合 64 位整数。在那之后,你在大多数机器上都用完了。然而,34!适合一个无符号的 128 位整数,57!适合 256 位整数和 98!适合无符号 512 位整数。计算100!作为一个整数,你至少需要 525 位。

          此bc 脚本计算阶乘(最多 35 个!但您可以轻松更改限制):

          #!/usr/bin/bc -l
          
          define f(n) {
             auto r, i
             r = 1
             for (i = 1; i <= n; i++)
             {
                 r *= i;
                 print "n = ", i, ", log2 = ", l(r)/l(2), ", n! = ", r, "\n"
             }
          }
          
          f(35)
          quit
          

          还有一些样本值:

          # Key values
          # n =  1, log2 =   0.00000000000000000000, n! = 1
          # n =  2, log2 =   1.00000000000000000000, n! = 2
          # n =  3, log2 =   2.58496250072115618147, n! = 6
          # n =  4, log2 =   4.58496250072115618149, n! = 24
          # n =  5, log2 =   6.90689059560851852938, n! = 120
          # n =  6, log2 =   9.49185309632967471087, n! = 720
          # n =  7, log2 =  12.29920801838727881834, n! = 5040
          # n =  8, log2 =  15.29920801838727881836, n! = 40320
          # n =  9, log2 =  18.46913301982959118130, n! = 362880
          # n = 10, log2 =  21.79106111471695352921, n! = 3628800
          # n = 11, log2 =  25.25049273335425078544, n! = 39916800
          # n = 12, log2 =  28.83545523407540696694, n! = 479001600
          # n = 13, log2 =  32.53589495221649912738, n! = 6227020800
          # n = 14, log2 =  36.34324987427410323486, n! = 87178291200
          # n = 15, log2 =  40.25014046988262176421, n! = 1307674368000
          # n = 16, log2 =  44.25014046988262176426, n! = 20922789888000
          # n = 17, log2 =  48.33760331113296117256, n! = 355687428096000
          # n = 18, log2 =  52.50752831257527353551, n! = 6402373705728000
          # n = 19, log2 =  56.75545582601885902935, n! = 121645100408832000
          # n = 20, log2 =  61.07738392090622137726, n! = 2432902008176640000
          # n = 21, log2 =  65.46970134368498166621, n! = 51090942171709440000
          # ...
          # n = 34, log2 = 127.79512061296909618950, n! = 295232799039604140847618609643520000000
          # n = 35, log2 = 132.92440362991406264487, n! = 10333147966386144929666651337523200000000
          # ...
          # n = 57, log2 = 254.48541573017643505939
          # n = 58, log2 = 260.34339672530400718017
          # ...
          # n = 98, log2 = 511.49178048020535201128
          # n = 99, log2 = 518.12113710028496163045
          # n = 100, log2 = 524.76499329005968632625
          

          对于阶乘 57!, 58!, 98!, 99!, 100!我省略了阶乘值,因为它分布在输出中的多行上,并不是那么重要。注意100!至少需要 525 位的精度。

          此代码位于我在 GitHub 上的 SOQ(堆栈溢出问题)存储库中,作为 src/miscellany 子目录中的文件 factorial.bc。

          您可以使用double 或long double 来扩展值的范围,但会损失一些准确性。

          【讨论】:

            【解决方案12】:

            这肯定是由于溢出。您需要一种表示大数的方法(unsigned long long 甚至无法覆盖 21 个!)。

            【讨论】:

            • 你是对的,但我还要提到“unsigned long long”在标准 ISO C90 中不可用。
            【解决方案13】:

            我猜那是因为您超出了 int 范围,最多约为。 20亿。如果您使用 unsigned int,您最多可以获得 40 亿,但除此之外,您必须使用 bigint library。

            【讨论】:

              【解决方案14】:

              100! = 933262154439441526816992388562667004907159682643816214685929 6389521759999322991560894146397156518286253697920827223758251185210 9168640000000000000000000000000

              你不能用 int 或 long 来表示这么大的数字。

              【讨论】:

                【解决方案15】:

                除了其他人的建议之外,我建议您熟悉您实际使用的任何计算机/平台的基本类型(int、long、long long 等)的存储限制。 (“如有疑问,请打印更多内容!”)

                之前的一张海报提到了 80 位精度限制,但这是 x86 CPU 特有的。

                另一个人多次引用ISO C90,虽然C99是最新的标准;即使许多编译器还没有完全实现 C99,你可能会发现它们很可能至少支持 long long,这应该对应 >= 64 位精度。

                【讨论】:

                • 当然,在撰写本文后的十年里,C11 和 C18 标准已经发布。不过,它们不会对讨论产生实质性影响——这些后来的标准没有相关的变化。
                【解决方案16】:

                不要使用我认为的递归算法,它超级慢,即使缓存也会很慢。这只是您应该考虑的事情。

                这样做的原因是,当您调用 fact(100) 时,您实际上并没有运行它 100 次,而是实际上运行了该函数 5050 次。这很糟糕,如果它被缓存,那么它可能是 100 次,但是,使用 if 语句运行函数调用然后运行循环仍然更慢。

                double print_solution(int n)
                {
                    double rval = 1;
                    unsigned int i;
                
                    for( i = 1; i <= n; i++ ) {
                        rval *= i;
                    }
                
                    return rval;
                
                }
                

                使用任意精度算术可以让它变得非常高,但是,你需要使用一个库来做到这一点,或者你可以制作自己的库,但这需要很多时间。

                【讨论】:

                • 你能解释一下你的 5050 电话吗?我可以看到,如果您计算 1 到 100 之间的每个阶乘,那么您会经常调用它,但要一次调用来评估 100! - 我还不相信。
                • 我认为您正在考虑递归计算斐波那契数——递归阶乘函数仍应仅使 N(或 N+1,取决于您以 1 还是 0 为基数)调用计算事实(N)。
                • 5050 来自 (* 1/2 100 101),这与对前 100 个数字求和相同。现在我了解了函数式编程,我意识到递归实际上是一件好事,很抱歉我之前的陈述。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2016-11-29
                • 2023-03-22
                • 2015-01-08
                • 1970-01-01
                相关资源
                最近更新 更多