问题
((((a ++ b) ++ c) ++ d) ++ e) ++ f
是嵌套。 (++) 的应用程序是左嵌套的,这很糟糕;右嵌套
a ++ (b ++ (c ++ (d ++ (e ++f))))
不会有问题。那是因为(++) 被定义为
[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
所以要找到使用哪个方程,实现必须深入表达式树
(++)
/ \
(++) f
/ \
(++) e
/ \
(++) d
/ \
(++) c
/ \
a b
直到它找出左操作数是否为空。如果它不是空的,它的头部被取出并冒泡到顶部,但左操作数的尾部保持不变,所以当需要连接的下一个元素时,同样的过程再次开始。
当连接是右嵌套时,(++) 的左操作数总是在顶部,检查空头/冒泡是 O(1)。
但是当串联是左嵌套时,n 层深,要到达第一个元素,必须遍历树的 n 节点,对于结果的每个元素(来自第一个列表,@987654330 @对于那些来自第二个等的人。
让我们考虑a = "hello"
hi = ((((a ++ b) ++ c) ++ d) ++ e) ++ f
我们想要评估take 5 hi。所以首先,必须检查是否
(((a ++ b) ++ c) ++ d) ++ e
为空。为此,必须检查是否
((a ++ b) ++ c) ++ d
为空。为此,必须检查是否
(a ++ b) ++ c
为空。为此,必须检查是否
a ++ b
为空。为此,必须检查是否
a
为空。呸。不是,所以我们可以再次冒泡,组装
a ++ b = 'h':("ello" ++ b)
(a ++ b) ++ c = 'h':(("ello" ++ b) ++ c)
((a ++ b) ++ c) ++ d = 'h':((("ello" ++ b) ++ c) ++ d)
(((a ++ b) ++ c) ++ d) ++ e = 'h':(((("ello" ++ b) ++ c) ++ d) ++ e)
((((a ++ b) ++ c) ++ d) ++ e) ++ f = 'h':((((("ello" ++ b) ++ c) ++ d) ++ e) ++ f)
对于'e',我们必须重复,对于'l's 也是...
绘制树的一部分,冒泡是这样的:
(++)
/ \
(++) c
/ \
'h':"ello" b
成为第一
(++)
/ \
(:) c
/ \
'h' (++)
/ \
"ello" b
然后
(:)
/ \
'h' (++)
/ \
(++) c
/ \
"ello" b
一直回到顶部。最后成为顶层(:)右孩子的树的结构,和原树的结构完全一样,除非最左边的列表是空的,当
(++)
/ \
[] b
节点被折叠成b。
因此,如果您有短列表的左嵌套连接,则连接变为二次,因为要获得连接的头部是 O(嵌套深度)操作。一般来说,左嵌套的串联
(...((a_d ++ a_{d-1}) ++ a_{d-2}) ...) ++ a_2) ++ a_1
是 O(sum [i * length a_i | i <- [1 .. d]]) 以进行全面评估。
使用差异列表(为了简化说明,没有 newtype 包装器),组合是否左嵌套并不重要
((((a ++) . (b ++)) . (c ++)) . (d ++)) . (e ++)
或右嵌套。一旦你遍历了嵌套到达(a ++),(++) 就会被提升到表达式树的顶部,因此获取a 的每个元素也是 O(1)。
事实上,只要你需要第一个元素,整个组合就会与差异列表重新关联,
((((a ++) . (b ++)) . (c ++)) . (d ++)) . (e ++) $ f
变成
((((a ++) . (b ++)) . (c ++)) . (d ++)) $ (e ++) f
(((a ++) . (b ++)) . (c ++)) $ (d ++) ((e ++) f)
((a ++) . (b ++)) $ (c ++) ((d ++) ((e ++) f))
(a ++) $ (b ++) ((c ++) ((d ++) ((e ++) f)))
a ++ (b ++ (c ++ (d ++ (e ++ f))))
在此之后,每个列表都是顶级(++)在前一个列表被消耗后的最左边的操作数。
其中重要的是,前置函数 (a ++) 可以在不检查其参数的情况下开始生成结果,以便重新关联
($)
/ \
(.) f
/ \
(.) (e ++)
/ \
(.) (d ++)
/ \
(.) (c ++)
/ \
(a ++) (b ++)
通过
($)---------
/ \
(.) ($)
/ \ / \
(.) (d ++) (e ++) f
/ \
(.) (c ++)
/ \
(a ++) (b ++)
到
($)
/ \
(a ++) ($)
/ \
(b ++) ($)
/ \
(c ++) ($)
/ \
(d ++) ($)
/ \
(e ++) f
不需要知道最终列表f的组合函数,所以只是O(depth)的重写。然后是顶层
($)
/ \
(a ++) stuff
变成
(++)
/ \
a stuff
并且a的所有元素都可以一步获取。在这个例子中,我们有纯左嵌套,只需要重写一次。如果不是(例如)(d ++),那个地方的函数是一个左嵌套组合,(((g ++) . (h ++)) . (i ++)) . (j ++),顶级重新关联将保持不变,当它成为顶部的左操作数时,它将重新关联-level ($) 在所有之前的列表都被消费完之后。
所有重新关联所需的总工作量为O(number of lists),因此连接的总成本为O(number of lists + sum (map length lists))。 (这意味着你也可以通过插入大量左嵌套的([] ++) 来带来糟糕的性能。)
newtype DiffList a = DiffList {getDiffList :: [a] -> [a] }
instance Monoid (DiffList a) where
mempty = DiffList (\xs -> [] ++ xs)
(DiffList f) `mappend` (DiffList g) = DiffList (\xs -> f (g xs))
只是将其包装起来,以便抽象处理。
DiffList (a ++) `mappend` DiffList (b ++) ~> DiffList ((a ++) . (b++))
请注意,它只对不需要检查其参数即可开始产生输出的函数有效,如果任意函数包装在DiffLists 中,则没有这样的效率保证。特别是,附加((++ a),是否已包装)可以在右嵌套组合时创建(++) 的左嵌套树,因此如果DiffList 构造函数被公开,您可以创建O(n²) 连接行为。