题目链接:https://leetcode.com/problems/factorial-trailing-zeroes/
题目描述:
Given an integer n, return the number of trailing zeroes in n!.
Note: Your solution should be in logarithmic time complexity.
大概意思就是求N!的末尾0的个数.
这道题一拿过来基本没什么思路,先不想代码,纯粹当做一个数学问题上网搜了一下,很容易得到以下思路:
看从1到n能分解出多少个因子5,因为一个5与一个偶数相乘会有一个0,而偶数的个数是远大于因子5的个数的,所以转换为求因子5的个数。
然后依次看1到n中能被5整除的个数m1,
能被5的平方整除的个数m2,
能被5的立方整除的个数m3,。。。
一直到5的(k+1)次方大于n为止,n!末尾0的个数就等于m1+m2+m3+...+mk.
对于求5的因子的个数,很容易直观的想到第一种解法,就是从1遍历到n的每个数都除以5,可以被5整除的,继续除以5,直到不能整除为止,每整除一次,计数器就加1.代码如下:
先贴第一下自己第一次写下的错误代码:(此段代码别人可忽略)
int trailingZeroes(int n) { void division(int m, int c); int i,count; count = 0; for(i = 1; i <= n; i++){ if(i%5 == 0){ count++; division(i/5, count); } } return count; } void division(int m, int c){ if(m%5 == 0){ c++; division(m/5, c); } }
int trailingZeroes(int n) { int i,count; count = 0; for(i = 1; i <= n; i++){ while(i%5 == 0){ count++; i /= 5; } } return count; }这里的运行时间又超时了,不是由于这个算法本身的复杂度问题(下面再说),而是因为在while循环里修改了索引变量i的值,正确版本为:
int trailingZeroes(int n) { int i,temp; int count = 0; for(i = 1; i <= n; i++) { temp = i; while(temp % 5 == 0) { count++; temp /= 5; } } return temp; }
现在分析这个代码的复杂度,因为要从1一直遍历到n共n次,每遇到5的倍数还要加上除以5的次数,即本题所得结果,大概是n/4次,所以计算次数大概是1.25n次,在LeetCode的测试数达到近20亿次的时候,显然运行时超时。
如何减少时间复杂度呢?考虑上文分析的5的次数计算方法,其提到说结果为m1+m2+m3+...+mk,其实只要看n里面有几个5,几个25,。。。几个5的k次方即可,所以直接用n依次除以5的各阶次方,把每次得到的加起来即为所求结果。
对于优化版,第一次用的是乘法,代码如下:
int trailingZeroes(int n) { int i; int count = 0; for(i = 5; i < n; i *= 5){ count += n/i; } return count; }
所以最后通过的版本是采用除法,代码如下:
int trailingZeroes(int n) { int count = 0; while(n){ count += n/5; n = n/5; //换成了除法 } return count; }
小结:
1.做题的时候可以先用直观的容易想到的方法解题,然后看代码,如果可以容易发觉的错误在运行前就应该找到,然后再根据错误改代码,并优化代码。
2.有些递归操作,是可以用while或者for循环代替的,要先想着用循环语句解决问题。
3.在做一些循环的时候,乘法可能会超出运行时间,可以改成除法。
4.就是对这一类数学题的解决办法了,对于求N!末尾0的个数,可以转换为求因子5的个数。
令1到n中能被5整除的个数m1,能被5的平方整除的个数m2,能被5的立方整除的个数m3,。。。一直到5的(k+1)次方大于n为止,n!末尾0的个数就等于m1+m2+m3+...+mk.