Recursive programming with List


I will use List to teach recursive programming.  A recursive function is a function that calls itself.  To avoid infinite loop, there must be a terminating condition, when to stop the self-call.  In our small exercise, we will concentrate on very small programs. I will "abstract" the main "method" to be only four.  I will use this four methods to show you how to do recursion. That is, there will be no other "method" in our program except these four and itself.

I will use the "structure" of data to show how to think recursively, therefore the "value" in List will be limited to only numbers.

a = [1,2,3,4]   is a simple list
b = [[5,6],7]   is a complex list

These two kinds of list will be the things that we write recursive program to manipulate.

First, let us start with defining the four methods:

head(L)           return  the first element in the list
tail(L)           return  the list without the first element
cons(L1,L2)   return the list with L1 as the first element and the rest is L2
islist(L)      return True if L is a list else return False

a = [1,2,3,4]
print( head(a) )
1


print( tail(a) )
[2,3,4]


b = cons(1, tail(a))
print(b)
[1,2,3,4]


You can immediately see this invariance:

   cons(head(L),tail(L))  ==  L

Arm with these four simple methods, we will begin our adventure.
Let us count how many element there is in a simple list.

mylen(L)

We use case-analysis.  The first case is when the list is empty.  The second case is when the list is not empty.

1)  if L is empty  the number of element is 0.

We want to make the list smaller step-by-step while we keep counting until the list is empty.

2)  if L is not empty the number of element is 1 + count the tail of L

so

def mylen(a):
  if( a == [] ):
    return 0
  return 1 + mylen(tail(a))


See when it runs.

mylen([1,2,3])
  2)  1 + mylen([2,3])
      2)  1 + 1 + mylen([3])
          2)  1 + 1 + 1 + mylen([])
              2) 1 + 1 + 1 + 0


return 3

We can apply this analysis to another example.  We want to print each element of a list individually.  Two cases:

1)  the list is empty, stop printing
2)  print one element, then do the rest, the list is getting smaller each step.

def printeach(a):
  if( a == [] ):
    return
  print(head(a))
  printeach(tail(a))

printeach([1,2,3])
>
1
2
3


Now, we will tackle a bit more difficult counting task.  How to count a complex list?  Let us start with using mylen() with a complex list.

print(mylen([[5,6],7])
2

It counts [5,6] as one element.

We need to check if an element is simple we keep counting as before.

def mylen2(a):
  if( a == [] ):
    return 0
  if( not islist(head(a)) ):
    return 1 + mylen2(tail(a))
  return ....


We must add another case when the first element is a list.  Here is how a magic happen!   If head(a) is a list, surely we can use our counting method to count it.  No need to write any new method.

def mylen2(a):
  if( a == [] ):
    return 0
  if( not islist(head(a)) ):
    return 1 + mylen2(tail(a))
  return mylen2(head(a)) + mylen2(tail(a))


Bravo!

Now try,

print(mylen2([[5,6],7]))
3


Next task is constructing a new list, using cons().  Write a copy function for a simple list.  Two cases:

1)  list is empty, return an empty list
2)  construct a new list from the first element and the "copy" of the rest.

In the second case, the input list becomes shorter and shorter until it is empty.

def copy(a):
  if( a == [] ):
    return []
  return cons( head(a),  copy(tail(a)))

a = [1,2,3]
b = copy(a)
print(b)
> [1,2,3]

The cons works by adding the first element to the rest which will be recursively built.

cons(1, ...
        cons(2, ...
                cons( 3, [] )))

Now a more challenging task, write a function to reverse a list.

rev([1,2,3])
[3,2,1]


We use a trick of creating an extra variable to hold the new list during construction.

def rev(a):
  return(rev2(a,[]))


Then we can take elements from a, one-by-one, and put that into the second list using cons().  Definitely, using case-analysis:

1)  if the input list is empty, the task is done, return the new list
2)  meanwhile, take head() from input, and cons it to the new list

at step 2,  the input list becomes shorter and shorter.

def rev2(a,b):
  if(a == []):
    return b
  return rev2(tail(a), cons(head(a),b))


