2.63 Exercise 2.63
We’re comparing the following procedures for converting binaries trees into lists:
(define (tree->list-1 tree) (if (null? tree) '() (append (tree->list-1 (left-branch tree)) (cons (entry tree) (tree->list-1 (right-branch tree))))))
(define (tree->list-2 tree) (define (copy-to-list tree result-list) (if (null? tree) result-list (copy-to-list (left-branch tree) (cons (entry tree) (copy-to-list (right-branch tree) result-list))))) (copy-to-list tree '()))
The first procedure appends the list representation of the left-branch of the tree to the list formed by consing the entry of the tree with the list representation of the right-branch of the tree. This produces a list with elements sorted in ascending order from left to right. (The base case, where the tree is empty, is to return an empty list.)
The second procedure defines an inner procedure, copy-to-list, taking a tree and a list as parameters. The base case of an empty tree is the same; however, rather than returning an empty list, the given result-list argument is returned. Otherwise, it calls copy-to-list, with the first argument being the left-branch of the tree and the second being the list formed by consing the entry of the tree and the list representation of the right-branch of the tree. To create the second argument, copy-to-list is called with the right-branch of the tree and the result-list from the outer copy-to-list scope supplied as arguments.
The arguments to cons suggest that the resulting list will also be in ascending order. The two procedures visit the nodes in the same order – they just accumulate their answers in a different way. The first procedure is arguably more straightforward, by just appending results of recursive calls in a predictable order. The second procedure, however, first accumulates new results in the innermost copy-to-list call, and then uses that with cons to add the value of the current node, and then uses this with an outer copy-to-list as a result-list when working through the left side of the tree. The end result is the same, but the pipelining of data is more explicit.
We can use the trees in from figure 2.16 to verify that these procedures produce the same results:
(define ta (list 7 (list 3 (list 1 '() '()) (list 5 '() '())) (list 9 '() (list 11 '() '())))) (define tb (list 3 (list 1 '() '()) (list 7 (list 5 '() '()) (list 9 '() (list 11 '() '()))))) (define tc (list 5 (list 3 (list 1 '() '()) '()) (list 9 (list 7 '() '()) (list 11 '() '())))) (tree->list-1 ta) (tree->list-2 ta) (tree->list-1 tb) (tree->list-2 tb) (tree->list-1 tc) (tree->list-2 tc)
However, although these procedures produce the same results, they do not have the same order of growth. The second procedure, visiting every node in the tree once and applying cons once per visit, is clearly O(N).
However, the first procedure calls append when visiting each node. Recall from earlier in the chapter that append is an O(N) operation. We can formalize the running time of the first procedure as a recurrence relation:
T(n) = O(n) + 2T(n/2) |
This has a solution of O(nlogn).