【问题标题】:Square root of BigDecimal in JavaJava中BigDecimal的平方根
【发布时间】:2012-11-30 17:04:09
【问题描述】:

我们能否仅使用 Java API 而不是定制的 100 行算法来计算 Java 中 BigDecimal 的平方根?

【问题讨论】:

  • 不用自己写算法?达到理想的精度?没有。
  • 定制50行算法,包括cmets怎么样?牛顿法没那么复杂。
  • 从 Java 9 开始可以!见BigDecimal.sqrt()。 @dimo414 有这个问题的正确答案。

标签: java bigdecimal square-root


【解决方案1】:

我用过这个,效果很好。 Here's an example of how the algorithm works at a high level.

编辑:我很想知道下面定义的准确度。这是来自official source 的 sqrt(2):

(first 200 digits) 1.41421356237309504880168872420969807856967187537694807317667973799073247846210703885038753432764157273501384623091229702492483605585073721264412149709993583141322266592750559275579995050115278206057147

这里使用的是我在下面概述的方法,SQRT_DIG 等于 150:

(first 200 digits) 1.41421356237309504880168872420969807856967187537694807317667973799073247846210703885038753432764157273501384623091229702492483605585073721264412149709993583141322266592750559275579995050115278206086685

第一个偏差发生在 195 位精度之后。如果您需要如此高的精度,请自行承担风险。

SQRT_DIG 更改为 1000 会产生 1570 位精度

private static final BigDecimal SQRT_DIG = new BigDecimal(150);
private static final BigDecimal SQRT_PRE = new BigDecimal(10).pow(SQRT_DIG.intValue());

/**
 * Private utility method used to compute the square root of a BigDecimal.
 * 
 * @author Luciano Culacciatti 
 * @url http://www.codeproject.com/Tips/257031/Implementing-SqrtRoot-in-BigDecimal
 */
private static BigDecimal sqrtNewtonRaphson  (BigDecimal c, BigDecimal xn, BigDecimal precision){
    BigDecimal fx = xn.pow(2).add(c.negate());
    BigDecimal fpx = xn.multiply(new BigDecimal(2));
    BigDecimal xn1 = fx.divide(fpx,2*SQRT_DIG.intValue(),RoundingMode.HALF_DOWN);
    xn1 = xn.add(xn1.negate());
    BigDecimal currentSquare = xn1.pow(2);
    BigDecimal currentPrecision = currentSquare.subtract(c);
    currentPrecision = currentPrecision.abs();
    if (currentPrecision.compareTo(precision) <= -1){
        return xn1;
    }
    return sqrtNewtonRaphson(c, xn1, precision);
}

/**
 * Uses Newton Raphson to compute the square root of a BigDecimal.
 * 
 * @author Luciano Culacciatti 
 * @url http://www.codeproject.com/Tips/257031/Implementing-SqrtRoot-in-BigDecimal
 */
public static BigDecimal bigSqrt(BigDecimal c){
    return sqrtNewtonRaphson(c,new BigDecimal(1),new BigDecimal(1).divide(SQRT_PRE));
}

请务必查看 barwnikk 的回答。它更简洁,并且似乎提供了相同或更好的精度。

【讨论】:

  • 请注意,对于较大的 BigDecimal,此递归解决方案将耗尽堆栈。
  • 试试 BigDecimal.valueOf(123.123123123*123.123123123) 它给出了一个有趣的结果
  • 当然可以... 123.1231231229999988908297926818176540649... 但是,如果您执行乘法运算,您会得到15159.303447561417273129 ..... 使用String 构造函数BigDecimal 确实会产生正确的结果回答。 bigSqrt(new BigDecimal("15159.303447561417273129")) = 123.1231231230000000000000000000000000... 我想这是DoubleBigDecimal 便捷构造方法的问题,而不是 sqrt 算法。
  • @snd 如果您点击 javadoc 中的链接,您会发现这不是我的作品,我保留了原作者的作品。还有一个维基百科链接显示了为什么这种方法在数学上是合理的。
