Counting trailing zeros in n!

Recall that the problem that we're trying to solve is to count the number of trailing zeros in $n!$. Here is an example:

In [1]:
def fact(n):
    res = 1
    for i in range(1, n+1):
        res *= i
    return res

if __name__ == '__main__':
    print(fact(20))
2432902008176640000

$20!$ has 4 trailing zeros -- zeros at the end of the number.

Here is the naive approach to the problem. First, compute $n!$, then, keep dividing the number by 10 (i.e., removing 0's), while that's possible; all the while, keep incrementing a counter that keeps track of how many times we divided by 10.

In [2]:
def trailing_zerosA(n):
    '''Return the number of trailing zeros in n!
    
    Arguments:
    n - a positive integer
    
    '''
    n_fact = fact(n)
    
    res = 0  #number of times
             #n_fact was divided by 10 
             #so far.
    
    while n_fact % 10 == 0: 
        n_fact //= 10
        res += 1
        
    return res

if __name__ == '__main__':
    n = 20
    print("The number of trailing zeros in", n, "factorial is", trailing_zerosA(n))
The number of trailing zeros in 20 factorial is 4

Here is another approach. Observe that

$n! = 1\times 2\times 3\times ... \times n = a\times 10^k$. If $k$ is as large as it can be, then $k$ is the number of trailing zeros in $n!$.

Now, we also have $n! = a\times 2^k\times 5^k$. So if we find out how many factors of $5$ there are in the product $n! = 1\times 2\times 3\times ... \times n$, we can find the number of trailing zeros. Let's do it factor-by-factor.

First, let's write a function that finds how many factors of factor there are in an arbitrary number m.

In [3]:
def count_factor(m, factor):
    '''Return k s.t. k is the largest number s.t. m = factor^k * a
    
    Arguments
    m, factor -- positive integers
    '''        
    
    res = 0  #number of times
             #m  was divided by factor 
             #so far.
    while m % factor == 0: 
        m //= factor
        res += 1
    
    return res

The idea is to count up the number of factors of 5 in 1, 2, 3, 4, ...., n, and add them all up. Here is how this would go for $31!$:

1: 0 factors of 5
2: 0 factors of 5
3: 0 factors of 5
4: 0 factors of 5
5: 1 factors of 5
6: 0 factors of 5
7: 0 factors of 5
8: 0 factors of 5
9: 0 factors of 5
10: 1 factors of 5
11: 0 factors of 5
12: 0 factors of 5
13: 0 factors of 5
14: 0 factors of 5
15: 1 factors of 5
16: 0 factors of 5
17: 0 factors of 5
18: 0 factors of 5
19: 0 factors of 5
20: 1 factors of 5
21: 0 factors of 5
22: 0 factors of 5
23: 0 factors of 5
24: 0 factors of 5
25: 2 factors of 5
26: 0 factors of 5
27: 0 factors of 5
28: 0 factors of 5
29: 0 factors of 5
30: 1 factors of 5
31: 0 factors of 5

In total, we have 1+1+1+1+2+1=7 factors of 5, which is also the number of 0's

In [4]:
def trailing_zerosB(n):
    
    res = 0
    
    #Compute the total number of factors of 5 in all of
    #1, 2, 3, ..., n
    for i in range(1, n+1):
        res += count_factor(i, 5)
        
    return res

if __name__ == '__main__':
    n = 20
    print("The number of trailing zeros in", n, "factorial is", trailing_zerosA(n))
    
    n = 31
    print("The number of trailing zeros in", n, "factorial is", trailing_zerosA(n))
The number of trailing zeros in 20 factorial is 4
The number of trailing zeros in 31 factorial is 7

Why bother with all this? Because, as we saw in class, computing $n!$ for large $n$ is very computationally expensive, since it involves multiplying huge numbers. The second method is much more efficient.

Refactoring trailing_zerosA()

Note that we can now refactor trailing_zerosA(). That is, we can rewrite it to reuse a helper function:

In [5]:
def trailing_zerosA(n):
    '''Return the number of trailing zeros in n!
    
    Arguments:
    n - a positive integer
    
    '''
    n_fact = fact(n)
    
    return count_factor(n_fact, 10)

if __name__ == '__main__':
    n = 20
    print("The number of trailing zeros in", n, "factorial is", trailing_zerosA(n))
The number of trailing zeros in 20 factorial is 4