我有以下红黑树:
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中。
答案 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
,因为它会在模式匹配中进行仔细检查。