问题描述: 设计一个算法,计算出n阶乘中尾部零的个数。
样例:
11!=39916800
11
!
=
39916800
,因此应该返回 2。
解答(js实现):
const trailingZeros = function (n) {
var sum = 0;
var power = Math.floor(Math.log(n) / Math.log(5));
for (var i=1;i<=power;i++) {
sum += Math.floor(n / Math.pow(5, i));
}
return sum;
}
分析:思考一个数尾部的零是怎么得来的,乘以10对吧,所以n的阶乘尾部有多少零就可以看成它有多少个10这个因子,而10又可分解为两个质因数相乘
2×5
2
×
5
。将
n!
n
!
分解为质因数相乘的形式,大概是这样:
那么这个怎么找呢?我们看一下上边的等式,现在已经乘到10了,出现了2个5,想象一下继续乘下去的情况,到15时会再出现一个,到20时又会再出现一个,到25时则会出现2个,继续看下去,就会发现一个规律:
(1) 每逢遇到5的倍数,就会出现一个;
(2) 每逢遇到5的幂次方的倍数,就会出现5的幂次方的指数个,为了便于计算,我们把遇到5的幂次方的倍数时也算到5的倍数里,这时遇到5的幂次方的倍数就会出现 (5的幂次方的指数−(5的幂次方的指数−1)) ( 5 的 幂 次 方 的 指 数 − ( 5 的 幂 次 方 的 指 数 − 1 ) ) 个,也就是1个。
这样用文字来说会有点绕,我们不妨用代数式说明一下:
- 5 5 的倍数,遇到一次就出现1个5,会遇到 次,记作 n5×1=n5 n 5 × 1 = n 5 个;
- 52 5 2 的倍数,遇到一次就会出现2个5,会遇到 n52 n 5 2 次,但是 52 5 2 的倍数又包含在 5 5 的倍数中,所以如果我们按2个算就会每次遇到都多计算1次,因此把那1次减去,就记作 个;
- 53 5 3 的倍数,遇到一次就会出现3个5,会遇到 n53 n 5 3 次,但是 53 5 3 的倍数又包含在 5 5 的倍数和 的倍数中,所以如果我们按3个算就会每次遇到都多计算2次,因此把那2次减去,就记作 n53×(3−2) n 5 3 × ( 3 − 2 ) 个;
- 54 5 4 的倍数,同理,可记作 n54×(4−3) n 5 4 × ( 4 − 3 ) 个;
- 依此类推下去。
根据以上,我们不难得出一个公式:
js中有Math.log()函数,返回一个数的自然对数(loge, 即ln)。虽然没有直接求 log5n log 5 n 的方法,但 log5n=lnnln5 log 5 n = ln n ln 5 ,所以n(包括n)以内5的幂次方的最大指数可求得:
var power = Math.floor(Math.log(n) / Math.log(5));
至此,我们从1开始循环到power,每次循环都加上公式中对应循环次数的项,即可得到结果:
var sum = 0;
for (var i=1;i<=power;i++) {
sum += Math.floor(n / Math.pow(5, i));
}
总结:由以上的计算方式可以看出,power就是
lnn
ln
n
,所以符合O(logN)的时间复杂度。在做这道题时,也找了不少资料,很多都提到求Sum的那个公式,但是具体怎么来的都没有很清晰地写思路,于是自己就试了试,期间也跑偏了几次,还好最后自圆其说地归纳出来了,希望对看到的朋友有所帮助。
另外,还有一种思路也能按O(logN)的时间复杂度实现,这篇博客对于另一种思路写得很清晰,博主很用心,推荐一下 ^-^ 。