この解答例はChatGPTによって生成されています,正しいかは自己判断で。

BFE.devでのAI解答例 - JavaScript コーディング問題
129. BigDecimal 割り算

解法1:バカラ・ラス法

バカラ・ラス法を使うと、長い被除数を繰り下がりながら割ることができます。おおよそ足し算の手法に似ていますが、桁が大きくなるたびにコンピュータリソースの量が指数関数的に増加するため、注意が必要です。

この方法では、被除数を部分的に引いていくことで割り算を行います。具体的には、以下の手順に従って処理を行います。

  1. 結果の初期値をゼロにします。
  2. 左から1桁ずつ、割る数の桁数と同じ桁数を持つ数を取り出します。最初は、被除数の最上位の桁から取得します。
  3. 割る数で、先に取り出した数を割ります。
  4. 得られた整数部を、答えの最下位に加算し、小数部を保持します。
  5. 小数部が十分に精度を持つまで、同じ割り算を繰り返します。小数部が0になったら、処理を終了します。
  6. 被除数の桁数が0になるまで、3〜5を繰り返します。

実行時間は、操作された小数部の桁数と割る数の影響を受けます。一般的な割り算の実行時間はO(n²)であることを考えると、この方法の実行時間は、桁数が大きくなるにつれて指数関数的に増加することがわかります。

// 被除数を割る数で割る関数
function divide(dividend, divisor) {
  // 被除数と割る数を10進数に変換します
  const [dividendInt, dividendDec] = dividend.split(".")
  const [divisorInt, divisorDec] = divisor.split(".")

  // 左から数えた割る数の桁数
  const divisorPrecision = (divisorDec || "").length

  // 狭められた被除数と答え
  let rest = ""
  let quotient = ""

  // 全ての被除数の桁に対してループを行います
  for (let i = 0; i < dividendInt.length; i++) {
    // restに次の被除数の桁を追加して、直前の割り算の余りも含めます
    rest += dividendInt.charAt(i)
    rest = rest.replace(/^0+/, "") // 先頭の0を削除

    // もしなお余りが割る数より小さい場合、0を埋めません
    if (+rest < (+divisorInt || 0)) {
      if (quotient) quotient += "0"
      continue
    }

    // restを割る数で割り、結果と余りを取得します
    let div = Math.floor(+rest / +divisorInt).toString()
    let mod = (+rest % +divisorInt).toString()

    // もしあれば小数桁を追加します
    if (divisorPrecision) {
      // 小数部を有効数字まで繰り上げる (666 -> 667 など)
      while (mod.length < divisorPrecision + 1 && i < dividendInt.length - 1) {
        i++
        rest += dividendInt.charAt(i)
        mod = (+rest % +divisorInt).toString().padStart(divisorPrecision + 1, "0")
      }

      // 小数部を狭めます
      mod = mod.slice(1)
    }

    // 適切な桁数に削除します
    mod = mod.replace(/0+$/, "")
    rest = mod

    // 先頭の0を削除します
    div = div.replace(/^0+/, "")
    quotient += div
  }

  // 残りの余りを野あしておきます
  rest = "." + (rest.padEnd(divisorPrecision, "0") || "0")

  // 小数部の有効桁数が20桁を超える場合、切り捨てを実行します
  const MAX_DECIMALS = 20
  if (rest.length > MAX_DECIMALS + 1) {
    rest = rest.slice(0, MAX_DECIMALS + 1)
  }

  // 少数部で先頭の0を削除します
  if (rest.startsWith(".")) {
    rest = rest.slice(1)
  }
  if (rest.endsWith("0")) {
    rest = rest.replace(/0+$/, "")
  }

  // 空なら0にします
  if (!rest || rest === ".") rest = ".0"

  return quotient + rest
}

時間計算量はO(n^2)となるように処理をするように注意しましょう。

時間計算量
割る数の桁数をm、被除数の桁数をnとする
O(n * m) ~ O(n^2)

解法2:同時除算法

この解決策では、ラス法の代わりに、割る数と被除数を同時に処理します。

具体的には、以下の手順に従って処理を行います。

  1. 結果の初期値をゼロにします。
  2. 左から1桁ずつ、割る数と被除数の桁を同時に1ずつ取り出します。
  3. 割る数で、先に取り出した数を割ります。
  4. 得られた整数部を、答えの最下位に加算し、小数部を保持します。
  5. 小数部が十分に精度を持つまで、同じ割り算を繰り返します。小数部が0になったら、処理を終了します。
  6. 被除数と割る数の両方の桁数が0になるまで、3〜5を繰り返します。

この方法は、ラス法よりも良い計算時間を持ちます。このアルゴリズムの時間計算量はO(nm)であることが知られていますが、Big O表記における「最悪の状況」を感じることなく、実践的な範囲で動作することが知られています。

// 被除数を割る数で割る
function divide(dividend, divisor) {
  const [dividendInt, dividendDec = ""] = dividend.split(".")
  const [divisorInt, divisorDec = ""] = divisor.split(".")
  const divisorPrecision = divisorDec.length

  let rest = ""
  let quotient = ""

  // 台の値によってダミーのゼロを付けます
  const padding = "0".repeat(divisorPrecision)

  for (let i = 0; i < dividendInt.length || rest.length >= divisorPrecision; i++) {
    if (i < dividendInt.length) {
      rest += dividendInt[i]
    }
    // 被除数が足りない場合、ダミーの末尾に0を追加します
    else {
      rest += "0"
    }

    // すべてのzero paddingを削除
    rest = rest.replace(/^0+/, "")

    // もしrestが挿入されたときに0に戻る場合、ループを終了します
    if (!rest) break

    while (rest.length < padding.length + 1 && i < dividendInt.length) {
      i++
      rest += dividendInt[i] || "0"
    }

    // もし小数部が有効桁数を超えている場合、切り捨てます
    if (quotient.length + divisorPrecision >= MAX_DECIMALS) {
      break
    }

    // 割る数を取得します
    const divisorPart = +divisorInt + padding
    let d = 0

    // 割る数が被除数より大きい場合、1を引きます。また、剰余部分も取得します
    while (rest >= divisorPart) {
      rest -= divisorPart
      d++
    }

    dividend = rest.toString().padStart(divisorPrecision, "0")
    quo = (useDivisor || d).toString()
    quotient += quo
  }

  // 残りの余りを野あしておきます
  rest = "." + (rest.toString().padEnd(divisorPrecision, "0") || "0")

  // 小数部が有効桁数を超えている場合、切り捨てます。
  if (rest.length > MAX_DECIMALS + 1) {
    rest = rest.slice(0, MAX_DECIMALS + 1)
  }

  // 少数部で先頭の0を削除します
  if (rest.startsWith(".")) {
    rest = rest.slice(1)
  }
  if (rest.endsWith("0")) {
    rest = rest.replace(/0+$/, "")
  }
  
  // 空なら0にします
  if (!rest || rest === ".") rest = ".0"

  return quotient + rest
}

時間計算量はO(nm)で、実際にはO(n + m)のオーダーであるようです。ただし、被除数に大幅に余裕がある場合を除き、ラス法よりもはるかに速く処理されます。

divide('100000000000000.1', '-0.001') // '-100000000000000100'
divide('-0.123', '-0.00971') // '12.66735324407826982492'