【詳細筆記】JavaScript數字類型詳解

前言

這是一篇偏綜合性的總結,根據筆者的 routine 整理好的,並對代碼進行了改進。如下內容出處均已在 參考資料 中列出,若有侵權,聯繫筆者刪除。javascript

思惟導圖

前置知識

本質緣由

計算機是二進制的,沒法直接表示正負數,另外在計算機內部直接實現減法,也會影響計算機效率,因此人們但願要找到一種既能使用二進制表示10進制正負數的編碼格式,同時這種編碼格式又能知足將減法轉換成加法進行運算。
html

原碼、反碼和補碼

原碼

image.png

反碼

image.png

補碼

image.png

總結

要想弄清楚補碼,必需要弄清楚補碼要解決的問題,計算機是二進制的,沒法直接表示正負數,另外在計算機內部直接實現減法,也會影響計算機效率,因此人們但願要找到一種既能使用二進制表示10進制正負數的編碼格式,同時這種編碼格式又能知足將減法轉換成加法進行運算,同時知足這兩個條件有反碼和補碼,但因爲反碼中的0有兩個編碼格式,另外反碼加法運算也比較複雜,慢慢地反碼被淘汰了。補碼恰好解決了反碼的兩個缺點,因此補碼成了現代計算機的通用編碼。
前端

IEEE 754標準

背景

隨着技術的更新,在1978年的時候,Intel公司推出了首枚16bit微處理器(CPU)8086。這臺x86的老祖宗雖然自身沒法處理小數的運算,可是在編譯器層面能夠經過用整數指令模擬出小數的運算,不過這種運算的方式效率是很是低的。


爲了解決這類問題,1980年Intel公司推出了首款x87浮點協處理器運算單元(FPU)8087,經過主板上額外的協處理器插槽,安裝後不只能夠解決小數的運算問題,而且對於不一樣的應用,性能提高了20%~500%。


對於計算機發展來講,8087是款很是棒的FPU,可是它的意義真正體如今這款FPU的設計師之一的William Kahan教授設計了IEEE-754標準的雛形,而正是由於這套標準,咱們計算機才能精準的處理小數。


1985年時,IEEE推出了IEEE 754-1985標準,隨着大佬們的努力,IEEE還推出了目前的版本——IEEE 754-2008。


而咱們使用的高級語言中浮點數的運算,如C、C++、JavaScript、Java都是基於這個標準而定。
java

概念

image.png
image.png

image.png
也叫
image.png
git

概念

JavaScript的數字類型的本質就是一個基於 IEEE 754 標準的雙精度 64 位的浮點數。
github

小數運算(爲何0.2 + 0.1 !== 0.3?)

概述

  1. 0.1 和 0.2 按照 IEEE 754 存儲後都有精度損失
  2. 運算後進行規格化時的舍入操做也有精度損失

**
面試

詳解

關於浮點數的運算,通常由如下五個步驟完成:對階、尾數運算、規格化、舍入處理、溢出判斷
image.png
將它轉換爲10進制數就獲得 0.30000000000000004440892098500626
由於兩次存儲時的精度丟失加上一次運算時的精度丟失,最終致使了 0.1 + 0.2 !== 0.3
算法

解決方案

number-precision
全面總結 JS 中浮點數運算問題 - 掘金
json

大數運算如何處理?

現成方案

1.bignumber.js
2.true-json-bigint.js
3.BigInt - JavaScript | MDN
image.png
segmentfault

大數運算

核心思想

既然數字類型會有精度問題,那就通通轉用字符串來處理。

大數相加


普通版本

/**
 * @param {string} a
 * @param {string} b
 * @return {number}
 */
function add(a, b) {
  // 首先檢查傳來的大數是不是字符串類型,若是傳Number類型的大數,在傳入的時候已經丟失精度了,
  // 就如 若是傳入11111111111111111,處理的時候已是丟失精度的11111111111111112了,則須要傳入
  // 字符串類型的數字 '11111111111111111'
  const checkNum = num => typeof num === "string" && !isNaN(Number(num))
  // 格式化數字
  const formatNum = num => (isNaN(+num) ? 0 : +num)

  if (!checkNum(a) || !checkNum(b)) {
    throw new TypeError("Big Number Type Error")
  }
  const result = []

  a = a.split("").reverse()
  b = b.split("").reverse()
  const length = Math.max(a.length, b.length)

  let carry = 0
  
  // 以較長的數字爲基準進行從前日後逐個加和,爲避免兩個數相加最高位進位後,導
  // 致結果長度大於兩個數字中的長度,for循環加和長度爲最長數字長度加一
  for (let i = 0; i <= length; i++) {
    const tempResult = formatNum(a[i]) + formatNum(b[i]) + carry
    result[i] = tempResult % 10
    
    // 當加和的數字大於10的狀況下,進行進位操做,將要進位的數字賦值給temp,在下一輪使用
    carry = Math.trunc(tempResult / 10)
  }
  
  // 計算完成,反轉回來
  result.reverse()
  
  // 將數組for中多加的一位進行處理,若是最高位沒有進位則結果第一個數位0,
  // 結果第一個數位1,則發生了進位。 如99+3,最大數字長度位2,結果數長度位3
  // 此時結果的第一位爲1,發生了進位,第一位保留,若是是2+94,第一位爲0,則不保留第一位
  return result.join('').replace(/^0+/, "")
}


