This submission to Programming Praxis给出一个O(n)函数,它“撤消”二进制搜索树的前序遍历,将列表转换回树。提供缺失的数据声明:
data Tree a = Leaf | Branch {value::a, left::Tree a, right:: Tree a} deriving (Eq, Show) fromPreOrder :: Ord a => [a] -> Tree a fromPreOrder [] = Leaf fromPreOrder (a:as) = Branch a l (fromPreOrder bs) where (l,bs) = lessThan a as lessThan n [] = (Leaf,[]) lessThan n all@(a:as) | a >= n = (Leaf,all) | otherwise = (Branch a l r,cs) where (l,bs) = lessThan a as (r,cs) = lessThan n bs
很明显,在每个递归步骤中将一个构造函数添加到树中,这是其效率的关键。
唯一的“问题”是列表是手动穿过计算的,这不是一种非常糟糕的Haskellian方式,并且让它更难以看到它实际上是在单线程中逐个元素消耗的方式。
我试图使用状态monad(prettified on Codepad)来纠正这个问题:
import Control.Monad.State data Tree a = Leaf | Branch {root::a, left::Tree a, right::Tree a} deriving (Eq,Show) peek = State peek' where peek' [] = (Nothing,[]) peek' a@(x:_) = (Just x,a) pop = State pop' where pop' [] = error "Tried to read past the end of the list" pop' (_:xs) = ((),xs) prebuild'::Ord a => State [a] (Tree a) prebuild' = do next <- peek case next of Nothing -> return Leaf Just x -> do pop leftpart <- lessThan x rightpart <- prebuild' return (Branch x leftpart rightpart) lessThan n = do next <- peek case next of Nothing -> return Leaf Just x -> do if x < n then do pop leftpart <- lessThan x rightpart <- lessThan n return (Branch x leftpart rightpart) else return Leaf prebuild::Ord a => [a] -> Tree a prebuild = evalState prebuild'
不幸的是,这只是看起来很混乱,似乎没有任何理由更容易理解。
有人认为我还没有能够到达任何地方(部分原因是因为我对基础概念的理解不够深入,很可能):我可以在列表中使用左侧折叠构建一个最终产生树的延续?这可能吗?还有什么不是疯了吗?
另一个想法就是把它写成一棵树展开,但我认为不可能有效地做到这一点;列表最终会被遍历太多次,程序将为O(n ^ 2)。
从另一个方向拿东西,我有一种唠叨的怀疑,即可能想出一个算法,该算法首先将列表分成增加的段和减少段,但我还没有发现具体的事情要做有这个想法。
答案 0 :(得分:4)
我认为您使用State
时遇到的问题是您的原语(push
,pop
,peek
)不正确。我认为更好的是像available_
,它会检查堆栈的前端是否与特定条件匹配,并在每种情况下执行不同的操作:
available_ p f m = do
s <- get
case s of
x:xs | p x -> put xs >> f x
_ -> m
实际上,在我们的用例中,我们可以专注一点:当我们的堆栈的头部不满足条件时,我们总是希望返回Leaf
,并且我们总是希望在何时递归确实如此。
available p m = available_ p
(\x -> liftM2 (Branch x) (lessThan' x) m)
(return Leaf)
(你也可以写available
开始并完全跳过available_
。在我的第一次迭代中,这就是我所做的。)现在写fromPreOrder
和{{1}很快,我想也能对他们的行为有所了解。我会用素数命名它们,以便我们可以仔细检查它们是否使用QuickCheck做正确的事情。
lessThan
在ghci:
fromPreOrder' = available (const True) fromPreOrder'
lessThan' n = available (<n) (lessThan' n)
答案 1 :(得分:3)
虽然我无法回答关于继续传递的问题,但我相信基于State
monad的实现可以更清楚地编写。首先,我们可以使用来自Control.Applicative
的符号方便性,以便于阅读。其次,我们可以升级效果堆栈以包含Maybe
,以便捕获失败的概念(a)从空列表的头部开始,以及(b)从(a >= n)
比较作为效果。
import Control.Monad.State
import Control.Applicative
最终代码使用回溯状态monad变换器堆栈。这意味着我们在State
周围Maybe
而不是Maybe
周围State
。在某种意义上,我们可以认为这意味着失败是“主要”效应。在实践中,这意味着如果算法失败,则无法继续使用潜在的不良状态,因此必须回溯到最后一个已知的良好状态。
type Preord a b = StateT [a] Maybe b
由于我们不断采用列表的头部并希望正确捕获该失败,因此我们将使用“安全头”功能(无论如何都是列表的自然析构函数,尽管不在基本的Haskell库中)
-- Safe list destructor
uncons :: [a] -> Maybe (a, [a])
uncons [] = Nothing
uncons (a:as) = Just (a, as)
如果我们巧妙地看待它,我们会注意到这已经完全我们的monadic计算形式(StateT [a] Maybe b
与[a] -> Maybe (b, [a])
同构)。当我们进入Monad
时,我们会给它一个更令人回味的名字。
-- Try to get the head or fail
getHead :: Preord a a
getHead = StateT uncons
此算法的一个共同特征是通过提供默认值来停止本地故障。我将在certain
组合器
-- Provides a default value for a failing computation
certain :: b -> Preord a b -> Preord a b
certain def p = p <|> return def
现在我们可以在Preord
monad中非常干净地编写最终算法。
fromPreOrder :: Ord a => Preord a (Tree a)
fromPreOrder = certain Leaf $ do
a <- getHead
Branch a <$> lessThan a <*> fromPreOrder
lessThan :: Ord a => a -> Preord a (Tree a)
lessThan n = certain Leaf $ do
a <- getHead
guard (a < n)
Branch a <$> lessThan a <*> lessThan n
请注意,Applicative
样式有助于表明我们正在使用进一步有效(状态消耗)计算构建Branch
构造函数的组件。当枢轴已经是预订遍历中的最少元素时,guard
短路lessThan
。我们还明确了解fromPreOrder
和lessThan
如何无法计算出更好的结果时默认为Leaf
。
(另请注意,fromPreOrder
和lessThan
现在几乎相同,这是Daniel Wagner在撰写available
时在自己的答案中利用的共性。)
我们最终想要隐藏所有的monadic噪音,因为对于外部用户来说,这只是一个纯粹的算法。
rebuildTree :: [a] -> Tree a
rebuildTree = fromMaybe Leaf . runStateT fromPreOrder
要获得完整的图片,这里是仅使用State
monad的算法实现。注意处理失败的所有额外噪音!我们将整个popElse
函数吸收到了回溯状态monad的效果中。我们还将if
提升为失败效应。没有那个效果堆栈,我们的组合器非常特定于应用程序,而不是在其他地方进行解相关和有用。
-- Try to take the head of the state list and return the default
-- if that's not possible.
popElse :: b -> (a -> State [a] b) -> State [a] b
popElse def go = do
x <- get
case x of
[] -> return def
(a:as) -> put as >> go a
push :: a -> State [a] ()
push a = modify (a:)
fromPreOrder :: Ord a => State [a] (Tree a)
fromPreOrder = popElse Leaf $ \a -> Branch a <$> lessThan a <*> fromPreOrder
lessThan :: Ord a => a -> State [a] (Tree a)
lessThan n =
popElse Leaf $ \a ->
if a >= n
then push a >> return Leaf
else Branch a <$> lessThan a <*> lessThan n
答案 2 :(得分:1)
正如你所说的那样,状态monad并没有真正改善这种情况,我不认为可以预期,因为它太过笼统,因为它允许任意访问状态,并且令人讨厌因为它强制执行不必要的排序。
乍一看,这看起来很像foldr
:我们为空案件做了一件事,在(:)
的情况下,我们把头关了,然后根据尾部做一个递归调用。但是,由于递归调用不仅仅是直接使用尾部,因此它不是foldr
。
我们可以将其表达为paramorphism,但我认为这不会给可读性带来任何好处。
我注意到的是尾部的复杂递归都是基于lessThan
,这使我得出了以下打破算法的想法:
lessThans [] = []
lessThans (a:as) = (a, l) : lessThans bs
where (l, bs) = lessThan a as
fromPreOrder2 :: Ord a => [a] -> Tree a
fromPreOrder2 = foldr (\(a, l) r -> Branch a l r) Leaf . lessThans
我确信lessThans
可以有一个更好的名字,但我不太确定是什么!
foldr
也可以表示为foldr (uncurry Branch) Leaf
,但我不确定这是否有所改善。
编辑:此外,lessThans
是unfoldr
,导致此版本:
fromPreOrder3 :: Ord a => [a] -> Tree a
fromPreOrder3 = foldr (uncurry Branch) Leaf . unfoldr lessThanList
lessThanList [] = Nothing
lessThanList (a:as) = Just ((a, l), bs)
where (l, bs) = lessThan a as