阶乘函数末尾零的个数问题在算法面试中属于经典题型,LeetCode第793题将其提升到了一个更有趣的维度。题目要求我们找出所有满足x!(x的阶乘)末尾恰好有k个零的非负整数x的数量。乍看简单的问题背后隐藏着几个关键难点:
首先,直接计算阶乘再统计零个数对于大数完全不现实。比如当k=1e9时,x的阶乘将是天文数字,常规计算方法必然导致数值溢出。其次,零的个数与x之间存在非线性的阶梯增长关系,需要找到其中的数学规律。
这个问题的实际意义在于训练程序员将数学洞察力与算法设计相结合的能力。在分布式系统设计、数据分片策略等场景中,类似的非线性映射关系分析非常常见。
末尾零的个数实际上由10的因子个数决定,而10=2×5。在阶乘运算中,2的因子远多于5的因子(每两个连续数字就有一个偶数),因此零的个数完全取决于5的因子个数。
例如:
计算n!中5的因子数量的正确方法:
java复制int count = 0;
while(n > 0) {
n /= 5;
count += n;
}
这个算法的精妙之处在于:
以25为例:
由于x与零个数单调递增,可以采用二分查找:
java复制long left = 0, right = 5L * k;
while(left < right) {
long mid = left + (right - left)/2;
if(countZero(mid) < k) {
left = mid + 1;
} else {
right = mid;
}
}
为什么解要么是0要么是5?这是因为:
因此解的数量呈现周期性变化,每个有效区间包含5个连续整数。
特别注意k=0时的特殊情况:
在代码中可以通过提前判断处理:
java复制if(k == 0) return 2;
由于k可达1e9,计算过程中可能超过int范围:
对于k=1e9,logk≈30,整个算法只需约900次操作,效率极高。
java复制class Solution {
public int preimageSizeFZF(int k) {
if(k == 0) return 2; // 处理特殊情况
long left = 0, right = 5L * k;
while(left < right) {
long mid = left + (right - left)/2;
if(countZero(mid) < k) {
left = mid + 1;
} else {
right = mid;
}
}
return countZero(left) == k ? 5 : 0;
}
private int countZero(long n) {
int count = 0;
while(n > 0) {
n /= 5;
count += n;
}
return count;
}
}
根据LeetCode提交数据,常见错误包括:
java复制System.out.println("mid="+mid+" zeros="+countZero(mid));
code复制k | x范围
0 | 0,1
5 | 无
6 | 25-29
对于需要多次查询的场景,可以:
优化后的查询复杂度可降至O(1),适合在线判题系统的高频查询需求。
五因子计数实际上是勒让德公式的特例:
对于素数p,n!中p的幂次为:
∑[i=1→∞]⌊n/(p^i)⌋
当n→∞时,零个数Z(n)≈n/4。这个近似可以帮助快速估计解的范围,为二分查找提供更好的初始猜测。
python复制def preimageSizeFZF(k):
def count_zero(n):
cnt = 0
while n > 0:
n //= 5
cnt += n
return cnt
left, right = 0, 5*k
while left < right:
mid = (left + right) // 2
if count_zero(mid) < k:
left = mid + 1
else:
right = mid
return 5 if count_zero(left) == k else 0
cpp复制class Solution {
public:
int preimageSizeFZF(int k) {
auto countZero = [](long n) {
int cnt = 0;
while(n) {
n /= 5;
cnt += n;
}
return cnt;
};
long left = 0, right = 5L*k;
while(left < right) {
long mid = left + (right - left)/2;
if(countZero(mid) < k) {
left = mid + 1;
} else {
right = mid;
}
}
return countZero(left) == k ? 5 : 0;
}
};
不同语言的实现核心逻辑相同,但要注意:
全面的测试应该包含:
| 测试类型 | 示例输入 | 预期输出 | 验证要点 |
|---|---|---|---|
| 边界情况 | 0 | 2 | 0!和1!处理 |
| 小数值 | 5 | 0 | 无解情况 |
| 常规情况 | 6 | 5 | 25-29区间 |
| 大数值 | 1e9 | 5/0 | 性能与溢出 |
| 过渡点 | 31 | 0 | 验证二分准确性 |
| 特殊倍数 | 124 | 5 | 检查计数准确性 |
阶乘零个数问题最早由勒让德在1808年研究素数分布时提出。现代计算机科学中,Knuth在《计算机程序设计艺术》中详细讨论了相关算法。LeetCode等平台的出现使得这类问题成为检验程序员数学建模能力的试金石。
要系统掌握此类问题,建议:
遇到此类问题时:
面试官通常更关注解题思路而非完美代码,清晰的沟通比立即给出答案更重要。