雜記 › 技術筆記 › 171! 才溢出,23! 就已經算錯:階乘頁那句「0~1000 不會溢出」的實測邊界

171! 才溢出,23! 就已經算錯:階乘頁那句「0~1000 不會溢出」的實測邊界

那句「0~1000 不會溢出」是怎麼讓我起疑的

這頁文案寫著「基於 BigInt,支持計算 0~1000 的階乘,不會溢出」,同一句裡還藏著另一半:Number 最大約 1.7×10^308(170!),更大才需要 BigInt。於是我用最普通的寫法複核了 23 的階乘——var r = 1; for (var i = 2; i <= 23; i++) r *= i; 得到 25852016738884978212864,真值卻是 25852016738884976640000,差 1572864,第一個錯位落在 23 位數字的第 17 位。23 就在頁面宣稱的區間之內,比「170 才出問題」早了 147 檔。

結論先放著:頁面本身不犯這個錯,它沒有 Number 的乘法路;會錯的是我們在 Excel 或隨手寫的腳本裡複核它的那一步。

模板裡只有一條累乘路,第 111 行起就是 BigInt

factorialBig(第 108-116 行)只有一個循環:第 111 行 var result = 1n;,第 112 行 for (var i = 2n; i <= BigInt(n); i++),第 113 行 result *= i;。全文 BigInt 種子 1 處、Number 種子 0 處,連 5! 也是 BigInt 算的;文案那句「更大需用 BigInt」描述的是一次根本不存在的切換。

溢出檔在 171,精度檔在 23:兩條分界不在同一處

口徑分水嶺那一檔的實際值
Number 精確性23!比真值多出差額 1572864
Number 有限性171!170! 仍是 7.257415615307994e+306,171! 起為 Infinity
頁面 BigInt無分界170! 給 307 位、171! 給 310 位、1000! 給 2568 位

還有個細節:170! 與 171! 的尾數零同為 41 個(171 不含因子 5),位數卻從 307 漲到 310。「不溢出」與「數字對」是兩件事,前者晚 147 檔,後者在 22! 之後就失守。

Number 精確 22 起丟位 Number 有限 171 是 Infinity 頁面 BigInt 1000 給 2568 位 0 1000

圖 1:橫軸為 n,條長是該口徑靠得住的區間。

1000 是輸入閘門,不是算法上限

第 142 行是 if (n > 1000),用的「大於」,所以 1000 本身放行:1000! 裸值 2568 位,帶千分位顯示成 3423 個字元,多的 855 個逗號只是格式化,第 169 行的「結果位數」取的是裸值長度。輸 1001 是硬報錯,toast 原文「n 不能超過 1000」,不截斷也不靜默。把閘門繞過去直調函數,1001! 得 2571 位、2000! 得 5736 位——頁面上輸不進去,但足以說明 1000 只是閘門。

249 個尾數零:頁面上沒這個欄位,是腳本口徑

口徑先說清:第 168、169、170 行的統計塊只有 3 項,n、結果位數、是否超長結果,對模板 grep「尾隨」「零個數」是 0 匹配。所以這數是自己數的——拿 BigInt 值末尾的 0 去撞勒讓德公式 floor(n/5)+floor(n/25)+floor(n/125)+floor(n/625),n = 0..1000 全檔 0 處不一致:1000! = 200+40+8+1 = 249 個,500! 是 124 個,100! 是 24 個,25! 是 6 個。

這條階梯本身有洞:1~249 裡只有 201 個值取得到,缺 49 個,最小的洞是 5——24! 還是 4 個零,25! 直接跳 6 個,因為 25 一次貢獻兩個 5,125 處一次貢獻 3 個。

5 24 25 100 125 500 1000 1 4 6 24 31 124 249 尾數零 5 這一檔是洞

圖 2:尾數零個數隨 n 走的階梯(腳本口徑)。

最隱蔽的失敗路:parseInt 在字母處停住

