杂记 › 技术笔记 › 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