Trees

A list that may have other lists as elements, which in turn may have still other lists as elements, and so on, is sometimes metaphorically called a tree. (The elements of the list are analogous to the branches of a tree, each of which may have branches of its own, and so on.) One way of thinking about deep recursion is to compare it to the process of climbing up one branch of a tree, or sometimes climbing around in it until one has inspected the leaves at the ends of all of the branches.

Here's a tree in which all of the leaves are strings:

(("The" ("busy" "clerk")) ("took" ("the" "key" ("to" ("the" "safe")))))

The tree structure is more apparent if one breaks out each list into its elements, with connecting lines:

(("The" ("busy" "clerk")) ("took" ("the" "key" ("to" ("the" "safe")))))
                                |
              .----------------' `----------------.
              |                                   |
  ("The" ("busy" "clerk"))   ("took" ("the" "key" ("to" ("the" "safe"))))
              |                                   |
   .---------' `----.            .---------------' `----------.
   |                |            |                            |
 "The"      ("busy" "clerk")   "took"    ("the" "key" ("to" ("the" "safe")))
                    |                                         |
              .----' `----.                  .------.--------' `--.
              |           |                  |      |             |
           "busy"      "clerk"             "the"  "key" ("to" ("the" "safe"))
                                                                  |
                                                         .-------' `--.
                                                         |            |
                                                       "to"   ("the" "safe")
                                                                      |
                                                                 .---' `--.
                                                                 |        |
                                                               "the"   "safe"

In this case, the nested lists are supposed to indicate the syntactic structure of the sentence by grouping together words and phrases that are syntactically related (e.g., ``busy clerk'' is a syntactic unit, a phrase, while ``took the'' is not).

Here's a procedure that takes a tree with strings as leaves, like the one shown above, and returns a long string constructed by inserting a space between successive leaves. It assumes that the tree is not the empty list.

(define leaf-string
    (lambda (tree)
      (cond ((string? (car tree))
             (if (null? (cdr tree))
                 (car tree)
                 (string-append (car tree) " " (leaf-string (cdr tree)))))
            ((null? (cdr tree))
             (leaf-string (car tree)))
            (else
             (string-append (leaf-string (car tree)) " "
                            (leaf-string (cdr tree)))))))

Or in English: If the first element of the list is a string, then either return it unchanged (if there are no more elements) or append a space to the end of it and after that the string formed by the leaves on the rest of the list. Otherwise, the first element of the list is itself a list; apply leaf-string to it recursively, and either return the result (if there are no more elements) or, once again, append a space at the end and after that the string formed by the leaves on the rest of the list. Here's what a sample call to the procedure looks like:

(leaf-string '(("The" ("busy" "clerk"))
               ("took" ("the" "key" ("to" ("the" "safe"))))))
===> "The busy clerk took the key to the safe"

  1. The leaf-string procedure has a precondition: Its argument must be a list containing at least one element, and each element must be either a string or a list that meets the same precondition. Write and test a predicate string-tree? that determines whether a given object meets this condition.

    (string-tree? '()) ===> #f
    (string-tree? '("This" "one" "is" "OK")) ===> #t
    (string-tree? '((("This" "one") ("is" "OK")) "too") ===> #t
    (string-tree? '(("This" "one") ("is" ("just" (2 "bogus"))))) ===> #f
    (string-tree? '(symbols not strings)) ===> #f
    
  2. Insert a precondition test in the definition of leaf-string so that it will report an error if it is called with an argument that does not meet the precondition. (To do this right, use two procedures -- husk and kernel -- so that the precondition is checked only once, not on each recursive call.)

Another common kind of operation on trees is leaf transformation -- preserving the structure of nested lists, but replacing each leaf with a transformed version of itself or with some derived datum. For example, one might replace every string in a tree of strings with its length:

(convert-strings-to-lengths '(("The" ("busy" "clerk"))
                              ("took" ("the" "key" ("to" ("the" "safe"))))))
===> ((3 (4 5)) (4 (3 3 (2 (3 4)))))

Here's the definition of the procedure that performs this leaf transformation:

(define convert-strings-to-lengths
  (lambda (tree)
    (cond ((null? tree) '())
          ((string? (car tree))
           (cons (string-length (car tree))
                 (convert-strings-to-lengths (cdr tree))))
          (else
           (cons (convert-strings-to-lengths (car tree))
                 (convert-strings-to-lengths (cdr tree)))))))

In English: If the list contains no elements, return the empty list. Otherwise, if the first element is a string, find the length of that string and cons it onto the front of the structure that results from a recursive call to deal with the rest of the list. Otherwise, the first element must be a pair; issue one recursive call to deal with that first element and another to deal with the rest of the list, then use cons to unite the results.

  1. Write and test a leaf-transformation procedure increment-every-leaf that takes a tree of integers as its only argument and returns a similarly structured tree in which each of the leaves has been increased by 1:

    (increment-every-leaf '((3 (4 5)) (4 (3 3 (2 (3 4))))))
    ===> ((4 (5 6)) (5 (4 4 (3 (4 5)))))
    (increment-every-leaf '()) ===> '()
    (increment-every-leaf '(79 -1 86 (-2 -3) 4)) ===> (80 0 87 (-1 -2) 5)
    

This document is available on the World Wide Web as

http://www.math.grin.edu/courses/Scheme/trees.html

created February 26, 1997
last revised May 28, 1997
John David Stone (stone@math.grin.edu)