在Haskell中使用尾递归拆分BinTree

时间:2016-06-05 16:47:26

标签: haskell recursion binary-tree tail-recursion

所以本周我们在Haskell中了解了联合类型,尾递归和二叉树。我们定义了我们的树数据类型:

data BinTree a = Empty
           | Node (BinTree a) a (BinTree a)
           deriving (Eq, Show)

leaf :: a -> BinTree a
leaf x = Node Empty x Empty

现在我们被要求编写一个函数来查找最左边的节点,返回它,将其剪切掉,并返回剩余的树而不用我们刚切割的节点。

我们做了类似的事情,效果很好:

splitleftmost :: BinTree a -> Maybe (a, BinTree a)
splitleftmost Empty = Nothing
splitleftmost (Node l a r) = case splitleftmost l of
                                 Nothing -> Just (a, r)
                                 Just (a',l') -> Just (a', Node l' a r)

现在我需要使这个函数尾递归。我想我理解尾递归是什么,但发现很难将它应用于这个问题。我被告知编写一个函数,它使用拟合参数调用main函数,但仍然无法解决这个问题。

3 个答案:

答案 0 :(得分:4)

由于节点没有父链接,因此一种方法是在列表中维护root-to-leaf路径。最后,可以使用左折叠来构造修改后的树:

slm :: BinTree a -> Maybe (a, BinTree a)
slm = run []
    where
    run _ Empty = Nothing
    run t (Node Empty x r) = Just (x, foldl go r t)
        where go l (Node _ x r) = Node l x r

    run t n@(Node l _ _) = run (n:t) l

答案 1 :(得分:2)

在这里,不要破坏任何东西,是一些"尾递归"用于沿左右分支求和的函数的定义,至少我理解"尾递归":

sumLeftBranch tree = loop 0 tree where
  loop n Empty        = n
  loop n (Node l a r) = loop (n+a) l

sumRightBranch tree = loop 0 tree where
  loop n Empty        = n
  loop n (Node l a r) = loop (n+a) r

你可以看到循环的所有递归用法都与第一次调用loop 0 tree具有相同的答案 - 参数只是保持更好和更好的形状,直到它们处于理想的形状,{{ 1}},即loop n Empty,所需的总和。

如果这是需要的东西,n的设置将是

splitleftmost

此处,splitLeftMost tree = loop Nothing tree where loop m Empty = m loop Nothing (Node l a r) = loop ? ? loop (Just (a',r')) (Node l a r) = loop ? ? 的首次使用采用loop的形式,但与loop Nothing tree相同 - 当我们来到它时,{{1} }}。我花了几次尝试才把正确的loop result Empty错过的论点弄清楚,但是,像往常一样,一旦我得到它们就很明显了。

答案 2 :(得分:2)

正如其他人所暗示的那样,在Haskell中没有理由使这个函数尾递归。事实上,尾递归解决方案几乎肯定会慢于你设计的解决方案!您提供的代码中的主要潜在效率低下涉及配对和Just构造函数的分配。我相信GHC(启用优化)将能够弄清楚如何避免这些。我的猜测是它的最终代码可能看起来像这样:

splitleftmost :: BinTree a -> Maybe (a, BinTree a)
splitleftmost Empty = Nothing
splitleftmost (Node l a r) =
  case slm l a r of
    (# hd, tl #) -> Just (hd, tl)

slm :: BinTree a -> a -> BinTree a
    -> (# a, BinTree a #)
slm Empty a r = (# a, r #)
slm (Node ll la lr) a r =
  case slm ll la lr of
    (# hd, tl' #) -> (# hd, Node tl' a r #)

那些看起来很滑稽的(# ..., ... #)内容是未装箱的对,它们的处理方式与多个返回值非常相似。特别是,直到结束才分配实际的元组构造函数。通过认识到splitleftmost对非空树的每次调用都会产生Just结果,我们(因此几乎可以肯定GHC)可以将空案例与其余案例分开,以避免分配中间{{1构造函数。所以这个最终代码只分配堆栈帧来处理递归结果。由于这种堆栈的某些表示本质上是解决这个问题的必要条件,因此使用GHC的内置函数似乎很可能会产生最佳结果。