首页 / 资讯中心 / 文章详情

阶乘末尾连续零个数怎么算?纯整数除法实现原理与面试代码

阶乘末尾连续零个数怎么算?纯整数除法实现原理与面试代码 ★ FEATURED ARTICLE
简介一份面向爬虫逆向、JS加密与数据签名研究者的代码解析包围绕阿里140-n值生成逻辑展开完整复现从数组组装到自定义base64加密、再加140#前缀的纯算流程。资源共8个文件、大小16KB包含Python与PHP两个版本的生成器实现对应3个php、1个py附带2个HTML演示页面便于本地查看效果另有.gitignore与.inscode配置适合在CodeInsight等环境中直接运行。已有155人学习查看。通过这份资源读者可以理清Ye、ke、B、u四个数组的构成关系其中Ye由P与T拼合P基于32位字符串和时间戳拆分T借助B和g拼接u则依赖浏览器数据与JS计算最终经自定义base64编码输出目标n值。包内代码注释与调试文件如test_php_version.php、php_demo.html可辅助对照学习既适合初学者理解加密流程也能为开发者提供可直接改造的参考实现。1. 阿里140-n值纯算解析到底在算什么看到《阿里140-n值纯算解析[代码]》这个标题先别被“140”吓到。这是一道非常经典的数论编程题给定 n算出 n! 末尾连续 0 的个数。n140 时答案是 34而整个过程既没有把 140! 这个 248 位的大数算出来也没有调用任何大数库。一道题只靠一张草稿纸加三段循环代码就能落地这就是标题里“纯算”二字的含金量。这类题考的是透过现象看本质末尾的 0 是因子 2 与因子 5 相乘产生的而 2 永远比 5 多于是问题被化简成统计 1 到 n 中因子 5 的总个数。适合正在准备算法面试的人也适合想把手写边界条件练扎实的开发者。你不需要记住任何捷径口诀看完就能独立手推。2. 先想清楚数学原理为什么末尾 0 的个数只跟因子 5 有关2.1 “纯算”指什么不查表、不调库、不碰大数我见过不少人拿到这道题第一反应是先算阶乘再循环除以 10 数零。这个思路本身不蠢但纯算解法完全绕开了大数运算。核心逻辑其实一句话10 2 × 5一个末尾 0 必然对应一对因子 (2, 5)。在 1 到 n 的连乘里2 的倍数几乎随处可见每两个数就至少有一个包含因子 2而 5 的倍数明显稀疏得多。因此因子 2 的数量远大于因子 5 的数量末尾 0 的个数被因子 5 的总个数卡死。把“瓶颈”这个思路记下来后面的推导就顺了。我们不需要关心 2 有多少个只需要统计所有乘数里共贡献了多少个因子 5。这里的“纯算”不是玄学就是把一个看起来需要大数支持的问题用整数除法逐级拆解。它不依赖语言特性不依赖大数库任何语言都能用同样的方式实现。一个容易忽略的细节是25 5 × 5 贡献了两个因子 5125 贡献三个以此类推。所以统计因子 5 不能只数 5 的倍数个数还要数 25 的倍数、125 的倍数……一直数到 5 的幂超过 n 为止。这个逐级累加的过程就是整道题的骨架。2.2 手算 n14028 5 1 34为什么不能漏掉 25拿 n140 实际走一遍整个过程只需要小学除法。先数 1 到 140 里有多少个 5 的倍数140 ÷ 5 28得到第一层贡献 28。再数有多少个 25 的倍数140 ÷ 25 5这 5 个数在刚才的 28 个里已经各被算过一次因子 5但它们还额外多出一个因子 5所以要再加 5。接着数 125 的倍数140 ÷ 125 1这个数又额外多出一个因子 5再加 1。140 ÷ 625 0商为零停止累加。分母140 ÷ 分母本次累加累计值528282825553312511346250034所以 140! 末尾连续 0 的个数是 34。注意这里用的是整除不是浮点除法。很多人在这一步开始漂移有人会写 140 / 5 140 / 25 140 / 125其实等价于上面的表格但如果漏掉 25 这一项答案就会少 5。还有人说“那就 140 ÷ 5 28再 28 ÷ 5 5再 5 ÷ 5 1”这其实是同一个意思的另一种写法每次把当前的商再除以 5本质上就是在数 25、125、625 的倍数。这种连续除以 5 的写法就是后面代码里循环的原型。2.3 写成通用公式逐级除以 5 的幂直到商为零把上面的手算过程抽象成公式f(n) ⌊n/5⌋ ⌊n/25⌋ ⌊n/125⌋ …直到某一项 ⌊n/5^k⌋ 等于 0 为止。这里 ⌊⌋ 表示向下取整。整个公式只有整数运算没有乘法、没有浮点所以叫“纯算”很贴切。复杂度也一目了然每次分母乘以 5循环次数约等于 log₅n。n140 时只要循环 3 轮n 取 10 亿时也只要 13 轮左右。空间复杂度是 O(1)除了一个计数器不需要任何额外存储。这是全篇最反直觉的点n 可以大得离谱但计算轮数始终是几十次封顶。你甚至可以用这个特性来反向估算答案的量级——后面第 6 章会专门讲这个技巧。3. 把 n 值纯算公式翻译成代码循环与递归的边界差异3.1 最直接的写法while 循环里连续除以 5既然手算时是“除以 5 → 再除以 5 → 再除以 5”代码里就用一个 while 循环模拟这个过程。下面是完整可运行的核心函数选择 Java 是因为这类题在面试白板上用 Java 写的人最多但逻辑对所有语言通用。public static long trailingZeroes(int n) { long count 0; while (n 0) { n / 5; count n; } return count; }这段代码的逻辑顺序很关键先执行n / 5再把更新后的 n 累加到 count。第一次进入循环时n / 5算出的就是 ⌊n/5⌋第二次进入时n 已经被缩小为 ⌊n/5⌋再除以 5 就得到 ⌊n/25⌋。这样每轮循环恰好对应公式里的一项不需要额外维护分母变量。参数 n 是 int 类型返回值用 long 是为了给后面第 4 章要讲的超大输入留余地。如果面试官问“循环条件能不能写成n 5”答案是不能因为 n5 时n 5成立但最后一项 ⌊5/5⌋1 会被算进去这是对的可 n0 时n 5直接跳过返回 0 也对——表面上看没问题但只要你把条件改成n 0就不需要再讨论 n 小于 5 的分支统一处理更干净。3.2 递归改写更贴近数学公式但要注意栈深度递归版几乎和公式一一对应f(n) n/5 f(n/5)递归出口是 n 5 时返回 0。这样写的好处是读代码的人一眼能看出数学结构坏处是多了函数调用开销。不过这里的递归深度是 log₅nn140 时深度只有 3 层n 取 int 上限时深度也不超过 14 层完全不用担心栈溢出。public static long trailingZeroesRec(int n) { if (n 5) { return 0; } return n / 5 trailingZeroesRec(n / 5); }注意递归出口写的是n 5不是n 0。虽然两者在这个函数里结果一样但n 5表达的是数学语义当 n 小于 5 时不存在因子 5。递归版每次先取n / 5再调用自身和循环版里的n / 5语义完全一致。我最常用的还是循环版因为不用考虑递归深度而且函数调用本身有额外开销递归版适合在讲解公式时用代码即公式。3.3 最小可运行测试用一组用例验证 34 这个答案光写核心函数不够要能跑起来验证。下面这个 main 方法覆盖了几个关键边界点n0、n4 验证小于 5 的返回值n5 和 n10 验证基本增长n25 验证 25 贡献双倍因子 5n140 对应标题场景最后用 Integer.MAX_VALUE 压一下 int 上界确认返回值不会溢出。public static void main(String[] args) { int[] cases {0, 4, 5, 10, 25, 100, 140, Integer.MAX_VALUE}; for (int n : cases) { System.out.printf(n%-12d zeroes%d%n, n, trailingZeroes(n)); } }运行结果应该是下面这张表。n25 返回 6 而不是 5这是最容易验证“循环除以 5 有没有漏项”的用例nInteger.MAX_VALUE 返回 536870902这个数字后面会用来讨论返回值溢出。n输出0040511022561002414034Integer.MAX_VALUE536870902为什么 n25 是 6因为 25 的倍数只有 25 一个数它贡献两次因子 5而 5 的倍数有 5、10、15、20、25 共 5 个数其中 25 已经被算过一次。所以总数是 5 1 6。这个用例能帮你快速排查“只统计了一次因子 5”的实现错误。4. 参数怎么设输入范围、返回值上限与复杂度实测4.1 输入范围分析int 和 long 的极限下返回值会不会溢出在 Java 里int 最大值是 2,147,483,647对应答案 536,870,902仍然在 int 范围内。所以单纯用 int 存返回值不会溢出。但如果你把入参类型改成 long答案会跟着变大long 最大值约 9.22 × 10¹⁸返回值约 2.305 × 10¹⁸仍在 long 范围内。这里有个隐含风险如果返回值类型写死 int一旦将来入参换成 long答案瞬间超过 int 上限编译期根本发现不了。我的做法是返回值一律用 long入参保持 int 或 long 均可。这样既覆盖了当前场景也避免后续改类型时埋雷。参数 n 为负数时数学上阶乘没有定义函数返回 0 是最稳妥的约定但真正工程化时应该在入口就拦截负数参数并抛出异常而不是静默返回 0。入参类型入参最大值返回值最大值建议返回类型int2,147,483,647536,870,902longlong9,223,372,036,854,775,807≈ 2.305e18long这里还有个小知识点答案的近似值是 n/4因为等比数列 ⌊n/5⌋ ⌊n/25⌋ ⌊n/125⌋ … 的极限是 n/(5-1) n/4。所以 int 上限的答案约等于 2,147,483,647 ÷ 4 ≈ 536,870,911实际是 536,870,902误差只有 9。用这个近似可以做快速心算粗验第 6 章会展开。4.2 时间复杂度与空间复杂度为什么 n 再大也就几十轮循环每轮执行一次整数除法和一次加法除法后面跟着更新 n 的值。循环轮数不是由 n 的大小直接决定而是由 n 能被 5 整除多少次决定也就是 log₅n。下面这张表给出几个典型量级下的迭代轮数n迭代轮数140310^5710^91310^1826看到这个表你就明白这个算法本质上是 O(log n)空间 O(1)。n 从 140 涨到 10 的 18 次方轮数只从 3 涨到 26。所以这类题常被拿来考察面试者对“大数问题的分解能力”而不是考察谁真的能把 10 的 18 次方算出来。你写代码时不需要做任何性能优化所有时间几乎都花在循环条件判断上肉眼可忽略。4.3 面试追问变体求二进制末尾 0 的个数怎么改参数同一个思路可以推广到任意进制。如果题目改成“求 n! 在二进制表示下末尾连续 0 的个数”只是因为进制里的因子从 5 变成了 2。2 是阶乘里最不缺的因子所以结论会变成一个看起来很大的数但公式完全对称。下面这个函数就是二进制版本核心改动只有两处分母换成 2用右移代替除以 2。public static long trailingZeroesBase2(long n) { long count 0; while (n 0) { n 1; count n; } return count; }右移一位等价于整除 2所以n 1和n / 2在这个语境下完全一样。二进制版本返回值约等于 n因为等比数列极限是 n/(2-1) n。比如 n140 时二进制末尾 0 的个数是 140 ÷ 2 140 ÷ 4 … ≈ 138比十进制版本大了四倍多。推广到 p 进制就是把分母换成 p循环里n / p即可。能在一道题里同时答出十进制、二进制、p 进制三种版本面试官通常会直接跳过后续的追问。5. 避坑清单四个最容易翻车的实现细节5.1 第一个坑非要把阶乘算出来再数 0n22 就翻车现象先用循环算出 n! 的真实值再一边除 10 一边计数n22 时结果变成负数或者输出明显不对。原因22! 约等于 1.12 × 10²¹远超 long 的 9.22 × 10¹⁸ 上限。算到一半整数溢出后面的计数自然全错。在 Java 里 long 溢出不会抛异常只会静默回绕这是最阴险的地方。解决从根上放弃计算阶乘本体。纯算解法的价值就在这里它只统计因子 5 的个数任何一步都不出现大数。验证方法很简单跑trailingZeroes(22)正确答案是 4如果实现正确根本不需要算 22! 是多少。5.2 第二个坑只除一次 5n25 给出 1 而不是 6现象核心函数写成return n / 5;n25 时返回 1正确答案是 6。原因25 是 5 的平方贡献了两个因子 5。单次除以 5 只统计了它作为“5 的倍数”的那一次漏掉了它比普通 5 的倍数多出的一个因子。写代码时如果只算n/5这一项本质上就是认为每个 5 的倍数只提供一个因子 5。解决用循环连续除以 5 并累加或者用公式n/5 n/25 n/125 ...。我建议在代码里加一行注释说明为什么要连续除防止三个月后的自己看不懂。写成while (n 0) { n / 5; count n; }n25 时会依次累加 5 和 1结果就是 6。5.3 第三个坑用 for 循环维护 5 的幂分母int 溢出导致少算一轮现象写成for (int i 5; i n; i * 5) { count n / i; }小 n 全部正常但 nInteger.MAX_VALUE 时输出比正确答案少 1。原因当 i 1,220,703,125 时乘 5 的结果溢出成负数循环条件i n立刻不成立最后一次count n / i没执行。int 溢出在乘法中不会报错只会在运行时悄悄变成负值这类 bug 在单测覆盖不到大边界时极难发现。解决不要用分母变量去乘 5改为每次把当前 n 除以 5让 n 自己变小。这样全程只有除法没有乘法溢出风险。这是循环版相对于 for 版最大的优势循环次数由 n 的缩小速度决定不受 5 的幂增长溢出影响。如果你确实想保留分母写法就必须把 i 声明为 long。5.4 第四个坑用浮点数做除法或算对数结果在边界处漂移现象有人用Math.log10(n) / Math.log10(5)来推断循环次数或者用Math.pow(5, i)生成分母再配合强制类型转换取整结果在 n125 这种边界上偶发性地少算 1。原因浮点数无法精确表示 5 的幂和大数的商。125 从整数转成 double 后仍是精确的但更大的数比如 5^13 1,220,703,125 在 double 里可能出现舍入误差n / Math.pow(5, i)得到 0.9999999 之类的值转成 int 后变成 0。解决全程只用整数运算。判断 5 的幂是否超过 n 的标准做法不是Math.pow而是观察“n 除以 5 后商是否为零”。只要遵循while (n 0) { n / 5; count n; }这个模式就不会用到任何浮点数。这也是我把“纯算”理解成“纯整数四则运算”的原因——这个约束本身就是避开精度坑的保障。6. 进阶验证用 n/4 粗验做心算自检养成小样本复盘这道题有个非常实用的心算技巧答案约等于 n/4。因为等比数列 ⌊n/5⌋ ⌊n/25⌋ ⌊n/125⌋ … 收敛到 n/4实际值会略小于这个近似。所以在面试现场你可以先写出精确值再用 n/4 快速验证有没有明显偏离。n140 时精确答案是 34140/4 35只差 1nInteger.MAX_VALUE 时精确值 536870902n/4 ≈ 536870911差 9。误差很小但永远不会为零因为级数截断后总比收敛值小一点。我现在的复盘习惯是拿到任何数论函数先跑一组小样本边界然后再接真实大边界。小样本指的是 n0、n4、n5、n10、n25 这一组它们分别对应“小于 5”“恰好为 5”“第一次进位”“25 双因子”四个里程碑。跑完这些再用 nInteger.MAX_VALUE 或 n10¹⁸ 压一遍边界基本能覆盖所有实现错误。这个习惯是某次我把循环条件写错、用 n25 才暴露问题之后养成的从那以后我再也不敢只跑一个用例就提交。如果你打算把这个解法迁移到其他进制记得粗验值同步变化二进制下答案约等于 n三进制下约等于 n/2p 进制下约等于 n/(p-1)。最后再说一个小习惯写完核心函数后我会顺手在注释里写下公式来源标明“公式来自 ⌊n/5⌋⌊n/25⌋…”这样代码评审的人不用从零猜你的思路。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站