奇技淫巧版

/**
 * @param {string} a
 * @param {string} b
 * @return {number}
 */
function add(a, b) {
  const checkNum = num => typeof num === "string" && !isNaN(Number(num))

  if (!checkNum(a) || !checkNum(b)) {
    throw new TypeError("Big Number Type Error")
  }

  let result = ""

  let c = 0
  a = a.split("")
  b = b.split("")

  while (a.length || b.length || c) {
    
    // ~取反操做符,~0 === -1, ~~0 === 0, ~~null === 0, ~~undefined === 0, ~~'0' === 0
        // 用+也能實現字符轉成數字,不過~~能處理null的狀況
    c = ~~a.pop() + ~~b.pop() + c
    result += c % 10
    
    // c>0會返回一個布爾值,布爾值轉換成數字的時候true爲1,false爲0
    c = c > 9
  }

  return result.replace(/^0+/, "")
}


優化版

/**
 * @param {string} a
 * @param {string} b
 * @return {number}
 */
function add(a, b) {
  const checkNum = num => typeof num === "string" && !isNaN(Number(num))

  if (!checkNum(a) || !checkNum(b)) {
    throw new TypeError("Big Number Type Error")
  }
  
  // 主要思想是分開一段段加,這樣既能保證精度且不溢出,也能提升速度
    // 若是索引超範圍,或者長度小於1那麼返回空字符串

  const MAX_PERCISION = Number.MAX_SAFE_INTEGER.toString().length - 1
  const arr = []

  while (a.length || b.length) {
    arr.push(
      parseInt(a.substring(a.length - MAX_PERCISION) || 0, 10) +
      parseInt(b.substring(b.length - MAX_PERCISION) || 0, 10)
    )
    a = a.substring(0, a.length - MAX_PERCISION)
    b = b.substring(0, b.length - MAX_PERCISION)
  }

  let carry = 0
  let result = ""

  while (arr.length) {
    const temp = (arr.shift() + carry).toString()
    result = temp.substring(temp.length - MAX_PERCISION) + result
    carry = parseInt(
      temp.substring(0, temp.length - MAX_PERCISION) || 0,
      10
    )
  }

  return result
}

// 簡化 =>

/**
 * @param {string} a
 * @param {string} b
 * @return {number}
 */
function add(a, b) {
  const checkNum = num => typeof num === "string" && !isNaN(Number(num))

  if (!checkNum(a) || !checkNum(b)) {
    throw new TypeError("Big Number Type Error")
  }

  const MAX_PERCISION = Number.MAX_SAFE_INTEGER.toString().length - 1

  let result = ""
  let carry = 0

  while (a.length || b.length) {
    const temp = (
      parseInt(a.substring(a.length - MAX_PERCISION) || 0, 10) +
      parseInt(b.substring(b.length - MAX_PERCISION) || 0, 10) +
      carry
    ).toString()
    result = temp.substring(temp.length - MAX_PERCISION) + result
    carry = parseInt(
      temp.substring(0, temp.length - MAX_PERCISION) || 0,
      10
    )

    a = a.substring(0, a.length - MAX_PERCISION)
    b = b.substring(0, b.length - MAX_PERCISION)
  }

  return result
}

大數相減


奇技淫巧版

/**
 * @param {string} a
 * @param {string} b
 * @return {number}
 */
function minus(a, b) {
  const checkNum = num => typeof num === "string" && !isNaN(Number(num))

  if (!checkNum(a) || !checkNum(b)) {
    throw new TypeError("Big Number Type Error")
  }

  while (a.length < b.length) {
    a = "0" + a
  }
  while (b.length < a.length) {
    b = "0" + b
  }

  let result = ""
  let borrow = false

  a = a.split("")
  b = b.split("")

  while (a.length) {
    const minuend = ~~a.pop()
    const subtrahend = ~~b.pop() + borrow

    if (minuend >= subtrahend) {
      borrow = minuend - subtrahend
      result = borrow + result
      borrow = false
    } else {
      borrow = minuend + 10 - subtrahend
      result = borrow + result
      borrow = true
    }
    //判斷最高位有無借位,如有借位,則說明結果爲負數
    if (a.length === 0 && borrow) {
      result = "-" + result
    }
  }

  result = result.replace(/^0+/, "")

  //判斷最後的結果是否爲0
  if (result === "") {
    result = 0
  }
  return result
}

大數相乘