【解决方案2】:
public static BigDecimal sqrt(BigDecimal A, final int SCALE) {
    BigDecimal x0 = new BigDecimal("0");
    BigDecimal x1 = new BigDecimal(Math.sqrt(A.doubleValue()));
    while (!x0.equals(x1)) {
        x0 = x1;
        x1 = A.divide(x0, SCALE, ROUND_HALF_UP);
        x1 = x1.add(x0);
        x1 = x1.divide(TWO, SCALE, ROUND_HALF_UP);

    }
    return x1;
}

这项工作完美!超过 65536 位的速度非常快!

【讨论】:

  • +1 用于使用良好的初始估计。 !x0.equals(x1) 虽然有点危险。你有证据证明循环在所有情况下都会终止吗?
  • 请注意,对于较大的 BigDecimal,A.doubleValue() = NaN。使用(比如说) A.divide(TWO, RoundingMode.FLOOR) 可以让它使用更大的值。
  • 什么是二,它不编译?它只是第二个 BigDecimal.valueOf(2) 吗?
  • 使用BigDecimal.ZERO 而不是重新创建轮子。此外,doubleValue() 调用通过丢弃任意精度来破坏本练习的全部目的。
  • @EntangledLoops 该算法是Babylonian method 的实现。无论初始估计如何,它将在 SCALE 给出的精度级别上收敛到相同的精确结果。该估计仅用于性能提升; Math.sqrt() 是一个合理的开始:它对于较大的值会失败(使 MZB 的替代方案成为一个很好的通用选择)但是当它不返回 NaN 时 not 会影响精度。您可能会欺骗自己超出范围,但肯定不会精确。
【解决方案3】:

从 Java 9 开始,您可以!见BigDecimal.sqrt()

