【问题标题】:Converting a big integer to decimal string将大整数转换为十进制字符串
【发布时间】:2016-10-04 13:40:49
【问题描述】:

冒着这个问题被投票为重复,甚至被关闭的风险,我提出了这个问题。

背景

在 int、long long 等“普通”数据类型中,要将二进制数值转换为十进制字符串,您需要执行以下操作(在伪代码中):

Set length = 0
Set divisor to largest base10 value the data type will hold (Divisor).
  Loop
    Divide number in question by divisor.
    Place result in a string at position length.
    Increment the length by 1.
    Divide the divisor by 10.
Reverse the string.
Print the string.

(大多数)任何语言的实际实现都非常简单。

问题

我在使用上述方法时遇到的问题是,对于大整数(也称为任意精度算术),没有最大的以 10 为底的值开始。所以问题是“如果无法知道该值是什么,如何将除数初始化为最大可能的 base10 值?”

我的尝试

仍在尝试起草解决方案。

研究

我在这里找到的一些链接包括以下内容:

Convert a "big" Hex number (string format) to a decimal number (string format) without BigInteger Class

C: print a BigInteger in base 10

Fastest way to convert a BigInteger to a decimal (Base 10) string?

Convert a "big" Hex number (string format) to a decimal number (string format) without BigInteger Class

谷歌搜索发现了其他东西,但没有什么能具体回答我的问题。

想法

我认为可能工作的一种方法如下(在伪代码中):

Define p_divisor as previous divisor.
Set divisor = 1
  Loop:
    if divisor < dividend
      then
        Set p_divisor = divisor
        divisor = divisor * 10
      else
        end loop
  Loop:
    Divide number in question by divisor.
    Place result in a string at position length.
    Increment the length by 1.
    Divide the divisor by 10.
    if divisor == 1 then end loop
Reverse the string.
Print the string.

这是正确的方法吗?我有一个大型整数库正在运行(包括乘法和除法),因此实现它并不难。我看到这种方法的最大问题是性能,因为你必须运行一个乘法序列来获得初始除数,然后你必须为每个 base10 位置除两次。一个用于实际除法,另一个用于除数。

【问题讨论】:

  • 我会反复除以 10,从右边生成数字。最后,反转字符串。
  • 每次除以 10,然后是 100,然后是 1000 等等,而是每次除以 10,并将余数作为下一个数字 - 一直持续到除数为 0。(和要么在末尾反转字符串,要么首先将其反向构建——如果你能弄清楚你的数字中实际使用了多少位,你可以估计你需要的十进制数字的数量密切)。
  • 我想你的意思是除以 1000,然后是 100,然后是 10。我想我明白你的意思了。余数是放在字符串中的数字,而结果是新的被除数。这实际上是个好主意。因为除数是 10,高速一字除数算法将把它烧穿一空。此外,由于我从头开始填充字符串,我认为不需要字符串反转步骤。这应该是一个答案,而不是评论。谢谢。
  • 第二种方法在 C 中不起作用。因为if divisor &lt; dividend then Set p_divisor = divisor divisor = divisor * 10 将溢出用于大 dividend 的任何整数类型

标签: c biginteger bigint format-conversion


【解决方案1】:

一种(相当常见的)方法,无论是对于大整数还是普通整数类型,都是重复将数字除以 10,将余数保存为下一个数字(从最低有效位开始)。继续前进,直到数字达到零。由于找到的第一个数字是最不重要的,因此您可能需要在末尾反转字符串,或者在进行时将其反向构建。

使用普通unsigned int 的示例可能如下所示:

void printUInt(unsigned x) {
  char buf[(sizeof(x) * CHAR_BIT) / 3 + 2]; // slightly oversize buffer
  char *result  = buf + sizeof(buf) - 1; // index of next output digit

  // add digits to result, starting at 
  //   the end (least significant digit)

  *result = '\0'; // terminating null
  do {
    *--result = '0' + (x % 10);  // remainder gives the next digit
    x /= 10;
  } while (x); // keep going until x reaches zero

  puts(result);
}

这个过程对于一个大整数来说几乎是一样的——尽管如果可以的话,最好先进行除法并在一个步骤中找到余数。

上面的示例从缓冲区的末尾构建字符串(因此result 最终指向缓冲区的中间某处),但您也可以从头开始构建它,然后将其反转。

如果您可以确定原始数字中使用的位数(大约每 3 位增加 1 位 - 略少),您可以估计输出所需的大小。

【讨论】:

  • 这似乎是答案。在我将此标记为答案之前,我将等待几天以查看其他人的回复。我现在正在编码。根据我的发现,十进制位数似乎是每 16 位 5 位,这似乎有效。我通过 2^208 检查了转换,它仍然有效。我可能会编辑原始帖子以添加那个小花絮。
  • 次要:可以使用sizeof buf 而不是sizeof (buf) 2) 而不是/3,可以使用*28/93*87/289 来近似log10(2)。所以char buf[sizeof x * CHAR_BIT) *28/93 + 2]; // right size buffer up to 92 bit
【解决方案2】:

这是正确的方法吗?

第二种方法不适用于 C 中的所有整数值。if divisor &lt; dividend 依赖于将 divisor 创建为大于(或等于)dividend 的 10 的幂。由于大多数整数系统都有一个有限范围,因此不可能在 dividend == INTEGER_MAX 时创建大于(或等于)dividend 的 10 次幂。 (除非INTEGER_MAX 是 10 的幂)。


递归方法的工作原理是重复除以 10 并推迟数字分配,直到确定更高的有效数字。当目标缓冲区的大小未知但足够时,这种方法很有效。

