高精度數學運算

BBFBBF發表於2019-01-19

四捨六入計算

演算法規則:
四捨六入五考慮,
五後非零就進一,
五後皆零看奇偶,
五前為偶應捨去,
五前為奇要進一。

使用BigDecimal,保證精度的同時,能精準的進行四捨六入計算。

優化排列組合演算法

關於排列組合公式,請百度。網上一大堆演算法,都先計算階乘再相除。但實際上應該先約分,一下子就節約了很多計算步驟。以排列公式來說P(n,r)=n!/(n-r)!,實際計算中就是n 乘到 n-r就可以了。組合公式就是排列演算法再除以r的階乘。

MathUtil類

import org.apache.commons.lang3.ArrayUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * 精確的數學運算
 * <p>使用 {@link java.math.BigDecimal}來實現精準度</p>
 * 因為精度的原因<strong>BigDecimal(double val)</strong>構造方法的結果有一定的不可預知性,例如:
 * <pre>
 *   System.out.println(new BigDecimal(0.2)); //0.200000000000000011102230246251565404236316680908203125
 *   System.out.println(BigDecimal.valueOf(0.2f)); //0.20000000298023224
 *   System.out.println(BigDecimal.valueOf(0.2d)); //0.2
 *   System.out.println(BigDecimal.valueOf(0.2)); //0.2
 *   System.out.println(new BigDecimal("0.2")); //0.2
 * </pre>
 * <p>因此建議使用<strong>new BigDecimal(String)</strong>。
 * @author BBF
 */
public final class MathUtil {
  /**
   * PI,比Math.PI多兩位
   */
  public static final double PI = 3.1415926535897932384626;

  /**
   * 預設除法運算精度
   */
  private static final int DEFAULT_SCALE = 10;
  private static final double NUM_ROUND = 0.5;

  /**
   * 運算列舉
   */
  private enum MathType {
    /**
     * 加法
     */
    ADD,
    /**
     * 減法
     */
    SUB,
    /**
     * 乘法
     */
    MULTI,
    /**
     * 除法
     */
    DIV
  }

  private MathUtil() {
  }

  /**
   * 轉換為{@link java.math.BigDecimal}
   * <p>為保證精度,先轉成{@link java.lang.String}然後再用建構函式</p>
   * @param value 數值
   * @return {@link java.math.BigDecimal}
   */
  private static BigDecimal convertToBigDecimal(Number value) {
    return value == null ? BigDecimal.ZERO : new BigDecimal(value.toString());
  }

  /**
   * 提供精確的加法、減法和乘法運算
   * @param type 運演算法則
   * @param scale 精確到小數點後幾位,只在除法有效
   * @param values 多個值
   * @return 四則運算結果
   */
  private static BigDecimal calculate(MathType type, int scale, Number[] values) {
    if (ArrayUtils.isEmpty(values)) {
      return BigDecimal.ZERO;
    }
    // 第一個數作為被加數、被減數或被乘數
    Number value = values[0];
    BigDecimal result = convertToBigDecimal(value);
    for (int i = 1, l = values.length; i < l; i++) {
      value = values[i];
      if (value != null) {
        switch (type) {
          case ADD:
            result = result.add(convertToBigDecimal(value));
            break;
          case SUB:
            result = result.subtract(convertToBigDecimal(value));
            break;
          case MULTI:
            result = result.multiply(convertToBigDecimal(value));
            break;
          case DIV:
            result = result.divide(convertToBigDecimal(value), scale, RoundingMode.HALF_UP);
            break;
          default:
            break;
        }
      }
    }
    return result;
  }

  /**
   * 提供精確的冪運算
   * @param value 底數
   * @param n 指數
   * @return 冪的積
   */
  public static BigDecimal pow(Number value, int n) {
    return convertToBigDecimal(value).pow(n);
  }

  /**
   * 提供精確的加法運算
   * @param values 多個值的字串
   * @return 和
   */
  public static BigDecimal add(Number... values) {
    return calculate(MathType.ADD, DEFAULT_SCALE, values);
  }

  /**
   * 提供精確的減法運算
   * @param values 多個值的字串
   * @return 差
   */
  public static BigDecimal sub(Number... values) {
    return calculate(MathType.SUB, DEFAULT_SCALE, values);
  }

  /**
   * 提供精確的乘法運算
   * @param values 多個值的字串
   * @return 積
   */
  public static BigDecimal multi(Number... values) {
    return calculate(MathType.MULTI, DEFAULT_SCALE, values);
  }

  /**
   * 提供(相對)精確的除法運算
   * <p>當發生除不盡的情況時,精確到小數點以後10位,以後的數字四捨五入</p>
   * @param values 多個值的字串
   * @return 商
   */
  public static BigDecimal div(Number... values) {
    return calculate(MathType.DIV, DEFAULT_SCALE, values);
  }

  /**
   * 提供(相對)精確的除法運算
   * <p>當發生除不盡的情況時,由scale引數指定精度,以後的數字四捨五入</p>
   * @param scale 精確到小數點後幾位,只在除法有效
   * @param values 多個值的字串
   * @return 商
   */
  public static BigDecimal divByScale(int scale, Number... values) {
    if (scale < 0) {
      throw new IllegalArgumentException("The scale must be a positive integer or zero");
    }
    return calculate(MathType.DIV, scale, values);
  }