【讨论】:

    【解决方案4】:

    通过使用卡普的技巧,这可以在没有循环的情况下仅用两行实现,精度为 32 位:

    public static BigDecimal sqrt(BigDecimal value) {
        BigDecimal x = new BigDecimal(Math.sqrt(value.doubleValue()));
        return x.add(new BigDecimal(value.subtract(x.multiply(x)).doubleValue() / (x.doubleValue() * 2.0)));
    }
    

    【讨论】:

    • 你有卡普的诡计的来源吗?我找不到它。
    • 不幸的是,精度只有 32 位... BigDecimal 用于大数字。
    • 这会丢弃BigDecimal的任意精度,这是整个点。
    • 有人能解释一下为什么这个解决方案适用于 32 位精度吗?最多 32 位还是正好 32 位?
    【解决方案5】:

    如果您只需要找到integer square roots - 这是可以使用的两种方法。

    Newton's method - 即使对于 1000 位 BigInteger 也非常快:

    public static BigInteger sqrtN(BigInteger in) {
        final BigInteger TWO = BigInteger.valueOf(2);
        int c;
    
        // Significantly speed-up algorithm by proper select of initial approximation
        // As square root has 2 times less digits as original value
        // we can start with 2^(length of N1 / 2)
        BigInteger n0 = TWO.pow(in.bitLength() / 2);
        // Value of approximate value on previous step
        BigInteger np = in;
    
        do {
            // next approximation step: n0 = (n0 + in/n0) / 2
            n0 = n0.add(in.divide(n0)).divide(TWO);
    
            // compare current approximation with previous step
            c = np.compareTo(n0);
    
            // save value as previous approximation
            np = n0;
    
            // finish when previous step is equal to current
        }  while (c != 0);
    
        return n0;
    }
    

    Bisection method - 比牛顿慢 50 倍 - 仅用于教育目的:

     public static BigInteger sqrtD(final BigInteger in) {
        final BigInteger TWO = BigInteger.valueOf(2);
        BigInteger n0, n1, m, m2, l;
        int c;
    
        // Init segment
        n0 = BigInteger.ZERO;
        n1 = in;
    
        do {
            // length of segment
            l = n1.subtract(n0);
    
            // middle of segment
            m = l.divide(TWO).add(n0);
    
            // compare m^2 with in
            c = m.pow(2).compareTo(in);
    
            if (c == 0) {
                // exact value is found
                break;
            }  else if (c > 0) {
                // m^2 is bigger than in - choose left half of segment
                n1 = m;
            } else {
                // m^2 is smaller than in - choose right half of segment
                n0 = m;
            }
    
            // finish if length of segment is 1, i.e. approximate value is found
        }  while (l.compareTo(BigInteger.ONE) > 0);
    
        return m;
    }
    

    【讨论】:

      【解决方案6】:

      如果您想计算位数多于双精度数(大比例的 BigDecimal)的数字的平方根:

      维基百科有一篇计算平方根的文章:http://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method

      这是我的实现:

      public static BigDecimal sqrt(BigDecimal in, int scale){
          BigDecimal sqrt = new BigDecimal(1);
          sqrt.setScale(scale + 3, RoundingMode.FLOOR);
          BigDecimal store = new BigDecimal(in.toString());
          boolean first = true;
          do{
              if (!first){
                  store = new BigDecimal(sqrt.toString());
              }
              else first = false;
              store.setScale(scale + 3, RoundingMode.FLOOR);
              sqrt = in.divide(store, scale + 3, RoundingMode.FLOOR).add(store).divide(
                      BigDecimal.valueOf(2), scale + 3, RoundingMode.FLOOR);
          }while (!store.equals(sqrt));
          return sqrt.setScale(scale, RoundingMode.FLOOR);
      }
      

      setScale(scale + 3, RoundingMode.Floor) 因为过度计算提供了更高的准确性。 RoundingMode.Floor 截断数字,RoundingMode.HALF_UP 进行正常舍入。

      【讨论】:

        【解决方案7】:

        正如之前所说:如果您不介意答案的精度,但只想在第 15 个仍然有效的数字之后生成随机数字,那么您为什么要使用 BigDecimal?

        这是应该使用浮点 BigDecimals 实现技巧的方法的代码:

            import java.math.BigDecimal;
            import java.math.BigInteger;
            import java.math.MathContext;
        
        
        
        public BigDecimal bigSqrt(BigDecimal d, MathContext mc) {
            // 1. Make sure argument is non-negative and treat Argument 0
            int sign = d.signum();
            if(sign == -1)
              throw new ArithmeticException("Invalid (negative) argument of sqrt: "+d);
            else if(sign == 0)
              return BigDecimal.ZERO;
            // 2. Scaling:
            // factorize d = scaledD * scaleFactorD 
            //             = scaledD * (sqrtApproxD * sqrtApproxD)
            // such that scalefactorD is easy to take the square root
            // you use scale and bitlength for this, and if odd add or subtract a one
            BigInteger bigI=d.unscaledValue();
            int bigS=d.scale();
            int bigL = bigI.bitLength();
            BigInteger scaleFactorI;
            BigInteger sqrtApproxI;
            if ((bigL%2==0)){
               scaleFactorI=BigInteger.ONE.shiftLeft(bigL);
               sqrtApproxI=BigInteger.ONE.shiftLeft(bigL/2);           
            }else{
               scaleFactorI=BigInteger.ONE.shiftLeft(bigL-1);
               sqrtApproxI=BigInteger.ONE.shiftLeft((bigL-1)/2 );          
            }
            BigDecimal scaleFactorD;
            BigDecimal sqrtApproxD;
            if ((bigS%2==0)){
                scaleFactorD=new BigDecimal(scaleFactorI,bigS);
                sqrtApproxD=new BigDecimal(sqrtApproxI,bigS/2);
            }else{
                scaleFactorD=new BigDecimal(scaleFactorI,bigS+1);
                sqrtApproxD=new BigDecimal(sqrtApproxI,(bigS+1)/2);         
            }
            BigDecimal scaledD=d.divide(scaleFactorD);
        
            // 3. This is the core algorithm:
            //    Newton-Ralpson for scaledD : In case of f(x)=sqrt(x),
            //    Heron's Method or Babylonian Method are other names for the same thing.
            //    Since this is scaled we can be sure that scaledD.doubleValue() works 
            //    for the start value of the iteration without overflow or underflow
            System.out.println("ScaledD="+scaledD);
            double dbl = scaledD.doubleValue();
            double sqrtDbl = Math.sqrt(dbl);
            BigDecimal a = new BigDecimal(sqrtDbl, mc);
        
            BigDecimal HALF=BigDecimal.ONE.divide(BigDecimal.ONE.add(BigDecimal.ONE));
            BigDecimal h = new BigDecimal("0", mc);
            // when to stop iterating? You start with ~15 digits of precision, and Newton-Ralphson is quadratic
            // in approximation speed, so in roundabout doubles the number of valid digits with each step.
            // This fmay be safer than testing a BigDecifmal against zero.
            int prec = mc.getPrecision();
            int start = 15;
            do {
                h = scaledD.divide(a, mc);
                a = a.add(h).multiply(HALF);
                start *= 2;
            } while (start <= prec);        
            // 3. Return rescaled answer. sqrt(d)= sqrt(scaledD)*sqrtApproxD :          
            return (a.multiply(sqrtApproxD));
        }
        

        作为一项测试,尝试将一个数字重复平方几次,而不是取重复的平方根,然后看看你离开始的地方有多近。

        【讨论】:

          【解决方案8】:

          这是一个非常准确和快速的解决方案,它基于我的BigIntSqRoot solution 和下一个观察结果:A^2B 的平方根 - 是 A 乘以 B 的根。使用这种方法,我可以轻松计算出 2 的平方根的前 1000 位。

          1.4142135623730950488016887242096980785696718753769480731766797379907324784621070388503875343276415727350138462309122970249248360558507372126441214970999358314132226659275055927557999505011527820605714701095599716059702745345968620147285174186408891986095523292304843087143214508397626036279952514079896872533965463318088296406206152583523950547457502877599617298355752203375318570113543746034084988471603868999706990048150305440277903164542478230684929369186215805784631115966687130130156185689872372352885092648612494977154218334204285686060146824720771435854874155657069677653720226485447015858801620758474922657226002085584466521458398893944370926591800311388246468157082630100594858704003186480342194897278290641045072636881313739855256117322040245091227700226941127573627280495738108967504018369868368450725799364729060762996941380475654823728997180326802474420629269124859052181004459842150591120249441341728531478105803603371077309182869314710171111683916581726889419758716582152128229518488472
          

          这里是源代码

          public class BigIntSqRoot {
              private static final int PRECISION = 10000;
              private static BigInteger multiplier = BigInteger.valueOf(10).pow(PRECISION * 2);
              private static BigDecimal root = BigDecimal.valueOf(10).pow(PRECISION);
              private static BigInteger two = BigInteger.valueOf(2L);
          
              public static BigDecimal bigDecimalSqRootFloor(BigInteger x)
                      throws IllegalArgumentException {
                  BigInteger result = bigIntSqRootFloor(x.multiply(multiplier));
                  //noinspection BigDecimalMethodWithoutRoundingCalled
                  return new BigDecimal(result).divide(root);
              }
          
              public static BigInteger bigIntSqRootFloor(BigInteger x)
                      throws IllegalArgumentException {
                  if (checkTrivial(x)) {
                      return x;
                  }
                  if (x.bitLength() < 64) { // Can be cast to long
                      double sqrt = Math.sqrt(x.longValue());
                      return BigInteger.valueOf(Math.round(sqrt));
                  }
                  // starting with y = x / 2 avoids magnitude issues with x squared
                  BigInteger y = x.divide(two);
                  BigInteger value = x.divide(y);
                  while (y.compareTo(value) > 0) {
                      y = value.add(y).divide(two);
                      value = x.divide(y);
                  }
                  return y;
              }
          
              public static BigInteger bigIntSqRootCeil(BigInteger x)
                      throws IllegalArgumentException {
                  BigInteger y = bigIntSqRootFloor(x);
                  if (x.compareTo(y.multiply(y)) == 0) {
                      return y;
                  }
                  return y.add(BigInteger.ONE);
              }
          
              private static boolean checkTrivial(BigInteger x) {
                  if (x == null) {
                      throw new NullPointerException("x can't be null");
                  }
                  if (x.compareTo(BigInteger.ZERO) < 0) {
                      throw new IllegalArgumentException("Negative argument.");
                  }
          
                  return x.equals(BigInteger.ZERO) || x.equals(BigInteger.ONE);
              }
          }
          

          【讨论】:

          • Math.sqrt(x.longValue()) 注意Math.sqrt()(从 Java 9 开始)采用 double 参数:适用于大约 52 位。
          【解决方案9】:

          我想出了一个算法,它不仅取平方根,而且取每个 BigDecimal 的整数以下的每个根。 它的一大优势是它不执行搜索算法,因此运行时间为 0.1 毫秒 - 1 毫秒,速度非常快。

          但是你得到的速度和多功能性,它缺乏准确性,它平均 5 个正确的数字,第五个数字的偏差为 3。 (用一百万个随机数和根进行测试),虽然测试运行的根非常高,所以如果您将根保持在 10 以下,您可以期待更高的准确度。

          结果只有 64 位精度,其余数字为零,因此如果您需要非常高的精度,请不要使用此函数。

          它是用来处理非常大的数字和非常大的根,而不是非常小的数字。

          public static BigDecimal nrt(BigDecimal bd,int root) {
          //if number is smaller then double_max_value it's faster to use the usual math 
          //library
              if(bd.compareTo(BigDecimal.valueOf(Double.MAX_VALUE)) < 0) 
                  return new BigDecimal( Math.pow(bd.doubleValue(), 1D / (double)root ));
          
              BigDecimal in = bd;
              int digits = bd.precision() - bd.scale() -1; //take digits to get the numbers power of ten
              in = in.scaleByPowerOfTen (- (digits - digits%root) ); //scale down to the lowest number with it's power of ten mod root is the same as initial number
          
              if(in.compareTo(BigDecimal.valueOf( Double.MAX_VALUE) ) > 0) { //if down scaled value is bigger then double_max_value, we find the answer by splitting the roots into factors and calculate them seperately and find the final result by multiplying the subresults
                  int highestDenominator = highestDenominator(root);
                  if(highestDenominator != 1) {
                      return nrt( nrt(bd, root / highestDenominator),highestDenominator); // for example turns 1^(1/25) 1^(1/5)^1(1/5)
                  }
                  //hitting this point makes the runtime about 5-10 times higher,
                  //but the alternative is crashing
                  else return nrt(bd,root+1) //+1 to make the root even so it can be broken further down into factors
                              .add(nrt(bd,root-1),MathContext.DECIMAL128) //add the -1 root and take the average to deal with the inaccuracy created by this
                              .divide(BigDecimal.valueOf(2),MathContext.DECIMAL128); 
              } 
              double downScaledResult = Math.pow(in.doubleValue(), 1D /root); //do the calculation on the downscaled value
              BigDecimal BDResult =new BigDecimal(downScaledResult) // scale back up by the downscaled value divided by root
                      .scaleByPowerOfTen( (digits - digits % root) / root );
              return BDResult;
          }
          private static int highestDenominator(int n) {
              for(int i = n-1; i>1;i--) {
                  if(n % i == 0) {
                      return i;
                  }
              }
              return 1;
          }
          

          它的工作原理是使用一个数学属性,基本上说,当你做平方根时,你可以将 x^0.5 更改为 (x/100)^0,5 * 10,因此将底数除以 100 取幂并乘以 10 .

          一般化后变为 x^(1/n) = (x / 10^n) ^ (1/n) * 10。

          因此,对于立方根,您需要将底除以 10^3,对于四根,您需要除以 10^4,依此类推。

          该算法使用该函数将输入缩小到数学库可以处理的范围,然后根据输入的缩小程度再次将其重新放大。

          它还处理了一些输入无法足够缩小的边缘情况,正是这些边缘情况增加了很多准确性问题。

          【讨论】:

            【解决方案10】:

            java api 中没有任何内容,所以如果 double 不够准确(如果没有,为什么要使用 BigDecimal?)那么你需要类似下面的代码。)

            import java.math.BigDecimal;
            
            public class BigDSqrt {
              public static BigDecimal sqrt(BigDecimal n, int s) {
                BigDecimal TWO = BigDecimal.valueOf(2);
            
                // Obtain the first approximation
                BigDecimal x = n
                    .divide(BigDecimal.valueOf(3), s, BigDecimal.ROUND_DOWN);
                BigDecimal lastX = BigDecimal.valueOf(0);
            
                // Proceed through 50 iterations
                for (int i = 0; i < 50; i++) {
                  x = n.add(x.multiply(x)).divide(x.multiply(TWO), s,
                      BigDecimal.ROUND_DOWN);
                  if (x.compareTo(lastX) == 0)
                    break;
                  lastX = x;
                }
                return x;
              }
            }
            

            来源:http://www.java2s.com/Code/Java/Language-Basics/DemonstrationofhighprecisionarithmeticwiththeBigDoubleclass.htm

            【讨论】:

              【解决方案11】:
              public static BigDecimal sqrt( final BigDecimal value )
              {
                  BigDecimal guess = value.multiply( DECIMAL_HALF ); 
                  BigDecimal previousGuess;
              
                  do
                  {
                      previousGuess = guess;
                      guess = sqrtGuess( guess, value );
                 } while ( guess.subtract( previousGuess ).abs().compareTo( EPSILON ) == 1 );
              
                  return guess;
              }
              
              private static BigDecimal sqrtGuess( final BigDecimal guess,
                                                   final BigDecimal value )
              {
                  return guess.subtract( guess.multiply( guess ).subtract( value ).divide( DECIMAL_TWO.multiply( guess ), SCALE, RoundingMode.HALF_UP ) );
              }
              
              private static BigDecimal epsilon()
              {
                  final StringBuilder builder = new StringBuilder( "0." );
              
                  for ( int i = 0; i < SCALE - 1; ++i )
                  {
                      builder.append( "0" );
                  }
              
                  builder.append( "1" );
              
                  return new BigDecimal( builder.toString() );
              }
              
              private static final int SCALE = 1024;
              private static final BigDecimal EPSILON = epsilon();
              public static final BigDecimal DECIMAL_HALF = new BigDecimal( "0.5" );
              public static final BigDecimal DECIMAL_TWO = new BigDecimal( "2" );
              

              【讨论】:

                【解决方案12】:
                BigDecimal.valueOf(Math.sqrt(myBigDecimal.doubleValue()));
                

                【讨论】:

                • 我认为您需要明确指出,只有当对 double 精度的回答是可接受的并且原始 BigDecimal 在允许的范围内时,这才是足够的双倍的。通常使用 BigDecimal 的全部理由是这些条件中的一个或两个都不成立。
                • 嗯,这使用了 doubleValue() 这意味着我可能会失去很多精度,但另一方面我的问题是“如何仅使用 JAVA API”,所以,非常感谢.我将使用它,无论发生什么,都会发生。
                • BigDecimal 的目的就这样被打败了。
                • 使用 BigDecimal 的重点是实现高精度。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2015-12-28
                • 2014-02-26
                • 1970-01-01
                • 2021-04-10
                • 2013-02-17
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多