n! using a while loop

Let's write a function that computes $n!$ using a while loop.

In [1]:
def fact_while(n):
    #Invariant (a fact that we make sure doesn't change):
    #cur_prod == (i-1)!
    
    cur_prod, i = 1, 1
    #Note: 1 = (1-1)!, so cur_prod == (i-1)!
    
    
    while i != n+1: #Equivalently: if not (i == n+1)
        cur_prod *= i
        i += 1
        #Invariant: cur_prod == (i-1)!
        
    
    #Here, i == n+1, and since cur_prod = (i-1)!, we can return
    #cur_prod == ((n+1)-1)! == n
    return cur_prod  

Aside: default argument values

We can supply default arguments to functions. Here is a simple example:

In [2]:
def f(a = 2, b = 3):
    return a + b

Here, the default value of a is 2, and the default value for b is 3. We can canll f() with no arguments at all, and get 2 + 3 back:

In [3]:
f()
Out[3]:
5

The local variables got assigned their default values of 2 and 3 respectively. We can also supply a but not b:

In [4]:
f(10)
Out[4]:
13

In order to be able to supply b but not a, we need named arguments (explained elsewhere.)

An implementation of the same algorithm without while-loops

We are now ready to implement the same algorithm as above, but without any while- or for-loops:

In [5]:
def fact_iter(n, cur_prod = 1, i = 1):
    '''Return n!
    
    Arguments:
    n -- a non-negative int
    cur_prod, i -- ints such that cur_prod = (i-1)!
    '''
    
    if i == n + 1: #same as not (i != n+1)
        return cur_prod
    
    
    #cur_prod = (i-1)!
    #cur_prod <- cur_prod*i, i <- i+1    
    return fact_iter(n, cur_prod*i  ,i+1)

The idea is almost all contained in the docstring: we specify that cur_prod and i must be such that

cur_prod = (i-1)!

By itself, this doesn't mean much -- the docstring is just text, not Python code. But it helps us write the function. If we assume that the function works as long as we call it with cur_prod and i such that cur_prod = (i-1)!, we can perform the recursive leap of faith.

Let's start with the base case. If i == n+1, then out job is done, since cur_prod must equal (i-1) != (n+1-1)! = n!, so we can just return cur_prod.

Otherwise, we can call fact_iter again, and try to push i towards n+1. That's exactly what we do in the recursive call. (Note that we take care to make sure that it's still the case that in the new call, cur_prod = (i-1)!.

Finally, we'd like to be able to call fact_iter(n). We can do that, since we specify the default values of cur_prod = 1 and i = 1. (Note that 1 = (1-0)!.)

Everything from fact_while() has a direct equivalent in fact_iter() and vice-versa. You can hopefully see that we can transform any while-loop into a function that uses recursion instead of while-loops.

Here is what the call tree looks like:

               (n, 1*1*2*3*...*n, n+1)
               /      \ 
            n! /      |
              |       | 
               \      | 
                (n, 1*1*2*3, 4)
               /      | 
            n! /      |
              |       | 
               \      | 
                (n, 1*1*2, 3)
               /      | 
            n! /      |
              |       | 
               \      | 
                (n, 1*1, 2)
               /      | 
            n! /      |
              |       | 
               \      | 
                (n, 1, 1)
                   /   
                  /
                  n!



Note that this is different from the call tree for the other recursive factorial function we wrote:

In [6]:
def fact(n):
    if n == 0:
        return 1
    return n*fact(n-1)

In fact(n), computation was performed was going "up" the call tree and going "down" the call tree:

            0
           /|
       1 |  |
           \|
            1
          / .
       2/   .
        \   .
            .
          / .
(n-3)!  |   .
         \  |
           n-2
        /   |
(n-2)!  |   |
         \  |
            |
           n-1
        /   |
(n-1)!  |   |
         \  |
            |                
            n
           /
          /
          n!

We could express this computation as $n\times(...\times(2\times(1\times(1)))....)$

On the other hand, the computation in fact_iter is still essentially a loop, since we are not decomposing the problem into "smaller" subproblems. It's best expressed as simply $1\times 2\times 3\times ... \times n$.

Even though fact_iter is recursive in form, it really is still an iterative approach.