前言
這幾天在整理履歷相關的資料,中間有空檔時就會上Leetcode刷刷題目保持手感,其中一個問題讓我有些意外,看似簡單卻讓我在意想不到的地方碰到了一點麻煩!
Javascript中的 Safe Number
有與極大數字打過交道的朋友可能知道在JS中,一旦數字大於等於 253 ,就會出現各種問題。原因在於 253 是原生JS中,Number能夠表示的最大上界,而相對的- 253 則是最小下界。介於這兩個數字中間的數字,在 Javascript 中我們稱為所謂的Safe Number ,超出這個範圍的數字自然就是unsafe囉!
那,unsafe 又怎樣?
問得好!unsafe的數字就會出現一些有趣的現象,我們看一下以下的例子,你會看到有意思的印出結果。
console.log(Math.pow(2, 53)); // 9007199254740992 console.log(Math.pow(2, 53) + 1); // 9007199254740992 console.log(Math.pow(2, 53) + 2); // 9007199254740994 console.log(Math.pow(2, 53) + 3); // 9007199254740994 console.log(Math.pow(2, 53) + 4); // 9007199254740996
挺神奇的吧?原因在於在JS中數字其實是用依照IEEE754儲存,也就是所謂的二進位浮點數算術標準 ,會以 mantissa × 2exponent 這樣的形式儲存。當超過 253 的值之後,便只能每兩個數字才能正確顯示, IEEE754 這樣的標準也是為什麼那個經典問題0.1+0.2 ! == 0.3會產生,有興趣的朋友可以繼續往下探究。
好,我知道 unsafe number了,那我要如何進行運算?
這部分其實有相當多種方法可以使用,各種處理大數字的套件也是綽手可得,但如果你只是想簡單的計算,這邊提供一個我認為還不錯的方法-BigInt
What’s BigInt
BigInt是JS中原生的物件之一,可以用來表示 253 之後的數值,使用的方法除了利用bigInt強制轉型之外,也可以在數字的後方+一個小寫n做為表示。
console.log(BigInt(Math.pow(2, 53))); // 9007199254740992n console.log(typeof 10n); // bigint
特別注意,Bigint 與Number無法直接做運算,必須要先利用以上兩種方法之一去做轉換。
console.log(10n+5); //TypeError: Cannot mix BigInt and other types, use explicit conversions console.log(10n + 5n); // 15n
另外,當數字已經超過 253 的話,建議就用 Bigint 去做運算,而不要將 Bigint的值再次轉為Number,這會造成一些精度上的流失,比較好的做法會是利用toString之類的方法直接轉為字串儲存。
最後,來看看Leetcode上我是怎麼利用 Bigint 處理碰到的問題
Leetcode 66. Plus One
Given a non-empty array of digits representing a non-negative integer, plus one to the integer. The digits are stored such that the most significant digit is at the head of the list, and each element in the array contain a single digit. You may assume the integer does not contain any leading zero, except the number 0 itself. Example 1: Input: [1,2,3] Output: [1,2,4] Explanation: The array represents the integer 123.
簡單來說,就是給你正整數組合的陣列,你需要回傳其代表數字加1的結果。
當輸入值為 [1,5,9,7] ,你的輸出結果就需要是 [1,5,9,8]
這個題目應該會有不少切入方法,我自己的想法會是這樣的
1.將input的陣列利用join連接,此時會是一個字串 2.將該字串轉型為數字並+1 3.再次將數字轉為字串 4.利用擴展運算子或是ArrayFrom之類的方法將處理後的字串變為陣列回傳
想法相當單純,但在測試案例中硬是塞了幾個unsafe number,所以單純照上面虛擬碼的邏輯會出現錯誤,最後我修改了以上虛擬碼的邏輯,寫出以下的程式通過測試案例。
var plusOne = function (digits) { digits = [...(BigInt(digits.join("")) + 1n).toString()].map(item => Number(item)) return digits };

結語
處理大數字或是小數點之類的問題一直是JS中難以避免的,剛好刷leetcode時就讓我複習了一下這樣基本的觀念,翻了不少的資料,最後寫成文章做個簡單的紀錄!
參考文章