Take, for example, the first tree: 1.1) tree->list-1 [7 [3 [9 [] [1 [] []] [5 [] []]] [11 [] []]]] 2.1) tree->list-1 [3 [1 [] []] [5 [] []]] <--- first left subtree 3.1) tree->list-1 [1 [] []] <--- left subtree of the first left subtree 4.1) tree->list-1 [] 3.2) append [] 1 [] -> [1] 3.3) tree->list-1 [5 [] []] <--- right subtree of the first left subtree 4.2) tree->list-1 [] 3.4) append [] 5 [] -> [5] 2.2) append [1] 3 [5] -> [1 3 5] 2.3) tree->list-1 [9 [] [11 [] []]] <--- first right subtree 3.5) tree->list-1 [] -> [] 3.6) tree->list-1 [11 [] []] -> [11] <--- right subtree of the first right subtree 3.7) append [] 9 [11] -> [9 11] 2.4) append [1 3 5] 7 [9 11] -> [1 3 5 7 9 11] <--- result 1.1) copy-to-list [7 () [3 [9 [] [1 [] []] [5 [] []]] [11 [] []]]] 2.1) copy-to-list [9 [] [11 [] []]] () 3.1) copy-to-list [11 [] []] () 4.1) cons 11 () -> (11) 3.2) cons 9 (11) -> (9 11) 2.2) cons 7 (9 11) -> (7 9 11) 3.4) copy-to-list [3 [1 [] []] [5 [] []]] (7 9 11) 4.2) copy-to-list [5 [] []] (7 9 11) -> (5 7 9 11) 4.3) cons 3 (5 7 9 11) -> (3 5 7 9 11) 4.4) copy-to-list [1 [] []] (3 5 7 9 11) -> (1 3 5 7 9 11) <--- result 2.2) unwind the stack We can see that both functions produce equal results for all of the trees specified in the book, however, the biggest difference between the functions is in the way they gather the final results: the second function gathers the result sequentially into the resulting list, while the first one divides the task by subtrees and joins the results of sub-tasks together. In the second function the process first goes as deep as it can into the rightmost subtree and then gathers the left subtree. In the first function both subtrees are flattened simultaneously (which is a nice property for parallelization, we wouldn't be able to parallelize the second function as easily as we could the first one). Note that both functions produce sorted lists, which means that the results of applying functions will be equal for all binary trees. | |
First functionWell, let's analyze the first one. First of all, the height of a balanced
tree is at most log2(n+1) where n = number of elements in the tree.
Each recursion level reduces the height by one and spawns another recursive
process which works with the left or right subtree. The operation taking
actual time in the first function is The tree doesn't start with O(n) because we append to the left subtree in the topmost recursion level and only need to iterate over the result of flattening the left subtree (which is at most n/2-1 elements). The height of the tree is at most log2(n+1), then T(n) = O(n/2) + 2O(n/4) + 4O(n/8) + ... = log2(n+1) * O(n/2) = O(n*log2(n)) Second functionThis one doesn't do append so the operation performed at each recursion level is just O(1). We still visit each node in the tree, so the order of growth is n*O(1) = O(n). | |
As this process is performed recursively on both each half of the argument list, the resulting tree should be balanced. Running the function on (1 3 5 7 9 11) results in ([5 [1 () [3 () ()]] [9 [7 () ()] [11 () ()]]]) b) The order of growth is O(n) as the below function must visit each element of the argument list and perform a cons to make it a node in the resulting tree. | |
However, sorting the list is minimum n*logn so this solution won't cut it. We must use the fact that the tree->list function produces a sorted list (set), so we need to get a union/intersection of 2 sorted sets in O(n) - which we already did in exercises 2.61-62. | |