减少深度优先树遍历的空间使用

时间:2015-12-23 07:23:04

标签: haskell garbage-collection

在Haskell中,可以在常量空间中对无限列表进行过滤,求和等,因为Haskell只在需要时生成列表节点,而垃圾收集它完成的列表。

我希望能与无限的树木一起工作。

下面是一个相当愚蠢的程序,它生成一个无限二叉树,其中的节点代表自然数。

然后,我编写了一个函数,对该树进行深度优先遍历,在特定级别吐出节点。

然后,我已经对可以除以5的节点做了快速求和。

理论上,该算法可以在O(n)空间中为n节点的O(2^n)深度树实现。只需动态生成树,删除已经完成处理的节点。

Haskell确实生成了树,但没有垃圾收集它看起来的节点。

以下是代码,我希望看到具有类似效果但不需要O(2^n)空间的代码。

import Data.List (foldl')

data Tree = Tree Int Tree Tree

tree n = Tree n (tree (2 * n)) (tree (2 * n + 1))
treeOne = tree 1

depthNTree n x = go n x id [] where
  go :: Int -> Tree -> ([Int] -> [Int]) -> [Int] -> [Int]
  go 0 (Tree x _ _) acc rest = acc (x:rest)
  go n (Tree _ left right) acc rest = t2 rest where 
    t1 = go (n - 1) left acc
    t2 = go (n - 1) right t1

main = do
  x <- getLine
  print . foldl' (+) 0 . filter (\x -> x `rem` 5 == 0) $ depthNTree (read x) treeOne

1 个答案:

答案 0 :(得分:2)

您的depthNTree使用2^n空格,因为当您遍历正确的子树时,通过t1保留左子树。对右子树的递归调用应该不包含对左边的引用,作为逐步垃圾收集遍历的必要条件。

这个天真的版本在这个例子中可以接受:

depthNTree n t = go n t where
  go 0 (Tree x _ _) = [x]
  go n (Tree _ l r) = go (n - 1) l ++ go (n - 1) r

现在main输入24使用2 MB空间,而原始版本使用1820 MB。此处的最佳解决方案与上述类似,不同之处在于它使用差异列表:

depthNTree n t = go n t [] where
  go 0 (Tree x _ _) = (x:)
  go n (Tree _ l r) = go (n - 1) l . go (n - 1) r

在许多情况下,这并不比普通列表版本快得多,因为树形深度大约在20-30左右,++的左嵌套并不是非常昂贵。如果我们使用大树深度,差异会变得更加明显:

print $ sum $ take 10 $ depthNTree 1000000 treeOne

在我的计算机上,这在差异列表中运行0.25秒,在列表中运行1.6秒。