【问题标题】:BigInteger: count the number of decimal digits in a scalable methodBigInteger:以可扩展的方法计算小数位数
【发布时间】:2013-09-16 12:42:14
【问题描述】:

我需要计算BigInteger 的小数位数。例如:

  • 99 返回2
  • 1234 返回4
  • 9999 返回4
  • 12345678901234567890 返回20

我需要为带有184948 十进制数字等的BigInteger 执行此操作。 我怎样才能快速且可扩展地做到这一点?

convert-to-String 方法很慢:

public String getWritableNumber(BigInteger number) {
   // Takes over 30 seconds for 184948 decimal digits
   return "10^" + (number.toString().length() - 1);
}

这种 loop-devide-by-10 方法更慢:

public String getWritableNumber(BigInteger number) {
    int digitSize = 0;
    while (!number.equals(BigInteger.ZERO)) {
        number = number.divide(BigInteger.TEN);
        digitSize++;
    }
    return "10^" + (digitSize - 1);
}

有没有更快的方法?

【问题讨论】:

  • 它有多慢,您需要多快?
  • @Kayaman 对于 184948 个十进制数字,2 中更快的一个需要 30 秒以上。我需要它少于 2 秒。
  • 2 秒?这听起来很像编程比赛的时间限制。
  • 有一些很好的小技巧可以做到这一点 - 尝试 here 开始。可能不适用于 BigInteger。
  • Guava,这是单行BigIntegerMath.log10(x, RoundingMode.FLOOR) + 1。 Guava 使用了这里讨论的几个技巧。

标签: java biginteger


【解决方案1】:

这里有一个基于Dariusz's answer的快速方法:

public static int getDigitCount(BigInteger number) {
  double factor = Math.log(2) / Math.log(10);
  int digitCount = (int) (factor * number.bitLength() + 1);
  if (BigInteger.TEN.pow(digitCount - 1).compareTo(number) > 0) {
    return digitCount - 1;
  }
  return digitCount;
}

以下代码测试数字 1、9、10、99、100、999、1000 等一直到万位数:

public static void test() {
  for (int i = 0; i < 10000; i++) {
    BigInteger n = BigInteger.TEN.pow(i);
    if (getDigitCount(n.subtract(BigInteger.ONE)) != i || getDigitCount(n) != i + 1) {
      System.out.println("Failure: " + i);
    }
  }
  System.out.println("Done");
}

这可以在一秒钟内检查 BigInteger184,948 十进制数字等等。

【讨论】:

  • 只会添加一些小的更改,因为对于零,您将得到 0 而不是 1,对于负值,位数很少(因此您需要先否定数字)。但除此之外做得很好(你和 Dariusz)(给了你们两个 +1)
【解决方案2】:

看起来它正在工作。我还没有运行详尽的测试,也没有运行任何时间测试,但它似乎有一个合理的运行时间。

public class Test {
  /**
   * Optimised for huge numbers.
   *
   * http://en.wikipedia.org/wiki/Logarithm#Change_of_base
   *
   * States that log[b](x) = log[k](x)/log[k](b)
   *
   * We can get log[2](x) as the bitCount of the number so what we need is
   * essentially bitCount/log[2](10). Sadly that will lead to inaccuracies so
   * here I will attempt an iterative process that should achieve accuracy.
   *
   * log[2](10) = 3.32192809488736234787 so if I divide by 10^(bitCount/4) we
   * should not go too far. In fact repeating that process while adding (bitCount/4)
   * to the running count of the digits will end up with an accurate figure
   * given some twiddling at the end.
   * 
   * So here's the scheme:
   * 
   * While there are more than 4 bits in the number
   *   Divide by 10^(bits/4)
   *   Increase digit count by (bits/4)
   * 
   * Fiddle around to accommodate the remaining digit - if there is one.
   * 
   * Essentially - each time around the loop we remove a number of decimal 
   * digits (by dividing by 10^n) keeping a count of how many we've removed.
   * 
   * The number of digits we remove is estimated from the number of bits in the 
   * number (i.e. log[2](x) / 4). The perfect figure for the reduction would be
   * log[2](x) / 3.3219... so dividing by 4 is a good under-estimate. We 
   * don't go too far but it does mean we have to repeat it just a few times.
   */
  private int log10(BigInteger huge) {
    int digits = 0;
    int bits = huge.bitLength();
    // Serious reductions.
    while (bits > 4) {
      // 4 > log[2](10) so we should not reduce it too far.
      int reduce = bits / 4;
      // Divide by 10^reduce
      huge = huge.divide(BigInteger.TEN.pow(reduce));
      // Removed that many decimal digits.
      digits += reduce;
      // Recalculate bitLength
      bits = huge.bitLength();
    }
    // Now 4 bits or less - add 1 if necessary.
    if ( huge.intValue() > 9 ) {
      digits += 1;
    }
    return digits;
  }

