我正在考虑将二叉树展平为列表,以便进行后续处理。
我首先考虑使用(++)
加入左右分支,但后来考虑了更糟糕的情况,需要花费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)
时间内工作,“堆栈空间”不会超过树的最大深度,并且可以与消费函数融合(即中间列表被消除)?这是压扁树木的“正确”方法吗?
答案 0 :(得分:12)
我对融合知之甚少,但我认为递归函数一般不能融合。但请记住,当你在Haskell中处理列表时,中间列表通常不会同时存在 - 你会知道开头并且没有计算结束,然后你会丢掉开头并且知道结束(与列表中的元素一样多的步骤)。这不是融合,这更像是“流良好行为”,并且意味着如果输出逐渐消耗,则空间要求更好。
无论如何,是的,我认为这是压扁一棵树的最好方法。当算法的输出是一个列表但是其他列表未经检查,并且正在进行连接时,difference lists(DList
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,而不是堆栈。