下面的句柄签名int 也适用于INT_MIN,没有未定义的行为。

// Return location of next char to write
// Note: value is expected to be <= 0
static char *itoa_helper(char *s, int value) {
  if (value/10) {
    s = itoa_helper(s, value/10);
  }
  *s = '0' - value % 10;  // C99
  return s+1;
}

void itoa(int n, char *s) {
  if (n < 0) {
    *s++ = '-';
  } else {
    n = -n;
  }
  *itoa_helper(s, n) = '\0';
}

#define INT_SIZEMAX  ((CHAR_BIT*sizeof(int) - 1)*28/93 + 3)
char buf[INT_SIZEMAX];
itoa(INT_MIN, buf);

此代码不是将负数转换为正数,而是相反,因为-INT_MIN 在大多数系统上都失败了。

【讨论】:

  • 我不喜欢递归方法。我已经实现了一个不使用递归的。
  • 为工作使用正确的工具。在递归是最好的方法的情况下,对它的偏见会导致使用斧头作为锯子或不熟悉正确使用工具。
  • 递归是否适合这里的工作?我不这么认为。这可以在一个简单的循环中完成。
  • 当然这可以循环完成。然而,循环需要先了解字符串的最大长度(其他答案)或后续循环才能重新复制(发布的算法)。正如这个答案中提到的:“当目标缓冲区的大小未知但足够时,效果很好。”根据对字符串的内存管理、堆栈空间、代码空间(帖子中没有说明)的需要,递归可能是最好的答案。
  • 很容易确定字符串的长度。只需计算位数(这很容易)并将其与 log10(2) 相乘。再分配几个数字,你就安全了。这就是我在自己的 BigInteger(在 Delphi 中)的简单版本 toString 中的操作方式。不太简单的方法使用分治算法,但缓冲区大小的计算是相同的。对于几百万位数的 BigInteger,它甚至是准确的。
【解决方案3】:

接受的答案已经为您提供了一种简单的方法来做到这一点。这很好用,给你一个很好的结果。但是,如果您确实需要将较大的值转换为字符串,则有更好的方法。

我不会详细介绍,因为我的解决方案是用 Delphi 编写的,很多读者不容易阅读,而且很长(100 多行代码中的几个函数,使用其他函数等)不能用简单的答案来解释,特别是因为转换以不同的方式处理一些不同的数字基数)。

但原理是将数字分成大小几乎相等的两半,乘以 10 的幂。为了转换它们,递归地将它们再次分成两个更小的部分,乘以 10 的幂等。直到零件的大小达到某种下限(例如,32 位),然后您最终将其转换为传统方式,即像接受的答案一样。

然后将部分转换“连接”(实际上,数字直接放入正确地址的单个缓冲区中),因此最后,您会得到一大串数字。

这有点棘手,我只为那些想要调查非常大的数字的人提及它。对于少于 100 位的数字,这没有意义。

这确实是一种递归方法,但不是简单地除以 10。

缓冲区的大小可以预先计算,通过执行类似的操作

bufSize = myBigInt.bitCount() * Math.log10(2) + some_extra_to_be_sure;

我为不同的基数使用了一个预先计算好的表格,但这是一个实现细节。

对于非常大的数字,这将比重复除以 10 的循环快很多,尤其是因为这样,整个数字必须全部除以 10时间,它只会非常缓慢地变小。分而治之的算法只划分更小的数字,并且切割部分的(昂贵的)划分的总数要低得多(我猜是 log N 而不是 N)。因此(平均而言)更小的数字上的划分更少。

参见。 Brent, Zimmermann,“现代计算机算术”,算法 1.26

我的代码和解释可以在这里找到,如果你想看看它是如何工作的:BigIntegers unit

【讨论】:

  • logN 是递归树的深度,划分的数量仍然是 O(N),但在这种方法中常数可能会小得多。
  • 我确实知道,对于非常大的数字,它要快得多,因为大大减少了昂贵除法的数量(大数字) .朴素算法不断将一个非常缓慢地减少的数字除以 10。因此,除法的总数可能仍然是 O(n),但其中大多数除以小得多的数字。
【解决方案4】:

我遇到了类似的问题,但没有找到任何我喜欢的解决方案,所以想出了我的 owm。我们的想法是使用任何基础将您的BigInt 转换为具有10 功率基础的另一个BigInt,尽可能大但仍然小于您当前的基础。您可以使用系统调用通过“数字”进行转换,并将结果连接起来。所以没有涉及到明确的划分,只隐藏在系统库函数中。总体复杂度仍然是二次方的(就像其他基于除法的解决方案一样)。

friend std::ostream& operator<<(std::ostream& out, const BigInt_impl& x){
    using Big10 = BigInt_impl<char32_t, uint64_t, 1000000000>; // 1e9 is the max power of 10 smaller then BASE
    auto big10 = Big10(0);
    auto cm = Big10(1);
    for(size_t i = 0; i < x.digits.size(); ++i, cm *= BASE){
        big10 += cm*x.digits[i];
    }
    out << big10.digits.back();
    for(auto it = next(big10.digits.rbegin()); it != big10.digits.rend(); ++it){ 
        out << std::setfill('0') << std::setw(9) << *it;
    }
    return out;
}

注意这个解决方案中的魔法常数 1e9 - 这只是我的 BASE = 2^32 的情况。懒得做好。

(抱歉,对于 C++,我刚刚意识到 qustion 是关于 C 的,但仍然想将代码留在这里,也许是为了说明想法)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-11-29
    • 2014-12-15
    • 2013-12-07
    相关资源
    最近更新 更多