第 137 行是 parseInt(nStr, 10),它在第一個非數字字元處停住且不報錯,於是寫法上最大的數能算出最小的結果:

  • 1e21 讀成 1,頁面安靜顯示 1! = 1,第 142 行的閘門根本沒觸發。
  • 1,000 與 1e5 同樣得 1;0x10 在 x 處停住得 0。
  • 小數向零截斷:5.5 與 5.9 都讀成 5,出 120,無提示。
  • 空串、abc、全形 20 落進第 140 行 isNaN 分支,只把結果區藏掉。
  • 輸 1001 反倒最乾淨:明確一句「n 不能超過 1000」。

範圍清單:文案說 100 項,程式放行 101 項

第 63 行提示寫「列表最多展示 100 項」,第 194 行的護欄卻是 if (n - m > 100)——比差值不比項數。於是 m=0, n=100 放行,第 213 行自己打出「項數:101」,列表 HTML 18133 字元;m=1, n=101 與 m=50, n=150 也是 101 項。反過來 m=0, n=1000 被拒,拒絕語「範圍過大,最多支持 100 項」,所以把 0! 到 1000! 整列出來這個操作在頁面上不存在。

耗時全是腳本口徑(Node v24.19.0,頁面上沒有耗時欄位):calcSingle(1000) 端到端 1.020 ms;最貴的是 900 到 1000 那一檔,102.485 ms、往 DOM 寫 334650 字元,因為第 202 行對每個 i 從頭重算。直調函數在 1000、5000、10000 上是 0.446、6.644、20.721 ms,n 漲 10 倍耗時漲 46 倍。

過程展示那 5898 個字元,一行都沒截

第 159 到 162 行是無條件 parts.join(' × '),全文唯一的 slice 在第 123 行、只剝負號:1000! 的過程行 5898 字元、「 × 」恰好 999 個、整塊 innerHTML 9460 字元,全檔 hasEllipsis=false。要挑刺就挑文案那串——5! 的過程塊是 DOM 裡的兩個 div,第 162 行只到「× 1」,= 120 在第 163 行那個裡,連續寫法並不存在。

Q:那 0~1000 之間到底哪裡會算錯?

頁面裡不會,它只有 BigInt 一條乘法路,n = 0..1000 全檔與公式核驗 0 處不一致。會錯的是複核它的那一步:Number 從 23! 起丟精確,171! 直接 Infinity。

Q:尾數零個數頁面上看得到嗎?

看不到,統計塊只有 n、結果位數、是否超長結果 3 項(第 168 到 170 行),超長閾值是嚴格大於 20 位,21! 還是「否」、22! 才「是」。249 是腳本數出來的。

Q:想一次拿到 0! 到 1000! 的整列怎麼辦?

範圍模式做不到,第 194 行按差值拒絕,能過的最大跨度是 101 項。真要長清單,就在階乘計算器的單值框裡按檔逐個取,最大一檔是 2568 位。

如果你是為排列組合在攢階乘,可以先看排列與組合那篇——把分子分母約掉之前先算出裸值是常見翻車點;同一類「先乘後除」的坑在最小公倍數那篇裡也出現過,中間量比結果先溢出。

📎 相關文章

一個漢字只算 5 bits:密碼強度評估器的破解時間什麼時候變成宇宙壽命級別 2026-10-27 文字↔ASCII 裡的「ASCII」其實是 UTF-8:1 個漢字吐出 3 個數,無分隔檔 100% 回不去 2026-10-27 它把餘數為 0 的那一行也打給你:GCD 過程展示頁的步數、摺疊順序與 29 步斐波那契 2026-10-27 GIF 縮完尺寸只剩首格:一個檔名叫 .gif 的 PNG 檔案 2026-10-27 BMP 進、PNG 出:12342 位元組的位圖變成 281 位元組,檔名卻還寫著 .bmp 2026-10-27