在 Haskell 中使用尾递归拆分 BinTree
Splitting a BinTree with tail recursion in Haskell
所以这周我们在 Haskell 中学习了联合类型、尾递归和二叉树。我们这样定义树数据类型:
data BinTree a = Empty
| Node (BinTree a) a (BinTree a)
deriving (Eq, Show)
leaf :: a -> BinTree a
leaf x = Node Empty x Empty
现在我们被要求编写一个函数来找到最左边的节点,return它,将其剪掉,然后 return 没有我们刚刚剪掉的节点的剩余树。
我们做了类似的事情,效果很好:
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)
现在我需要使这个函数尾递归。我想我了解尾递归是什么,但发现很难将它应用于这个问题。有人告诉我写一个函数,用合适的参数调用主函数,但仍然无法解决这个问题。
由于节点没有父节点link,一种方法是在列表中维护根到叶的路径。最后可以使用左折叠构造修改后的树:
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
在这里,为了不破坏任何东西,这里有一些 "tail recursive" 用于沿左右分支求和的函数定义,至少据我所知 "tail recursion":
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
- 参数不断地变得越来越好,直到它们处于理想状态, 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
,但是和loop result Empty
是一样的——到了时候,就是result
.我尝试了几次才使 loop ? ?
缺少的参数正确,但是,像往常一样,一旦我得到它们,它们就很明显了。
正如其他人所暗示的那样,在 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 #)
那些看起来很有趣的 (# ..., ... #)
东西是 未装箱的对 ,它们的处理方式与多个 return 值非常相似。特别是,直到最后才分配实际的元组构造函数。通过认识到每次使用非空树调用 splitleftmost
都会产生 Just
结果,我们(因此几乎可以肯定 GHC)可以将空情况与其余情况分开以避免分配中间 Just
构造函数。所以这个最终代码只分配堆栈帧来处理递归结果。由于 一些 这种堆栈的表示对于解决这个问题本质上是必要的,使用 GHC 的内置的似乎很可能会给出最好的结果。
所以这周我们在 Haskell 中学习了联合类型、尾递归和二叉树。我们这样定义树数据类型:
data BinTree a = Empty
| Node (BinTree a) a (BinTree a)
deriving (Eq, Show)
leaf :: a -> BinTree a
leaf x = Node Empty x Empty
现在我们被要求编写一个函数来找到最左边的节点,return它,将其剪掉,然后 return 没有我们刚刚剪掉的节点的剩余树。
我们做了类似的事情,效果很好:
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)
现在我需要使这个函数尾递归。我想我了解尾递归是什么,但发现很难将它应用于这个问题。有人告诉我写一个函数,用合适的参数调用主函数,但仍然无法解决这个问题。
由于节点没有父节点link,一种方法是在列表中维护根到叶的路径。最后可以使用左折叠构造修改后的树:
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
在这里,为了不破坏任何东西,这里有一些 "tail recursive" 用于沿左右分支求和的函数定义,至少据我所知 "tail recursion":
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
- 参数不断地变得越来越好,直到它们处于理想状态, 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
,但是和loop result Empty
是一样的——到了时候,就是result
.我尝试了几次才使 loop ? ?
缺少的参数正确,但是,像往常一样,一旦我得到它们,它们就很明显了。
正如其他人所暗示的那样,在 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 #)
那些看起来很有趣的 (# ..., ... #)
东西是 未装箱的对 ,它们的处理方式与多个 return 值非常相似。特别是,直到最后才分配实际的元组构造函数。通过认识到每次使用非空树调用 splitleftmost
都会产生 Just
结果,我们(因此几乎可以肯定 GHC)可以将空情况与其余情况分开以避免分配中间 Just
构造函数。所以这个最终代码只分配堆栈帧来处理递归结果。由于 一些 这种堆栈的表示对于解决这个问题本质上是必要的,使用 GHC 的内置的似乎很可能会给出最好的结果。