如何计算大 n 的 2^n?

Posted

技术标签:

【中文标题】如何计算大 n 的 2^n?【英文标题】:How can I calculate 2^n for large n? 【发布时间】:2019-03-27 20:34:04 【问题描述】:

我正在尝试编写一个程序,它将一个数字 n 作为输入,并输出 2 的结果为 n 的幂。问题是,n 可能非常大(最多 100,000)。本质上,我正在尝试为非常大的数字计算 pow(2, n);

我认为这样做的方法是将数字存储在一个数组中,因为没有内置的数字类型可以保存这么大的值。

数字采用十进制格式(以 10 为底)。

我使用的是 C,而不是 C++,所以我不能使用 STL 向量和其他 C++ 容器。我也不能使用外部库,比如GMP。我需要用纯 C 手动实现算法。

【问题讨论】:

最低有效位存储在第一个数组条目(索引 0)还是最后一个?有一个带有一个输入 n=... 和预期结果数组的示例会很有帮助。 @Socowi 数字是在数组的末尾还是在数组的开头对我来说并不重要。我想最后制作算法会更容易。例如,对于 n=9,数组应该是 00000...000512 乘法的方法?我们在学校教的那个 - 逐个数字相乘,移动进位等等 第 1 步:编写一个函数,将一个值的 2 个字符串十进制表示相乘。第 2 步使用Exponentiation by squaring 从一开始就知道有用的可能是 2^n 需要 ceil(n * log10(2)) 十进制数字(对于 n-100000 为 30103)。 【参考方案1】:

问题不在于计算 2 的高次幂,而是将此数字转换为十进制表示:

让我们用无符号 32 位整数数组来表示大数。 计算 2n 就像设置一个位一样简单。 可以通过将此数字反复除以 1000000000 来转换为二进制,一次生成 9 位数字。

这是一个简单但快速的实现:

#include <stdint.h>
#include <stdio.h>

void print_2_pow_n(int n) 
    int i, j, blen = n / 32 + 1, dlen = n / 29 + 1;
    uint32_t bin[blen], dec[dlen];
    uint64_t num;

    for (i = 0; i < blen; i++)
        bin[i] = 0;
    bin[n / 32] = (uint32_t)1 << (n % 32);

    for (j = 0; blen > 0; ) 
        for (num = 0, i = blen; i-- > 0;) 
            num = (num << 32) | bin[i];
            bin[i] = num / 1000000000;
            num = num % 1000000000;
        
        dec[j++] = (uint32_t)num;
        while (blen > 0 && bin[blen - 1] == 0)
            blen--;
    
    printf("2^%d = %u", n, dec[--j]);
    while (j-- > 0)
        printf("%09u", dec[j]);
    printf("\n");


int main() 
    int i;
    for (i = 0; i <= 100; i += 5)
        print_2_pow_n(i);
    print_2_pow_n(1000);
    print_2_pow_n(10000);
    print_2_pow_n(100000);
    return 0;

输出:

2^0 = 1
2^5 = 32
2^10 = 1024
2^15 = 32768
2^20 = 1048576
2^25 = 33554432
2^30 = 1073741824
2^35 = 34359738368
2^40 = 1099511627776
2^45 = 35184372088832
2^50 = 1125899906842624
2^55 = 36028797018963968
2^60 = 1152921504606846976
2^65 = 36893488147419103232
2^70 = 1180591620717411303424
2^75 = 37778931862957161709568
2^80 = 1208925819614629174706176
2^85 = 38685626227668133590597632
2^90 = 1237940039285380274899124224
2^95 = 39614081257132168796771975168
2^100 = 1267650600228229401496703205376
2^1000 = 10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
2^10000 = 1995063116880758384883742<...>91511681774304792596709376
2^100000 = 9990020930143845079440327<...>97025155304734389883109376

2100000 有 30103 个数字,正好是 floor(100000 * log10(2))。它在我的旧笔记本电脑上执行只需 33 毫秒。

【讨论】:

我喜欢your while (j --&gt; 0) loop @stef:著名的 dowto 运算符是迭代数组的最佳解决方案,索引值递减,它也适用于无符号类型:) @stef:我喜欢这个问题,尤其是风景如画的x slides to 0【参考方案2】:

只需制作一个位数组并设置第 n 位。然后除以 10,就好像位数组是 little-endian 编号并反向打印余数,以获得以 10 为底的 2 的 n 次方表示。

