Recursion & Recursion Templates


Touretzky Reading: Chapter 8

Touretzky Exercises: 8.4, 8.5, 8.9, 8.12, 8.17-19,8.22, 8.26, 8.27-28, 8.31,8.45 8.61,8.32, 8.34-35,8.40-41,8.44,8.48-49,8.58, 8.63

Before doing 8.44, be sure you understand how CONS works for lists that are not well formed (i.e. dotted pairs). More info on this can be found in section 2.17. I've selected a lot of exercises from this section because it is so important! Be sure that as you work these exercises that you keep in mind which section of Turetzky they are in so you know which recursion template to apply.


Administrivia

Recursion

I like the definition of how to define a recursive function by Turetzky. (If you'd like see Kurt's definition, its in his third lecture in the Recursion section.) Turetzky has "three rules of recursion" in section 8.8. They are: An example may be of some use here: (defun contains-number-p (the-list) (cond ((null the-list) nil) ((numberp (car the-list)) t) (t (contains-number-p (cdr the-list))))) This function returns T or NIL based on whether or not the list it is given as input in the-list contains any numbers. It uses the built-in NUMBERP to test for "numberness."

Note how the COND clauses in contains-number-p correspond to these cases. It could also be written as:

(defun contains-number-p (the-list) (if (null the-list) nil (if (numberp (car the-list)) t (contains-number-p (cdr the-list)))))

Here's one of Kurt's examples:

(defun my-member (input-item input-list) (cond ((null input-list) nil) ((eql input-item (first input-list)) T) (T (my-member input-item (rest input-list)) This function determines if the input item is a member of the input-list. What does this function return in the false case? What about the true case? This is why non-NIL is defined to be true.

Tail Recursion

Consider this very simple recursive definition of the factorial for integers:

(defun factorial (n) (cond ((equal n 0) 1) (T (* n (factorial (- n 1)))))) Tail Recursion can be used to allow the system to use only O(1) space. This is because the final recursive call is just substituted for the current call (there is no need to save state). Many lisp compilers/interpreters implement this optimization when they see it. Here is a new version of the factorial function: (defun factorial (n) (factorial-iterative 1 1 n)) (defun factorial-iterative (product counter max-count) (cond ((> counter max-count) product) (T (factorial-iterative (* counter product) (+ counter 1) max-count)))) What is this doing?

Although we told you that you shouldn't think about whether your program runs fast or not, this is a real problem in big lisp programs because many linear recursive processes could swamp available memory and leave you unable to perform your computation.

All this talk about Lisp being slow is mostly rhetoric, as we can see here. Lisp offers you the choice to do things the simple way or use a little brain power and do it the fast way.

Recursion Templates

Augmenting Recursion

Our first factorial example from before was an augmenting recursion: (defun factorial (n) (cond ((equal n 0) 1) (T (* n (factorial (- n 1)))))) This is type of recursion I usually think of first when trying to solve a problem. Somehow to me it seems to have the cleanest type of implementation.

This is called augmenting recursion because we are adding something to the result through each iteration. Further, we really aren't doing anything else in the way of computation. Can you see how to write a function to compute the length of a list this way?

List Consing Recursion
This is a special type of augmenting recursion where we add a cons cell at each step of the way. Here is an example: (defun laugh (number) (laugh-helper number 0 )) (defun laugh-helper (number target) (if (equal target number) nil (cons 'ha (laugh-helper number (+ target 1))))) And when you run it you get: > (laugh 2) (HA HA) > (laugh 0) NIL > (laugh 5) (HA HA HA HA HA) Note that although I used an IF conditional, it's still augmenting recursion. I find that this type of recursion tends to be most needed when you are constructing a data structure from "scratch" or based on some "specification" as we were above. Another common use is the transformation of one data-structure into another.

Kurt has an excellent example of list-consing recursion in his notes involving the function APPEND; click here to go to Kurt's lecture on recursion templates. A crucial thing you should pick up from this is that recursion postpones computation.

Conditional Augmenting Recursion
This is yet another version of the augmenting recursion template, mostly used in the transformation of one structure into another. Consider: (defun i-hate-zero (input-list) (cond ((null input-list) nil) ((= (car input-list) 0) (i-hate-zero (cdr input-list))) ( t (cons (car input-list) (i-hate-zero (cdr input-list)))))) This is effectively "selecting" all the zeroes and reconstructing the rest. The result of a "selection" here is a removal, but it might be replacement with some other value, etc.

Single Test Tail Recursion

This is the magic bullet that ups our performance and bounds our memory usage. Our improved version of the fibonacci is an example an example of this. (defun factorial (n) (factorial-iterative 1 1 n)) (defun factorial-iterative (product counter max-count) (cond ((> counter max-count) product) (T (factorial-iterative (* counter product) (+ counter 1) max-count)))) This is a fairly general type of computation, I really can't mention specific cases where I see it used more often.

Multiple Test Tail Recursion

This type of recursion is frequently used in searching some data structure for a value. This is not its only use, but this might be a clue.

Here is a double tail test recursive function:

(defun contains-number-p (the-list) (cond ((null the-list) nil) ((numberp (car the-list)) t) (t (contains-number-p (cdr the-list))))) Its our contains-number-p friend from before. Why is there no helper function needed here? Because the the-list is effectively all the temporaries we need, we don't need to introduce new ones.

Tree Recursion

This is probably the one that most people have problems with. I've stolen this example straight from Kurt's lecture on recursion templates if you would like to check out his discussion of it.

Here is an example of Tree recursion in action:

>(flatten '(a (b (c) d (e) f))) (A B C D E F)
  • This type of recursion has several names:
    Multiple Recursion
    This is because each call to flatten is replaced by multiple calls to flatten. (Think about the big-O of this!)
    Tree Recursion
    The structure we are flattening is really tree; the flatten is pre-order traversal.
    CAR/CDR recursion
    This is because we recursively call the flatten function on the car and cdr.
    Here is the function that makes it work: (defun flatten (my-list) (cond ((null my-list) nil) ((atom my-list) (list my-list)) (T (append (flatten (first my-list)) (flatten (rest my-list))))))


    Back To The CS2360 Home Page
    Ian Smith (iansmith@cc.gatech.edu)

    Last Modified 17 Jan 95 by Ian Smith (iansmith@cc.gatech.edu)