在Haskell中构建/评估一棵红黑树时,堆栈溢出

时间:2019-01-01 13:07:26

标签: haskell data-structures functional-programming lazy-evaluation red-black-tree

我有以下红黑树:

data Tree a
  = E
  | S a
  | C !Color !(Tree a) !(Tree a)

data Color = R | B

对于这棵树,所有数据都存储在叶子中(S构造函数)。我编写了一个insert函数,例如标准的Okasaki红黑树[1](修改内部值存储在其中的部分)

在这种情况下,我用1000万个元素填充了树:

l = go 10000000 E
  where
    go 0 t = insert 0 t
    go n t = insert t $ go (n - 1) t

当我尝试像这样评估树的最左侧元素(叶)时:

left :: Tree a -> Maybe a
left E = Nothing
left (S x) = Just x
left (C _ _ l _) = left l

我遇到以下情况:

  

左l

     

***例外:堆栈溢出

是由于构建树的方式(非尾递归)还是存在一些我看不到的空间泄漏。

请注意,该功能可用于一百万个元素。另外,我尝试了树构造的尾部递归方法:

l = go 10000000 E
  where
    go 0 t = insert 0 t
    go n t = go (n - 1) (insert n t)

但遇到相同的堆栈溢出异常。

[1] https://www.cs.tufts.edu/~nr/cs257/archive/chris-okasaki/redblack99.pdf

编辑

插入和平衡功能可确保完整性:

 insert :: Ord a => a -> Tree a -> Tree a
 insert x xs = makeBlack $ ins xs
   where
     ins E = S x
     ins (S a) = C R (S x) (S a)
     ins (C c l r) = balance c (ins l) r -- always traverse left and trust the balancing

     makeBlack (C _ l r) = C B l r
     makeBlack a = a

 balance :: Color -> Tree a -> Tree a -> Tree a
 balance B (C R (C R a b) c) d = C R (C B a b) (C B c d)
 balance B (C R a (C R b c)) d = C R (C B a b) (C B c d)
 balance B a (C R (C R b c) d) = C R (C B a b) (C B c d)
 balance B a (C R b (C R c d)) = C R (C B a b) (C B c d)
 balance color a b = C color a b

在我输入插入代码时从头到尾都感到困惑,它是insert n $ go (n - 1) t而不是insert t $ go (n - 1) t。但是,当实际遇到堆栈溢出时,代码是正确的,并且溢出发生在ghci中。

1 个答案:

答案 0 :(得分:3)

第一个插入代码示例有一个错误:它试图将树本身作为元素插入。

第二个版本

l = go 10000000 L.empty   where
    go 0 t = L.cons 0 t
    go n t = go (n - 1) (L.cons n t)

确实是尾递归,但是它仍然有一个问题:在构造树时,它在任何步骤都不会“强行”使用树。由于Haskell的懒惰,go将返回一个thunk,该隐藏L.cons的10000000个待处理的应用程序。

当运行时试图“弹出”该重击时,它将依次将每个n变量放入堆栈中,而下面的重击又被“弹出”,从而导致堆栈溢出。 "Function calls don't add stack frames in Haskell; instead, stack frames come from nesting thunks."

解决方案是将每个中间树强制为WHNF,以免积木堆积。这应该足够了(使用BangPatterns扩展名):

l :: Tree Int 
l = go 10000000 L.empty
  where
    go 0 !t = L.cons 0 t
    go n !t = go (n - 1) (L.cons n t)

这基本上意味着:“在递归添加另一个元素之前,请确保累加器位于WHNF中”。无需强制n,因为它会在模式匹配中进行仔细检查。