下面的这个快速程序可以做到这一点,它给我的结果与bc 相同,所以我想它可以工作。 打印例程可以使用一些调整。

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

uint_least32_t div32(size_t N, uint_least32_t Z[/*N*/], uint_least32_t X[/*N*/], uint_least32_t Y)

    uint_least64_t carry; size_t i;
    for(carry=0, i = N-1; i!=-1; i--)
        carry = (carry << 32) + X[i], Z[i] = carry/Y, carry %= Y;
    return carry;


void pr10(uint_least32_t *X, size_t N)

    /*very quick and dirty; based on recursion*/
    uint_least32_t rem=0;
    if(!X[N?N-1:0]) return;
    rem = div32(N,X,X,10);
    while(N && !X[N-1]) N--;
    pr10(X,N);
    putchar(rem+'0');

int main(int C, char **V)

    uint_least32_t exp = atoi(V[1]);
    size_t nrcells = exp/32+1;
    uint_least32_t *pow  = calloc(sizeof(uint_least32_t),nrcells);
    if(!pow) return perror(0),1;
    else pow[exp/32] = UINT32_C(1)<<(exp%32);
    pr10(pow,nrcells);


示例运行:

$ ./a.out 100
1267650600228229401496703205376

【讨论】:

这适用于 exp = 80'000 则没有输出。知道为什么吗? @Socowi 可能由于打印例程中的递归导致堆栈溢出。 @Socowi 我在那里犯了一个错误。它现在应该可以工作了。【参考方案3】:

第 1 步:决定如何表示 bignums

已经存在为此的库。 GNU Multiple Precision Integer library 是一个常用的选项。 (但根据您的编辑,这不是一个选项。您可能仍会瞥一眼其中的一些以了解他们如何做事,但这不是必需的。)

如果你想自己滚动,我确实建议存储十进制数字。如果这样做,则每次要对组件进行算术运算时,都需要在二进制表示形式之间进行转换。最好有一个类似uint32_ts 的链接列表,以及一个符号位。当你想读写时,你可以从/到十进制转换,但是用二进制做你的数学。

第 2 步:实现求幂

我将在这里假设链表 bignum 实现;您可以根据需要调整算法。

如果您只是计算 2 的幂,那很容易。它是一个 1 后跟 N 个 0,因此如果每个块存储 M 位并且您想表示 2^N,那么只需将 floor(N/M) 块全为 0,并将 1 &lt;&lt; (N % M) 存储在最重要的块中。

如果您希望能够以有效的方式对 任意 基进行求幂,则应使用 exponentiation by squaring。这背后的想法是,如果你想计算 3^20,你不要乘以 3 * 3 * 3 * ... * 3。相反,你计算 3^2 = 3 * 3。然后3^4 = 3^2 * 3^2. 3^8 = 3^4 * 3^4. 3^16 = 3^8 * 3^8。您可以随时存储这些中间结果。然后,一旦达到再次平方会产生比您想要的更大的数字的程度,您就停止平方并从您拥有的部分组装最终结果。在这种情况下,3^20 = 3^16 * 3^4

这种方法以 5 步而不是 20 步计算最终结果,并且由于时间是指数的对数,因此指数越大,速度增益越明显。即使计算 3^100000 也只需要 21 次乘法。

