第一次看到LeetCode 172题「阶乘后的零」时,很多人会和我一样产生错觉——这不就是统计阶乘结果末尾有多少个零吗?直接计算阶乘然后数零不就行了?但真正动手实现时才发现,当n稍微大一点(比如n=20),20!的结果就已经是个天文数字了,用常规方法根本无法存储和计算。
经过仔细分析题目,我发现问题的关键在于理解「尾随零」产生的本质原因。每个尾随零实际上都对应着一个因子10,而10=2×5。在阶乘的计算过程中:
因此,尾随零的数量实际上由阶乘中5的因子数量决定。这个洞察是解决整个问题的关键转折点。
为了验证这个结论,我手动计算了几个例子:
这些例子都验证了我们的结论:尾随零的数量等于n!中5的因子总数。
最初的暴力解法思路很直接:遍历1到n的所有整数,统计每个数中包含的5的因子数量,然后累加。用TypeScript实现的代码如下:
typescript复制function trailingZeroes(n: number): number {
let count = 0;
for (let i = 5; i <= n; i += 5) {
let current = i;
while (current % 5 === 0) {
count++;
current /= 5;
}
}
return count;
};
虽然这个解法正确,但它的时间复杂度是O(n log n):
当n=10^9时,这个算法明显会超时。在我的测试中,当n>10^6时,执行时间就已经不可接受了。
通过观察阶乘中5的分布规律,我发现可以分层次统计5的因子数量:
这实际上是一个数论问题:n!中质数p的幂次等于Σ[n/p^i],其中i从1到∞。
基于这个数学原理,可以写出极其简洁的代码:
typescript复制function trailingZeroes(n: number): number {
let count = 0;
while (n > 0) {
n = Math.floor(n / 5);
count += n;
}
return count;
};
这个算法的时间复杂度是O(log₅n),因为每次n都除以5,直到n为0。对于n=10^9,最多只需要执行log₅(10^9)≈13次循环。
数学证明:
在实际编码中,需要特别注意以下边界情况:
完善的测试案例应该包括:
typescript复制console.log(trailingZeroes(5)); // 1
console.log(trailingZeroes(10)); // 2
console.log(trailingZeroes(25)); // 6
console.log(trailingZeroes(100)); // 24
console.log(trailingZeroes(1000)); // 249
这个解题模式可以推广到其他类似问题:
我实际测试了两种算法的性能差异(Node.js环境):
| n值 | 暴力解法(ms) | 优化解法(ms) |
|---|---|---|
| 10^5 | 12.5 | 0.02 |
| 10^6 | 125 | 0.03 |
| 10^7 | 超时(>1000) | 0.04 |
可以看到,优化解法在n较大时优势极其明显。
这个问题实际上与数论中的Legendre公式密切相关,它给出了n!中质数p的幂次的精确计算公式:
vₚ(n!) = Σ[k=1→∞]⌊n/pᵏ⌋
这个公式在计算数论中有广泛应用。
在实际编码练习中,我总结了以下几点经验:
这道题的价值不仅在于解决一个具体问题,更在于培养我们分析问题、寻找优化路径的思维能力。在解决类似的数学相关算法题时,这种思维模式可以反复应用。