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 SBut 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 NNote 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?
One of the most common non-structural recursions is that of divide-and-conquer. The premise of a divide-and-conquer algorithm is simple:
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] [else ((DaQSolver-join solver) (map (lambda (x) (DaQFunc x param)) ((Solver-split) data param)) 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:
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)
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.
start: (4 2 6 1 5 3 7) split: (2 1 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)
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
©2002 Stephen Wong