据我所知,没有一种聪明的乘法方法;您可能可以按照您在小学学习的基本长乘法算法做一些事情,但是在块级别:我们之前使用uint32_ts 而不是 uint64_t`s 的原因是我们可以将操作数转换为较大的类型并将它们相乘而不会有丢失进位溢出的风险。

从二进制转换为十进制以进行打印

首先,找出比你的数小 10 的最大倍数。 我将高效留给读者作为练习,但是您可以通过求幂来管理它,通过平方来找到一个上限,然后减去各种存储的中间值以更快地得到实际值比你反复除以 10 的方式。

或者你可以通过反复乘以10来找到数字;无论第一部分如何处理,其余部分都将是线性的。

但是不管你怎么理解,你有一个q 这样q = k * 10, 10 * q &gt; n, q &lt;= n,你一次只能循环一个十进制数字:

for (; q; q /= 10) 
   int digit = n / q; //truncated down to floor(n/q)
   printf("%d", digit);
   n -= digit * q;

在某处的文献中可能有一种更有效的方法,但我不熟悉一种副手。但这不是什么大不了的事情,只要我们在写输出时只需要做低效的部分即可;无论算法如何,这都很慢。我的意思是,打印所有 100,000 个数字可能需要一两毫秒。当我们显示供人类消费的数字时,这无关紧要,但如果我们不得不在某个地方的循环中等待一毫秒作为计算的一部分,它会加起来并变得非常低效。这就是我们以十进制表示形式存储数字的原因:通过在内部将其表示为二进制,我们在输入和输出时执行一次低效的部分,但介于两者之间的一切都很快。

【讨论】:

快速傅立叶变换比平方取幂更好。 这并不能解决出现的问题。没有以十进制形式表示结果的直接方式。 这真的很好(特别是对于 2 方法的幂),但不幸的是,C 的 std 库没有将 1 后跟 100k 个零从基数 2 转换为基数 10 并写入一个的快速功能基本上是一遍又一遍地乘以2,这很好......从头开始做这个问题。 答案在哪里? @EugeneSh。我认为它超出了问题的范围,但我已经添加了一个关于如何做到这一点的快速解释。【参考方案4】:

由于原始问题陈述没有指定输出基数,这里是一个笑话实现:

#include <stdio.h>

void print_2_pow_n(int n) 
    printf("2^%d = 0x%d%.*d\n", n, 1 << (n % 4), n / 4, 0);


int main() 
    int i;
    for (i = 0; i < 16; i++)
        print_2_pow_n(i);
    print_2_pow_n(100);
    print_2_pow_n(100000);
    return 0;

输出:

2^0 = 0x1
2^1 = 0x2
2^2 = 0x4
2^3 = 0x8
2^4 = 0x10
2^5 = 0x20
2^6 = 0x40
2^7 = 0x80
2^8 = 0x100
2^9 = 0x200
2^10 = 0x400
2^11 = 0x800
2^12 = 0x1000
2^13 = 0x2000
2^14 = 0x4000
2^15 = 0x8000
2^100 = 0x10000000000000000000000000
2^100000 = 0x10...<0 repeated 24998 times>...0

【讨论】:

两个基数的二次幂。那是作弊! :D O(n) 解决方案,所以紫外线。注意:100,000 可能超过*printf() 环境限制,(c18 § 7.21.6.1 15)至少为 4095 - 因此“破坏”printf() @chux:确实是对printf 执行质量的压力测试。它可以在 OS/X 上使用基于 Apple BSD 的 Libc 以及在带有 GNU libc 的 Linux 上正常工作。其他系统可能不那么坚固:)【参考方案5】:

这是一个相当幼稚和低效的解决方案。根据要求,这些数字以十进制数字数组表示。我们通过将数字 2 与自身重复相加来计算指数 2n: 从e := 2 开始并重复e := e + e n 次。

要为 digits 数组的长度设定上限,我们使用以下方法:

以 b 为底的数字 x 的表示形式有 ⌈logb(x)⌉ 位。 比较任何数字 x 的二进制和十进制表示之间的位数,我们发现如果我们忽略四舍五入 (⌈⌉),它们只会相差一个常数因子。 log2(x) / log10(x) = 1 / log10(2) = 3.3219... > 3 2n 有 log2(2n) = n 个二进制数字。 因此 2n 大约有 n / 3 个十进制数字。由于四舍五入的问题,我们为此添加了 +1。

void print(int digits[], int length) 
    for (int i = length - 1; i >= 0; --i)
        printf("%d", digits[i]);
    printf("\n");

void times2(int digits[], int length) 
    int carry = 0;
    for (int i = 0; i < length; ++i) 
        int d = 2 * digits[i] + carry;
        digits[i] = d % 10;
        carry = d / 10;
    

int lengthOfPow2(int exponent) 
    return exponent / 3 + 1;

// works only for epxonents > 0
void pow2(int digits[], int length, int exponent) 
    memset(digits, 0, sizeof(int) * length);
    digits[0] = 2;
    for (int i = 1; i < exponent; ++i)
        times2(digits, length);

int main() 
    int n = 100000;
    int length = lengthOfPow2(n);
    int digits[length];
    pow2(digits, length, n);
    print(digits, length);
    return 0;

在类 unix 系统上,您可以使用以下方法检查固定 n 的正确性

diff \
  <(compiledProgram | sed 's/^0*//' | tr -d '\n') \
  <(bc <<< '2^100000' | tr -d '\n\\')

正如已经指出的那样,这种解决方案效率不高。使用clang -O2 编译,计算 2100'000 在 Intel i5-4570 (3.2GHz) 上耗时 8 秒。

加快速度的下一步是重复将您的数字乘以 2,而不是重复乘以 2。即使是简单的立方体步骤实现,这也应该比此答案中提供的实现更快。

如果您需要更高效,可以使用 Karatsuba 算法甚至快速傅立叶变换 (FFT) 等方法来实现立方体步骤。使用立方方法和 FFT,您可以在 O(n·log(n)) 左右计算 2n(由于 FFT 中的舍入问题,可能会有额外的 log(log(n)) 因子) .

【讨论】:

【参考方案6】:

我无法找到对数复杂度的解决方案(乘方求幂),但我确实设法编写了一个时间复杂度为 O(noOfDigits*pow), noOfDigits in 2^ 的简单实现n 将是 n*log10(2)+1;

我只检查了https://www.mathsisfun.com/calculator-precision.html的前几位数字的答案,它似乎是正确的。

#include <stdio.h>
#include <math.h>
//MAX is no of digits in 2^1000000
#define MAX 30103
int a[MAX];
int n;
void ipow(int base, int exp,int maxdigits)

    a[0]=1;
    for (;exp>0;exp--)
            int b=0;
            for(int i=0;i<maxdigits;i++)
                a[i]*=base;
                a[i]+=b;
                b=a[i]/10;
                a[i]%=10;
            
    

int main()

    int base=2;
    int pow=100000;
    n=log10(2)*pow+1;
    printf("Digits=%d\n",n);
    ipow(base,pow,n);
    for(int i=n-1;i>=0;i--)
        printf("%d",a[i]);
    
    return 0;

我还编写了通过平方但未优化的乘法函数求幂的代码。这似乎比上述实现更快。

#define MAX 30103
int a[MAX];
int b[MAX];
int z[MAX];
//stores product in x[]; mul of large arrays implemented in n^2 complexity
//n and m are no of digits in x[] and y[]
//returns no of digits in product
int mul(int x[],int y[],int n,int m)
    for(int i=0;i<n+m;i++)
        z[i]=0;
    for(int j=0;j<m;j++)
        int c=0;
        for(int i=0;i<n+m;i++)
            z[i+j]+=x[i]*y[j];
            z[i+j]+=c;
            c=z[i+j]/10;
            z[i+j]%=10;
        
    
    for(int i=0;i<n+m;i++)
            x[i]=z[i];
    
    if(x[n+m-1]==0)
        return n+m-1;
    return n+m;

//stores answer in x[]
int ipow(int base, int exp)

    int n=1,m=0;
    for(int i=0;base>0;i++)
        b[i]=base%10;
        base/=10;
        m++;
    
    a[0]=1;
    for (;;)
    
        if (exp & 1)
            n=mul(a,b,n,m);
        exp >>= 1;
        if (!exp)
            break;
        m=mul(b,b,m,m);
    

int main()

    int base=2;
    int pow=100000;
    n=log10(2)*pow+1;
    printf("Digits=%d\n",n);
    ipow(base,pow);
    printf("\n");
    for(int i=n-1;i>=0;i--)
        printf("%d",a[i]);
    
    return 0;

【讨论】:

不错的贡献。也做得足够复杂,如果学生在不理解的情况下上交,他们会得到一个很大的 0。 26 秒的执行时间来计算 2^100000。 是的,将结果输出到字符串表中,然后使用它。即时运行时间:) @SandeepPolamuri 您对测量时间的评论可能有点误导。是的,只用clang -lm 编译我也得到30 秒,但用clang -O2 只用7 秒。顺便说一句,我自动比较了 pow=100'000 的结果,所有数字都是正确的。 刚刚实现 Java BigInteger 可以在 1 秒内完成同样的操作。

以上是关于如何计算大 n 的 2^n?的主要内容,如果未能解决你的问题,请参考以下文章

如何计算时间复杂度

用啥软件来计算算法的复杂度??

如何找到 2 的 n 次方。 n 范围从 0 到 200

计算机快速计算,2^N是如何实现的?

如何计算集合 1,2,3,..,n 的互质子集的数量

如何计算该函数的增长率:T(n)= 2T(n ^(1/2))+ 2(n ^(1/2))