最近面試的時候遇到一道面試題,就是實現10000!,當時的第一反應是直接用遞歸實現:git
function factorial_recursion(n){
if(n <= 1) return 1;
return n * factorial_recursion(n-1)
}
複製代碼
可是這樣就會存在問題,Js中最大的安全整數爲2^53- 1,10000!結果溢出該範圍,代碼運行結果爲Infinity,沒法計算出正確的結果。github
那麼如何才能計算大數據的階乘呢?面試
可使用Js最新的基本數據類型BigInt,BigInt數據類型支持範圍更大的整數值,能夠解決整數溢出問題。算法
BigInt數據經過BigInt構造函數建立,修改代碼以下:數組
function factorial_recursion(n){
if(n <= 1) return 1;
return BigInt(n) * BigInt(factorial_recursion(n-1))
}
複製代碼
經過factorial_recursion(10000)
就能夠得出結果。瀏覽器
但當計算更高數值的階乘時,好比求20000的階乘,出現棧溢出的狀況。安全
那麼如何才能解決棧溢出問題?bash
算法思路函數
能夠從減小乘法運算的次數角度出發,階乘運算可轉換爲若干個平方差的積,使得階乘只須要n/2次乘法,而且得出規律平方差之間的差是連續的奇數。除了平方差乘數,其餘乘數根據n爲奇數或偶數也有不一樣規律。大數據
算法分析
當計算9的階乘時:
1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9
取中間值爲基值: (5 - 4) * (5 - 3) * (5 - 2) * (5 - 1) * 5 * (5 + 1) * (5 + 2) * (5 + 3) * (5 +4)
調換位置:(5 - 1) * (5 + 1) * (5 - 2) * (5 + 2) * (5 - 3) * (5 + 3) * (5 - 4) * (5 + 4) * 5
合併爲平方差:(5^2 - 1) * (5^2 - 2^2) * (5^2 - 3^2) * (5^2 - 4^2) * 5
計算平方差結果: 24 * 21 * 16 * 9
得出規律:2四、2一、1六、9之間的差分別爲基數三、五、7,奇數基礎乘數爲 n / 2
複製代碼
當計算10的階乘時:
1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10
取中間值位基值: (5 - 4) * (5 - 3) * (5 - 2) * (5 - 1) * 5 * (5 + 1) * (5 + 2) * (5 + 3) * (5 +4) * (5 + 5)
調換位置:(5 - 1) * (5 + 1) * (5 - 2) * (5 + 2) * (5 - 3) * (5 + 3) * (5 - 4) * (5 + 4) * 5 * (5 + 5)
合併爲平方差:(5^2 - 1) * (5^2 - 2^2) * (5^2 - 3^2) * (5^2 - 4^2) * 5 * (5 + 5)
平方差結果: 24 * 21 * 16 * 9
得出規律:2四、2一、1六、9之間的差分別爲基數三、五、7,偶數基礎乘數爲 n / 2 * n
複製代碼
代碼實現以下:
function factorial_square(n){
if(n <= 1) return 1;
const middle = Math.ceil(n / 2); //取中間值
let tmp = middle * middle,
result = n & 1 == 1 ? middle : middle * n; //奇偶數的基礎乘數規律不一樣
for(let i = 1 ; i <= n - 2 ; i += 2){ //連續減奇數得出各項乘數
tmp -= i;
result = BigInt(result) * BigInt(tmp);
}
return result;
}
複製代碼
BigInt兼容性並不友好,Chrome瀏覽器在67+版本中才支持該數據類型。
在不支持BigInt的瀏覽器中怎麼計算大數據階乘呢?
算法思路
可使用數組來存儲大數據結果的每位數,如result[0]存儲個位數,result[1]存儲十位數,以此類推。計算每位數時須要加上上一個位數得出的進位,最後再將數組反轉並拼接,就能夠得出大數據結果。
算法分析
以5! = 1 * 2 * 3 * 4 * 5爲例:
代碼實現
function factorial_array(n){
let result = [1], //存儲結果
digit = 1, //位數,從第1位開始
count , //每次計算的結果
num , //階乘的計算到的第幾個
i , //result中每一項
carry; //每次得數的進位
for(num = 2 ; num <= n ; num++){
for(i = 1 , carry = 0 ; i <= digit ; i++){
count = result[i - 1] * num + carry; //每一項計算結果
result[i - 1] = count % 10; //將一個數的每一位數利用數組進行儲存
carry = (count - result[i - 1]) / 10 //記錄進位
}
while (carry) { //若是還有進位,繼續存儲
result[digit] = carry % 10;
carry = (carry - result[digit]) / 10;
digit++;
}
}
return result.reverse().join("");
}
複製代碼
1.遞歸實現
優勢:實現代碼簡單,速度快
缺點:較大數據容易出現棧溢出,兼容性不夠友好
2.平方差實現
優勢:乘數計算少,速度快
缺點:兼容性不夠友好
3.數組存儲實現
優勢:數組清晰地存放大數據的每一個位數,每次計算時能簡單地從低位到高位求值
缺點:須要用數組存儲大數據的每位數,須要佔用較大內存,速度較慢