Let's step back for a second and take another look at traversals on regular tree structures. I'd like to make a few observations before we tackle traversals in the more complicated generative recursion situations.
Let's consider our venerable Binary Tree as a simple example:
;; BinTree is either ;; - MTBinTree ;; - NEBinTree ;; A MTBinTree is an empty structure ;; (make-MTBinTree) (define-struct MTBinTree ()) ;; A NEBinTree has a first, which is an any ;; and a left and right, which are BinTrees. ;;(make-NEBinTree any BinTree BinTree) (define-struct NEBinTree (first left right)) ;; An empty BinTree singleton (define MTBT (make-MTBinTree))
A depth-first traversal of a BinTree is all but trivial:
;; depthTraverse BinTree --> list-of-any ;; returns the elements of aBT in depth-first order. (define (depthTraverse aBT) (cond [(MTBinTree? aBT) empty] [(NEBinTree? aBT) (cons (NEBinTree-first aBT) (apply append (map depthTraverse (getNextLevel aBT))))]))
I've used a little utility function, getNextLevel, to get the left and right children for me, since I don't really care that this is a binary tree, after all:
;;getNextLevel: BinTree --> list-of-BinTree ;; Returns the BinTrees in the next level of the given BinTree (define (getNextLevel aBT) (cond [(MTBinTree? aBT) empty] [(NEBinTree? aBT) (list (NEBinTree-left aBT) (NEBinTree-right aBT))]))
We can see that a depth-first traversal examines all the child trees before it examines any sibling trees.
All very fine and dandy--nothing really new or earth-shaking here.
But now, let's consider a "breadth-first traversal" where we look at the elements in a tree in order from top to bottom, going across each "row" before going down to the next level. This is a very useful thing to do if you want to cover all possibilities in a tree whose depth may be too great to process.
The problem with a breadth-first traversal is that the tree is not defined recursively in terms of such a traversal. That is, a tree is recursively defined in terms of its child trees, not in terms of its sibling trees.
A breadth-first traversal examines all the firsts of the sibling trees before it examines any child trees.
So what we need is a mechanism to acquire all the sibling trees, which is the same as acquiring all the child trees of a given set of parent trees who are siblings.
Let's start simple: What are all the children of a single parent?
Our little utility function will do that for us: getNextLevel(aBinTree)
Next question: What are all the children of a set (list)of parent trees?
The union of all the children of all the parents. We can write another utility function for this:
;;getNextLevels: list-of-BinTree --> list-of-BinTree ;; Returns the BinTrees in the next level of all the given BinTrees (define (getNextLevels loBT) (apply append (map getNextLevel loBT)))
Notice how all of our code so far is immune to whether or not the BinTree is empty or not. This is not an accident.
Plan of attack:
No problem:
;;breadthTraverse: BinTree --> list-of-any ;; Returns the elements of aBT in breadth-first order. (define (breadthTraverse aBT) (cond [(MTBinTree? aBT) empty] [(NEBinTree? aBT) (cons (NEBinTree-first aBT) (breadthTraverse_helper (getNextLevel aBT)))]))
The helper is basically the same thing, except that it works on a list of BinTrees. The plan of attack is:
We'll need to make another utility function to get the firsts of the supplied list of trees (note that locals and lambdas could be used to shorten and encapsulate the code, but I'll write them as separate functions here for clarity's sake)..
;; breadthTraverse_helper: list-of-BinTree --> list-of-any ;; returns the elements of loBT in breadth-first order (define (breadthTraverse_helper loBT) (cond [(empty? loBT) empty] [(cons? loBT) (append (breadthTraverse_1level loBT) (breadthTraverse_helper (getNextLevels loBT)))])) ;;breadthTraverse_1level: list-of-BinTree --> list-of-any ;; returns the first elements of loBT as a list. (define (breadthTraverse_1level loBT) (apply append (map getFirst loBT))) ;; getFirst: BinTree --> list-of-any ;; utility function to return the first of aBT as a list. ;; returns empty if aBT is an MTBinTree (define (getFirst aBT) (cond [(MTBinTree? aBT) empty] [(NEBinTree? aBT) (list (NEBinTree-first aBT))]))
Why does the getFirst function need to check for an MTBinTree? Why could I just use NEBinTree-first?
The list-of-BinTree that it works on may contain either MTBinTrees or NEBinTrees.
This breadth-first traversal works just fine. But our real point here is about getting prepared for generative recursion situations. So let's compare our breadth-first approach, which should be a useful technique for generative recursion, against the the general generative recursion template:
(define (f args …) (cond [(trivial? args …) (solve-trivial args …)] [else (combine (f (generate-subproblem1 args …) …) … (f (generate-subproblemn args …) …))]))
Let's look at the helper function first, as it is really a more general case than the main function.
Breadth-first helper
|
Generative recursion template
|
loBT
|
args
|
empty?
|
trivial?
|
empty
|
solve-trivial
|
append
|
combine
|
(breadthTraverse_1level
loBT)
|
(f (generate-subproblem1
args …)
|
(breadthTraverse_helper
(getNextLevels loBT))) |
(f (generate-subproblem2
args …)
|
What we see is that a breadth-first traversal fits the template for generative recursion.
Exercise for home: Prove to yourself that the main function also follows the generative recursion template
The lessons here:
Note that divide-and-conquer generative recursion can simply be thought of as a "natural" recursion process, which is a depth-first traversal.
A generative recursion example:
Suppose we wanted to build a random (randomly build a) binary tree. We can do it by instantiating either an MTBinTree or a NEBinTree, in which case, we put a random number in first and with randomly generated left and right child trees. How would you characterize this process in terms of breadth-first vs. depth-first? Is this a divide-and-conquer algorithm?
This is a divide-and-conquer algorithm which makes it depth-first.
Download the traversal code here: breadthfirst.scm (includes fun code to display a BinTree and generate a random BinTree!)
Consider the problem of trying to figure out a good route from Houston to Billings, MT (near Yellowstone!), given that no airlines fly directly:
Existing connections:
Houston to:
|
Dallas to:
|
Denver to:
|
Seattle to:
|
Chicago to:
|
Billings to:
|
We can see that if we draw this out, we don't get a regular tree-like structure, we get a graph instead:
Here are the wrench-in-the-works:
What does this mean for us?
The last issue is going to require that we take a slight diversion to learn about a new concept: mutation
©2003 Stephen Wong