更确切地说,从Haskell中的深度优先预订单列表构建BST

时间:2014-01-08 18:25:22

标签: haskell tree binary-search-tree tree-traversal

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)。

修改

从另一个方向拿东西,我有一种唠叨的怀疑,即可能想出一个算法,该算法首先将列表分成增加的段和减少段,但我还没有发现具体的事情要做有这个想法。

3 个答案:

答案 0 :(得分:4)

我认为您使用State时遇到的问题是您的原语(pushpoppeek)不正确。我认为更好的是像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。我们还明确了解fromPreOrderlessThan如何无法计算出更好的结果时默认为Leaf

(另请注意,fromPreOrderlessThan现在几乎相同,这是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,但我不确定这是否有所改善。

编辑:此外,lessThansunfoldr,导致此版本:

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