Please take a notice that, the order of putting an element into the new list is the reverse of the input list.  Hence, it reverses the input list.

Let us tackle another interesting program.  Write a program to substitute one value by another value for all elements in a list.  For example, substitute 1 with 8 in the list [1,2,1,3]

print(subs(1,8,[1,2,1,3]))
[8,2,8,3]


We use similar trick of "rev" to go into the input list, looking at element, and construct a new list while the input is getting shorter and shorter.

def subs(a,b,c):          # substitute a with b in list c
  return subs(a,b,c,[])   # use an extra variable to hold the new list


when the element is matched, we use the substitute value to construct the new list, otherwise we use the old value.  The recursion occurs when the input list is getting shorter and shorter.   { subs2(... tail(c) ... ) }

def subs2(a,b,c,d):
  if(c == []):
    return d
  if( head(c) == a ):
    return subs2(a,b, tail(c), cons(b,d))
  return subs2(a,b, tail(c), cons(head(c),d))

But wait, the output is the reverse of input!  (because the new list is creating in the reverse order, similar to the function "rev").  A simple solution is to reverse it at the end.

print(subs(1,8,[1,2,1,3]))
[3,8,2,8]

def subs(a,b,c):
  d = subs2(a,b,c,[])
  return rev(d)

But that is double the work.  So, we will do it in the proper order, using similar pattern as in copy().  That is, using cons at the outset (and not using an extra variable to hold the new list).  It means, constructing a new list by
"consing" the first element with while substituting the rest of the input. The input list is getting shorter and shorter.

def subs3(a,b,c):
  if(c == []):
    return []
  if( head(c) == a ):
    return cons( b, subs3(a,b, tail(c)))
  return cons( head(c), subs3(a,b, tail(c)))


Before we finish, I want to show how to apply this thinking skill to other kind of data, for example, array.  Yes, you can do recursive in an array using index for elements in the array too. 

A simple example, write a recursive function to sum all elements in an array. We will use Python list to hold an array.

print(sum([1,2,3]))
> 6


We use index to pick an element and use index to check for terminating condition.  If it is not yet done, self-call until it reaches the end of array.

We use extra variables for start, end index.  The terminating condition is that the current index reaches the end of array.  { i == end }. The recursive step produces the sum of the current element with the sum of the rest of the array, hence the "sum2(i+1,end,a)".  "i+1" is the next element and advances the index toward the end.  Remember that the index of an array of size n, is range 0 .. n-1.

def sum(a):
  return sum2(0,len(a)-1,a)

def sum2(i,end,a):
  if( i == end ):
    return a[i]
  return a[i] + sum2(i+1,end,a)

Hope you enjoy the mental gymnastic of this lecture.

Here is my definition of the main four methods.  You must include them into your Python program.

def head(a):
    return a[0]

def tail(a):
    b = list(a)
    c = b.pop(0)
    return b

def cons(a,b):
    b.insert(0,a)
    return b

def islist(a):
    return isinstance(a,list)

Homework

Of course, all programs must be recursive :-)

1.  write a printeach() of a complex list.
2.  given a list of list, write a program to produce a list of the size of each sub-list.  lenoflist([[1,2,3],[4,5]])  is  [3,2]
3.  write a program to produce a new list which each element is double of the input list.  double([1,2,3]) --> [2,4,6]
4.  Here is a difficult one, write a program to reverse a complex list.  It means that all sub-list must also be reverse.  For example  revx([[1,2],[3,4]]) --> [[4,3],[2,1]]
5.  Write a program to sum element-wise of two input lists of the same size, produce the new list.  sumlist([1,2,3],[4,5,6]) -->  [5,7,9]
6.  Recursion in an array.  Do the similar problem as 5. but using index into the input array instead.  Observe how different two programs (4 and 5) are.
7.  In Ajarn Somchai's slide, he asked you to write a program to "flatten" a complex list.  Do it with our four methods.   flatten([[1,2],3]) --> [1,2,3]
8.  If you really want a mind-bending exercise, write a recursive program to multiply matrix (not using index), representing a matrix with list of list.

Solution to the problem 1, 2, 3
 
last update 29 Mar 2016