目前大數乘法算法主要有如下幾種思路:

  1. 模擬小學乘法:最簡單的乘法豎式手算的累加型;
  2. 分治乘法:最簡單的是Karatsuba乘法,通常化之後有Toom-Cook乘法;
  3. 快速傅里葉變換FFT:(爲了不精度問題,能夠改用快速數論變換FNTT),時間複雜度O(N lgN lglgN)。具體可參照Schönhage–Strassen algorithm
  4. 中國剩餘定理:把每一個數分解到一些互素的模上,而後每一個同餘方程對應乘起來就行;
  5. Furer’s algorithm:在漸進意義上FNTT還快的算法。不過好像不太實用,本文就不做介紹了。你們能夠參考維基百科Fürer’s algorithm


模擬小學乘法版

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
function multiply(a, b) {
  const cn = a.length + b.length
  const c = new Array(cn).fill(0)
  /****
         * 兩兩相乘,並放進不一樣的格子裏,若是裏面有東西,則相加
         * 0
         *   8
         *     10
         *     4
         *       5
         */
  for (let i = 0; i < a.length; i++) {
    for (let j = 0; j < b.length; j++) {
      c[i + j + 1] += Number(a[i]) * Number(b[j])
    }
  }
  // 處理進位
  for (let i = cn - 1; i >= 0; i--) {
    const carry = Math.trunc(c[i] / 10)
    if (carry) {
      c[i - 1] += carry
    }
    c[i] = c[i] % 10
  }
  while (c[0] === 0) {
    c.shift()
  }
  //處理最前面的零
  return c.join("") || "0"
}


利用下標取巧版

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
function multiply(num1, num2) {
    const checkNum = num => typeof num === "string" && !isNaN(Number(num))

  if (!checkNum(a) || !checkNum(b)) {
    throw new TypeError("Big Number Type Error")
  }
 
  let len1 = a.length,
      len2 = b.length
  let ans = []

  //這裏倒過來遍歷很妙,不須要處理進位了
  for (let i = len1 - 1; i >= 0; i--) {
    for (let j = len2 - 1; j >= 0; j--) {
      let index1 = i + j,
          index2 = i + j + 1
      let mul = a[i] * b[j] + (ans[index2] || 0)
      ans[index1] = Math.floor(mul / 10) + (ans[index1] || 0)
      ans[index2] = mul % 10
    }
  }

  //去掉前置0
  let result = ans.join("").replace(/^0+/, "")

  //不要轉成數字判斷,不然可能會超精度!
  return !result ? "0" : result
}

大數相除

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
function divide(a, b) {
  const checkNum = num => typeof num === "string" && !isNaN(Number(num))

  if (!checkNum(a) || !checkNum(b)) {
    throw new TypeError("Big Number Type Error")
  }
  
  const alen = a.length
  const blen = b.length
  let quotient = 0
  let remainder = 0
  const result = []
  let temp = 0
  for (let i = 0; i < alen; i++) {
    temp = remainder * 10 + parseInt(a[i])
    if (temp < b) {
      remainder = temp
      result.push(0)
    } else {
      quotient = parseInt(temp / b)
      remainder = temp % b
      result.push(quotient)
    }
  }
  return [result.join("").replace(/\b(0+)/gi, ""), remainder] //結果返回[商,餘數]
}

大數階乘

function fact(num) {
  let result = 1
  for(let i=1; i<=num; i++){
    result = result * i
  }
  return result
}

// => 

function fact(num) {
  const checkNum = num => typeof num === "string" && !isNaN(Number(num))

  if (!checkNum(num)) {
    throw new TypeError("Big Number Type Error")
  }
  
  let result = "1"
  for (let i = "1"; lte(i, num); i = add(i, "1")) {
    result = multiply(result, i)
  }
  return result
}

function lte(num1, num2) {
  if (num1.length < num2.length) {
    return true
  } else if (num1.length === num2.length) {
    return num1 <= num2
  } else {
    return false
  }
}

參考資料

  1. 原碼、反碼、補碼的產生、應用以及優缺點有哪些?
  2. 原碼, 反碼, 補碼 詳解 - ziqiu.zhang - 博客園
  3. IEEE-754標準與浮點數運算_C/C++_馬駿超的博客-CSDN博客
  4. JavaScript 深刻之浮點數精度 - 掘金
  5. 爲何不要在 JavaScript 中使用位操做符? | 咀嚼之味
  6. [[每日一題] JavaScript面試之「大數相加」運算 - 雲+社區 - 騰訊雲](https://cloud.tencent.com/dev...
  7. A comparison of BigNumber libraries in JavaScript
  8. 談談JS中的大數運算 | 前端腳印
  9. 有趣的大數運算
  10. JavaScript實現大整數減法_JavaScript_xiaoxiaoyoumeng的博客-CSDN博客
  11. js實現大數相乘 - 我的文章 - SegmentFault 思否
  12. [[leetcode] 大數乘法](https://zhuanlan.zhihu.com/p/...
  13. 【算法】大數乘法問題及其高效算法 | iTimeTraveler
相關文章
相關標籤/搜索