Haskell:拼合二叉树

时间:2012-05-15 01:02:37

标签: algorithm haskell tree binary-tree difference-lists

我正在考虑将二叉树展平为列表,以便进行后续处理。

我首先考虑使用(++)加入左右分支,但后来考虑了更糟糕的情况,需要花费O(n^2)时间。

然后我考虑向后构建列表,使用(:)以线性时间附加到前面。然而,我想如果我将这个列表发送到类似折叠的函数,它必须等到整个树遍历才能开始折叠,因此不能使用list fusion

然后我想出了following

data Tree a = Node a (Tree a) (Tree a) | Tip

flatten :: Tree a -> [a]
flatten x = (flatten' x) []

flatten' :: Tree a -> [a] -> [a]
flatten' (Node x left right) l = (flatten' left (x:(flatten' right l)))
flatten' Tip l = l

main = 
  putStrLn $ show $ flatten $ 
    (Node 2 (Node 1 Tip Tip) (Node 4 (Node 3 Tip Tip) Tip))

这是否会在O(n)时间内工作,“堆栈空间”不会超过树的最大深度,并且可以与消费函数融合(即中间列表被消除)?这是压扁树木的“正确”方法吗?

2 个答案:

答案 0 :(得分:12)

我对融合知之甚少,但我认为递归函数一般不能融合。但请记住,当你在Haskell中处理列表时,中间列表通常不会同时存在 - 你会知道开头并且没有计算结束,然后你会丢掉开头并且知道结束(与列表中的元素一样多的步骤)。这不是融合,这更像是“流良好行为”,并且意味着如果输出逐渐消耗,则空间要求更好。

无论如何,是的,我认为这是压扁一棵树的最好方法。当算法的输出是一个列表但是其他列表未经检查,并且正在进行连接时,difference listsDList s)通常是最好的方法。它们将列表表示为“预处理函数”,当您追加时,它不需要遍历,因为追加只是函数组合。

type DList a = [a] -> [a]

fromList :: [a] -> DList a
fromList xs = \l -> xs ++ l

append :: DList a -> DList a -> DList a
append xs ys = xs . ys

toList :: DList a -> [a]
toList xs = xs []

这些是实施的基本要素,其余的可以从中得出。 DList中的朴素展平算法是:

flatten :: Tree a -> DList a
flatten (Node x left right) = flatten left `append` fromList [x] `append` flatten right
flatten Tip = fromList []

让我们做一点扩展。从第二个等式开始:

flatten Tip = fromList []
            = \l -> [] ++ l
            = \l -> l
flatten Tip l = l

看看这是怎么回事?现在是第一个等式:

flatten (Node x left right) 
    = flatten left `append` fromList [x] `append` flatten right
    = flatten left . fromList [x] . flatten right
    = flatten left . (\l -> [x] ++ l) . flatten right
    = flatten left . (x:) . flatten right
flatten (Node x) left right l
    = (flatten left . (x:) . flatten right) l
    = flatten left ((x:) (flatten right l))
    = flatten left (x : flatten right l)

其中显示DList表达式与您的功能相同!

flatten' :: Tree a -> [a] -> [a]
flatten' (Node x left right) l = (flatten' left (x:(flatten' right l)))
flatten' Tip l = l

我没有任何证据证明DList为什么比其他方法更好(最终取决于你如何消耗你的输出),但DList是规范的方法来有效地做到这一点,这就是你所做的。

答案 1 :(得分:2)

flatten'是尾递归的,因此它不应占用任何堆栈空间。然而,它将从树的左侧走下来,在堆中吐出一堆th。声。如果你在你的示例树上调用它,并将它减少到WHNF,你应该得到这样的东西:

  :
 / \
1  flatten' Tip :
               / \
              2   flatten' (Node 4) []
                           /      \
                         (Node 3) Tip
                        /       \
                       Tip      Tip

算法为O(N),但必须检查Tip以及Node s。

这看起来是按顺序展平树的最有效方法。 Data.Tree模块有一个flatten函数here,它做了很多相同的事情,除了它更喜欢预订遍历。

<强>更新

在图表缩减引擎中,flatten中的main会生成如下图:

               @
              / \
             @  []
            / \
           /   \
          /     \
       flatten' Node 2
                /    \
               /      \
              /        \
           Node 1    Node 4
           /   \     /   \
          Tip  Tip  /     \
                   /       \
                Node 3     Tip
                /   \
               Tip  Tip

为了将其减少到WHNF,图形缩减引擎将展开脊柱,将[]Node 2推入堆栈。然后它将调用flatten'函数,该函数将图形重写为:

                 @
                / \
               /   \
              /     \
             @       :
            / \     / \
           /   \   2   \
          /     \       \
       flatten' Node 1   \
                /   \     \
               Tip  Tip    @
                          / \
                         @  []
                        / \
                       /   \
                      /     \
                   flatten' Node 4
                            /   \
                           /     \
                          /       \
                       Node 3     Tip
                       /   \
                      Tip  Tip

并将从堆栈中弹出两个参数。根节点仍然不在WHNF中,因此图形缩减引擎将展开脊柱,将2:...Node 1推入堆栈。然后它将调用flatten'函数,该函数将图形重写为:

                 @
                / \
               /   \
              /     \
             @       :
            / \     / \
           /   \   1   \
          /     \       \
       flatten' Tip      @
                        / \
                       /   \
                      /     :
                     @     / \
                    / \   2   \
                   /  Tip      @
                  /           / \
               flatten'      @  []
                            / \
                           /   \
                          /     \
                       flatten' Node 4
                                /   \
                               /     \
                              /       \
                           Node 3     Tip
                           /   \
                          Tip  Tip

并将从堆栈中弹出两个参数。根节点仍然不在WHNF中,因此图形缩减引擎将展开脊柱,将1:...Tip推入堆栈。然后它将调用flatten'函数,该函数将图形重写为:

                 :
                / \
               1   \
                    \
                     @
                    / \
                   /   \
                  /     :
                 @     / \
                / \   2   \
               /  Tip      @
              /           / \
           flatten'      @  []
                        / \
                       /   \
                      /     \
                   flatten' Node 4
                            /   \
                           /     \
                          /       \
                       Node 3     Tip
                       /   \
                      Tip  Tip

并将从堆栈中弹出两个参数。我们现在在WHNF中,最多消耗了两个堆栈条目(假设Tree节点不是需要额外堆栈空间来评估的thunks。)

因此,flatten' 尾递归。它无需评估其他嵌套的重新索引即可替换自身。第二个flatten'仍然是堆中的thunk,而不是堆栈。