Rice University

COMP 210: Principles of Computing and Programming

Lecture #25       Fall 2003

Generative Recursion

So far, we've always done "structural recursion" where the recursive process is dictated by the structural nature of the data, independent of its values.

But does this cover all possibilities?

It turns out that many problems can be thought of and processed as if they were recursively constructed, even though that recursive nature does not manifest itself in the data's actual structure.

For instance, one could model a recursive process based on the values that were held in the data:

Consider the following list of numbers:

50 25 75 10 30 60 80

There doesn't seem to much rhyme or reason to the order of the numbers unless I re-write them like this, where all I've done is to put some newlines and spaces in:

         50
   25         75
10    30   60    80

Notice that the left child is less than the root data and the right child is greater than the root data (B.T.W. this is called a "binary search tree").

The point here is that the list of numbers and the tree are equivalent representations of the same thing. But they are quite different in the ease in which particular things can be calculated. It's easier to sum all the elements in a list, but it's easier to find an element in a tree.

Sometimes it's not so obvious. Consider the problem of trying to find a way to get to Rt 59 from Rice. All the various streets are the data, and we can think of the intersections as forming relationships between the data. We get a rather complex set of connections called a "graph". For example, here is a portion of the graph (map) of the streets near Rice and how they connect to Rt 59.

              Bissonnet -- San Jacinto
                  |                |
Rice Ave -- Main Street -- Rt 59 N feeder---Rt 59 N  
   |              |         |
   |--------------+-----  Kirby 
   |              |         |          
   |-Shepherd -- Rt 59 S feeder ---Rt 59 S

But we can represent this as a tree as well:

      ___________________________Rice Ave_________________________________
     |                               |                                    |
  Shepherd               _________Main St.__________                  ___Kirby_____
     |                  |            |              |                |             |
Rt 59 S feed      Rt 59 S feed    Bissonet     Rt 59 N feed   Rt 59 S feed   Rt 59 N feed
    |                   |            |              |                |             |
Rt 59 S              Rt 59 S     San Jacinto     Rt 59 N          Rt 59 S       Rt 59 N
                                     |
                                  Rt 59 N 

Note that the same data appears more than once in the tree. However, as a tree, the situation can be much easier to process.

One, of many, ways to think about generative recursion is that, given a set of values, there already exists in that set of values, some sort of tree or list structure that will make it easy (or easier) to compute our required results. The only problem is that we don't know a priori what that structure is. So, how can we write a program that will traverse an structure of unknown topology?

Divide-and-Conquer

One of the most common non-structural recursions is that of divide-and-conquer. The premise of a divide-and-conquer algorithm is simple:

  1. Is the problem trivially solvable?
    1. Yes: Done!
    2. No: Not done:
      1. "Divide" up (split) the problem into smaller sub-problems.
      2. "Conquer" (solve) each of the sub-problems--the recursive call.
      3. Join the sub-solutions together to create a single solution to the problem.

A Divide-and-Conquer algorithm thus breaks down to specifying only two related functions: split and join plus one to determine if the trivial solution. who have the following abstract contracts:

split: list-of-any any2--> (list-of-list-of-any)

join: list-of-list-of-any any2 --> (list-of-any)

Above, any2 is for any sort of input parameter split and join might need.

Divide-and-Conquer algorithms thus have this basic code structure (in pseudo-code here) (Note: list-of-any could also be tree-of-any):

;;DaQSolver: a structure of the required functions to do a divide-and-conquer algorigthm
;; (make-DaQSolver (list-of-any -->boolean) 
;;                 (list-of-any --> list-of-list-of-any) 
;;                 (list-of-list-of-any --> list-of-any);;
(define-struct DaQSolver (trivial? split join))

;;DaQFunc: list-of-any  DaQSolver any2 --> any3
;; Performs a divide-and-conquer algorithm on the input data using the supplied
;; DaQSolver and parameter. 
(define (DaQFunc data solver param)
  (cond
    [((DaQSolver-trivial? solver) data) trivial-soln]  ;; solution is trivial
    [else ((DaQSolver-join solver)              ;; join solved sub-parts
           (map (lambda (x) (DaQFunc x solver param))  ;; solve sub-parts
                ((Solver-split solver) data param))    ;; split into sub-parts
           param))

 

Caveat: A real divide-and-conquer function may not look exactly like the above due to how the trivial? function works. For instance, it may dispatch directly to the split and join functions rather than returning a boolean.

We've seen this before, haven't we? ===> Sorters!!

For sorting, the trivial case is always when there is only zero or one element in the list, so only the split and join need to be specified. Here is a situation where the trivial? function dispatches directly to the split-join processing instead of returning a boolean and then dispatching via a cond statement.

Let's take a look at a couple of new sorters:

Selection Sort

This is a hard split/easy join.

Here's what the selection sort does to a list of data. The unsorted list that is being split is in red. The pair of lists being joined is in blue.

start: (4 2 6 1 5 3 7)
split: (1) (4 2 6 5 3 7)
split: (1) (2) (4 6 5 3 7)
split: (1) (2) (3) (4 6 5 7)
split: (1) (2) (3) (4) (6 5 7)
split: (1) (2) (3) (4) (5) (6 7)
split: (1) (2) (3) (4) (5) (6) (7)
join:  (1) (2) (3) (4) (5) (6 7)
join:  (1) (2) (3) (4) (5 6 7)
join:  (1) (2) (3) (4 5 6 7)
join:  (1) (2) (3 4 5 6 7)
join:  (1) (2 3 4 5 6 7)
join:  (1 2 3 4 5 6 7)

 

QuickSort Sort

This is one of the fastest sorting algorithms available.

This is a hard split/easy join.

Here's what the quicksort does to a list of data. The unsorted list that is being split is in red. The pair of lists being joined are in blue, purple, and green.

Notice how many fewer steps it takes than selection sort.

The sorter code here has been modified from the homework code to include the use of visitors to lists and the map function: sorter.scm

 

 

©2003 Stephen Wong