  /**
   * 四捨六入五成雙演算法
   * <p>四捨六入五成雙是一種比較精確比較科學的計數保留法,是一種數字修約規則。</p>
   * <pre>
   * 演算法規則:
   * 四捨六入五考慮,
   * 五後非零就進一,
   * 五後皆零看奇偶,
   * 五前為偶應捨去,
   * 五前為奇要進一。
   * </pre>
   * @param value 需要科學計算的資料
   * @param digit 保留的小數位
   * @return 指定小數位數的數字
   */
  public static BigDecimal round(Number value, int digit) {
    // 小數進位,然後取整計算,再退位得到結果
    BigDecimal ratio = pow(10, digit);
    // 進位後的數字
    BigDecimal number = multi(value, ratio);
    // 獲取BigDecimal整數部分,直接捨棄小數部分
    long integer = number.setScale(0, RoundingMode.DOWN).longValue();
    // 獲取小數部分
    double decimal = sub(number, integer).doubleValue();
    if (decimal > NUM_ROUND) {
      // 四捨六入
      integer = integer + 1;
    }
    if (decimal == NUM_ROUND && integer % 2 != 0) {
      // 五前為奇要進一
      integer = integer + 1;
    }
    return div(integer, ratio).setScale(digit, RoundingMode.HALF_UP);
  }

  /**
   * 計算階乘
   * <p>n! = n * (n-1) * ... * end</p>
   * @param n 階乘起始
   * @param end 階乘結束
   * @return 結果
   */
  public static BigDecimal factorial(Number n, int end) {
    int st = n.intValue();
    if (st < end) {
      return BigDecimal.ZERO;
    }
    if (st == end) {
      return BigDecimal.ONE;
    }
    return multi(n, factorial(sub(n, 1), end));
  }

  /**
   * 計算階乘
   * <p>n! = n * (n-1) * ... * 2 * 1</p>
   * @param n 階乘起始
   * @return 結果
   */
  public static BigDecimal factorial(Number n) {
    return factorial(n, 1);
  }

  /**
   * 計算排列
   * <p>P(n, r) = n!/(n-r)!</p>
   * <p>從n個不同的元素中,取r個不重複的元素,按次序排列</p>
   * @param n 總數
   * @param r 要取出數量
   * @return 排列數
   */
  public static long arrangement(int n, int r) {
    if (n < r) {
      return 0;
    }
    // 對公式約分,實際上是計算了n 到 n-r的階乘
    return factorial(n, n - r).longValue();
  }

  /**
   * 計算組合
   * <p>C(n, r) = n!/((n-r)! * r!)</p>
   * <p>從n個不同的元素中,取r個不重複的元素,不考慮順序</p>
   * @param n 總數
   * @param r 要取出數量
   * @return 組合數
   */
  public static long combination(int n, int r) {
    if (n < r) {
      return 0;
    }
    // 組合就是排列的結果再除以r的階乘
    return div(arrangement(n, r), factorial(r)).longValue();
  }
}

測試用例

import org.junit.Test;

import java.math.BigDecimal;

/**
 * MathUtil測試類
 * @author BBF
 */
public class MathUtilTest {

  @Test
  public void showBigDecimal() {
    System.out.println(new BigDecimal(0.2)); //0.200000000000000011102230246251565404236316680908203125
    System.out.println(BigDecimal.valueOf(0.2f)); //0.20000000298023224
    System.out.println(BigDecimal.valueOf(0.2d)); //0.2
    System.out.println(BigDecimal.valueOf(0.2)); //0.2
    System.out.println(new BigDecimal("0.2")); //0.2
  }

  @Test
  public void add() {
    BigDecimal bigDecimal = new BigDecimal("1.91");
    double ab = MathUtil.add(8, 0.1, 0.2f, 0.3d, bigDecimal).doubleValue();
    System.out.println("各種型別數值相加,預期:10.51  實際:" + ab);
  }

  @Test
  public void round() {
    System.out.println("四捨六入預期:4.24  實際:" + MathUtil.round(4.245, 2).toString());
    System.out.println("四捨六入預期:4.24  實際:" + MathUtil.round(4.2450, 2).toString());
    System.out.println("四捨六入預期:4.25  實際:" + MathUtil.round(4.2451, 2).toString());
    System.out.println("四捨六入預期:4.22  實際:" + MathUtil.round(4.2250, 2).toString());
    System.out.println("四捨六入預期:1.20  實際:" + MathUtil.round(1.2050, 2).toString());
    System.out.println("四捨六入預期:1.22  實際:" + MathUtil.round(1.2150, 2).toString());
    System.out.println("四捨六入預期:1.22  實際:" + MathUtil.round(1.2250, 2).toString());
    System.out.println("四捨六入預期:1.24  實際:" + MathUtil.round(1.2350, 2).toString());
    System.out.println("四捨六入預期:1.24  實際:" + MathUtil.round(1.2450, 2).toString());
    System.out.println("四捨六入預期:1.26  實際:" + MathUtil.round(1.2550, 2).toString());
    System.out.println("四捨六入預期:1.26  實際:" + MathUtil.round(1.2650, 2).toString());
    System.out.println("四捨六入預期:1.28  實際:" + MathUtil.round(1.2750, 2).toString());
    System.out.println("四捨六入預期:1.28  實際:" + MathUtil.round(1.2850, 2).toString());
    System.out.println("四捨六入預期:1.30  實際:" + MathUtil.round(1.2950, 2).toString());
  }

  @Test
  public void factorial() {
    System.out.println("階乘10!:預期:3628800  實際:" + MathUtil.factorial(10).toString());
  }

  @Test
  public void arrangement() {
    System.out.println("排列P(10,2),預期:90  實際:" + MathUtil.arrangement(10, 2));
  }

  @Test
  public void combination() {
    System.out.println("排列C(10,2),預期:45  實際:" + MathUtil.combination(10, 2));
  }
}

相關文章