我是否正确使用尾递归实现了顺序级别顺序树横向?
inorder (Leaf n) temp = n:temp
inorder (Node (n, left, right)) temp = inorder left (n:inorder right temp)
inorder :: Tree a -> [a] -> [a]
树被声明为
data Tree a = Leaf a | Node (a, Tree a, Tree a) deriving Show
并返回
[2,1,3]
随叫随到inorder three []
答案 0 :(得分:7)
这在技术上不是尾递归的,因为你在非尾部位置有一个递归调用inorder right temp
。解决这个问题的一种方法是延续。你编写了一个像以前一样使用累加器的函数,但是累加器只是一个列表,它实际上是一个函数,表示在计算中要做的工作。这意味着我们总是可以尾调用而不是进行非尾调用而只返回,因为我们需要的上下文被保存到延续中。
inorder = go id
where go :: ([a] -> r) -> Tree a -> r
go k Leaf = k []
go k (Node a l r) = go l (\ls -> go r (\rs -> k $ ls ++ n : rs))
这里的每个调用都是必需的尾调用,但是效率非常低,因为它需要在每个级别进行++
操作,从而将我们推向二次成本。更有效的算法将避免构建显式列表,而是建立差异列表,延迟具体结构的构造并提供更有效的算法
type Diff a = [a] -> [a] -- A difference list is just a function
nil :: Diff a
nil xs = xs
cons :: a -> Diff a -> Diff a
cons a d = (:) a . d
append :: Diff a -> Diff a -> Diff a
append xs ys = xs . ys
toList :: Diff a -> a
toList xs = xs []
请注意,所有这些操作都是O(1)
,但条目数为toList
的{{1}}除外。这里重点是差异列表便宜且易于附加,因此我们将在算法中构造这些并在最后构建具体列表
O(n)
现在,通过无偿应用功能,我们获得了一个完全独一无二的Haskell程序。你在Haskell中看到我们并不真正关心尾调用,因为我们通常想要正确地处理无限结构,如果我们要求所有东西都是尾递归的话,这是不可能的。事实上,我会说虽然不是尾递归,但你最初拥有的代码是最惯用的,甚至是它在 inorder = go toList
where go :: (Diff a -> r) -> Tree a -> r
go k Leaf = k nil
go k (Node a l r) =
go l (\ls -> go r (\rs -> k $ ls `append` cons n rs))
中的实现方式!它具有我们可以懒惰地消耗Data.Set
的结果的属性,它将与我们一起工作并懒惰地处理树。所以在你的实现中,像
toList
将非常接近于如何以手工效率明智地实施它!它不会像我的版本那样构造遍历整个树。这种懒惰的组合效果在真正的Haskell代码中比在语法上使得我们的代码仅使用尾调用(这实际上无法保证空间使用)。