  // Random tests.
  Random rnd = new Random();
  // Limit the bit length.
  int maxBits = BigInteger.TEN.pow(200000).bitLength();

  public void test() {
    // 100 tests.
    for (int i = 1; i <= 100; i++) {
      BigInteger huge = new BigInteger((int)(Math.random() * maxBits), rnd);
      // Note start time.
      long start = System.currentTimeMillis();
      // Do my method.
      int myLength = log10(huge);
      // Record my result.
      System.out.println("Digits: " + myLength+ " Took: " + (System.currentTimeMillis() - start));
      // Check the result.
      int trueLength = huge.toString().length() - 1;
      if (trueLength != myLength) {
        System.out.println("WRONG!! " + (myLength - trueLength));
      }
    }
  }

  public static void main(String args[]) {
    new Test().test();
  }

}

在我的 Celeron M 笔记本电脑上花了大约 3 秒,所以在一些不错的套件上应该会达到不到 2 秒。

【讨论】:

  • 在我的 64 位 i7 上大约需要 0.35 秒才能获得 10^180000 数量级的数字
  • 我在测试时发现代码中有一个错误 - 我使用的是 bitCount 而不是 bitLength - 如果你拿了一个,请刷新你的副本。现在,您需要大约 0.22 秒来处理大量数字。
  • 如果你想要一个更快的算法,看看 BigDecimal 使用什么来计算尾数的长度 :-)
【解决方案3】:

我认为您可以使用bitLength() 来获取 log2 值,然后使用change the base to 10

但是,结果可能有误,有一位数,所以这只是一个近似值。

但是,如果这是可以接受的,您总是可以将结果加 1 并将其限制为 至多。或者,减 1,得到至少

【讨论】:

  • 可以将它与testBit 结合起来循环使用以获得准确的答案,尽管在某些情况下会很慢。
  • @Dukeling 如果不计算原始数字,我想不出该怎么做。您可以测试最后几位,但无论如何您都会处理概率。
  • 好吧,如果你有 16 位,则范围是 65536(5 位)- 131071(6 位)。如果您要检查前 3 位并得到 111,这将保证 6 位数字,类似地 10 保证 5 位数字。所以你真的可以一直从左边检查,直到进入包含固定位数的某个范围。
  • 执行 BigInteger.pow() 相对较快(对于 184948 十进制数字 IIRC 不到 1 秒)。所以也许10.pow(result)可以用来解决off by 1 digit问题
【解决方案4】:

这是另一种比转换为字符串方法更快的方法。不是最好的运行时间,但在 0.65 秒与使用 Convert-to-String 方法的 2.46 秒(180000 位)仍然合理。

此方法根据给定值计算以 10 为底的对数的整数部分。但是,它没有使用循环除法,而是使用了类似于乘方求幂的技术。

这是实现前面提到的运行时的粗略实现:

public static BigInteger log(BigInteger base,BigInteger num)
{
    /* The technique tries to get the products among the squares of base
     * close to the actual value as much as possible without exceeding it.
     * */
    BigInteger resultSet = BigInteger.ZERO;
    BigInteger actMult = BigInteger.ONE;
    BigInteger lastMult = BigInteger.ONE;
    BigInteger actor = base;
    BigInteger incrementor = BigInteger.ONE;
    while(actMult.multiply(base).compareTo(num)<1)
    {
        int count = 0;
        while(actMult.multiply(actor).compareTo(num)<1)
        {
            lastMult = actor; //Keep the old squares
            actor = actor.multiply(actor); //Square the base repeatedly until the value exceeds 
            if(count>0) incrementor = incrementor.multiply(BigInteger.valueOf(2));
            //Update the current exponent of the base
            count++;
        }
        if(count == 0) break;

        /* If there is no way to multiply the "actMult"
         * with squares of the base (including the base itself)
         * without keeping it below the actual value, 
         * it is the end of the computation 
         */
        actMult = actMult.multiply(lastMult);
        resultSet = resultSet.add(incrementor);
        /* Update the product and the exponent
         * */
        actor = base;
        incrementor = BigInteger.ONE;
        //Reset the values for another iteration
    }
    return resultSet;
}
public static int digits(BigInteger num)
{
    if(num.equals(BigInteger.ZERO)) return 1;
    if(num.compareTo(BigInteger.ZERO)<0) num = num.multiply(BigInteger.valueOf(-1));
    return log(BigInteger.valueOf(10),num).intValue()+1;
}

希望这会有所帮助。

【讨论】:

    【解决方案5】:

    您可以先将BigInteger 转换为BigDecimal,然后使用此answer 来计算位数。这似乎比使用BigInteger.toString() 更有效,因为它会为String 表示分配内存。

        private static int numberOfDigits(BigInteger value) {
            return significantDigits(new BigDecimal(value));
        }
    
        private static int significantDigits(BigDecimal value) {
            return value.scale() < 0
                   ? value.precision() - value.scale()
                   : value.precision();
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-09
      相关资源
      最近更新 更多