Haskell 理解差异列表的概念
当我阅读《真实世界哈斯克尔》的第13章时,我想到了差异列表的概念 作者说,在命令式语言中,如果我们想将一个元素附加到一个列表中,代价将是Haskell 理解差异列表的概念,haskell,optimization,difference-lists,Haskell,Optimization,Difference Lists,当我阅读《真实世界哈斯克尔》的第13章时,我想到了差异列表的概念 作者说,在命令式语言中,如果我们想将一个元素附加到一个列表中,代价将是O(1),因为我们会保留一个指向最后一个元素的指针。 但是在Haskell中,我们有不可变的对象,因此每次都需要遍历列表并在末尾追加元素,因此0(n) 而不是[1,2,3]++[4]+[5] 我们可以使用部分应用程序:(++4)。(++5)[1,2,3] 我不明白这是如何提高效率的,因为: -当我做(++[5])[1,2,3]时,它仍然是O(n),然后是(+++
O(1)
,因为我们会保留一个指向最后一个元素的指针。
但是在Haskell中,我们有不可变的
对象,因此每次都需要遍历列表并在末尾追加元素,因此0(n)
而不是[1,2,3]++[4]+[5]
我们可以使用部分应用程序:
(++4)。(++5)[1,2,3]
我不明白这是如何提高效率的,因为:
-当我做
(++[5])[1,2,3]
时,它仍然是O(n)
,然后是(+++4)
0(n)
引用
There are two very interesting things about this approach. The first is that the cost of a partial application is constant, so the cost of many partial applications is linear. The second is that when we finally provide a [] value to
unlock the final list from its chain of partial applications, application
proceeds from right to left. This keeps the left operand of (++) small, and so
the overall cost of all of these appends is linear, not quadratic
我知道这种方法非常迫切,因此,我们没有像作者所说的那样保留
尚未应用的方法的thunk,而是保留左操作数小,但是。。。。我们仍然对每个附加执行遍历
给定一个列表:[1…100]
并且想要追加1,2
我仍然会遍历它2次,因为我会:
f(f([1..100])=f([1..100,1])
-遍历1次并追加1
[1..100,1,2]
-第二次遍历以追加2
有人能告诉我这在时间复杂度方面如何更有效吗?(因为space
——复杂性不再是因为thunks
,比如foldl'
)
p.S
我知道这个标准答案,我也读过,我觉得很有帮助。
我知道你可以通过使用:
,在左边附加+
来实现O(1)
复杂性,但它与+
不同
如果我在a=[1,2,3]
上使用f=(:)
:
(f4.f5)$a
我可以说我每次追加都有0(1)
效率,因为我总是在左边追加,但我不会得到[1,2,3,4,5]
,我会得到[5,4,1,2,3]
,那么,在这种情况下,差异列表
对于附加一个元素的单一操作如何更有效呢?您需要更加注意时间,即当这件事或那件事发生时
我们从不同的列表开始,而不是从列表开始
f1 = ([1,2,3] ++)
然后,在不断增长的差异列表的末尾“添加”4、5,我们有
f2 = f1 . ([4] ++)
f3 = f2 . ([5] ++)
在增长差异列表末尾的每一个这样的添加都是O(1)
当我们最终完成构建它时,我们将其按应用程序转换为“普通”列表
xs = f3 [] -- or f3 [6..10] or whatever
然后,我们小心地
xs = ((([1,2,3] ++) . ([4] ++)) . ([5] ++)) []
= (([1,2,3] ++) . ([4] ++)) ( ([5] ++) [] )
= ([1,2,3] ++) ( ([4] ++) ( ([5] ++) [] ))
= 1:2:3: ( 4 : ( 5 : [] ))
根据(++)
的定义
一个典型的答案:
即使是a1=(++[4])[1..
本身也是一个O(1)操作,就像a2=(++[5])a1
和a3=(++[6])a2
一样,因为Haskell是懒惰的,thunk创建是O(1)
只有当我们访问最终结果时,整个操作才会变成二次的,因为访问++
结构不会重新排列它——它保持左嵌套状态,因此在从顶部重复访问时是二次的
通过将左嵌套的
结构转换为[]
结构,将该结构内部重新排列为右嵌套的$
结构,如规范答案中所述,因此从顶部重复访问该结构是线性的
因此,(++[5])(++[4])[1,2,3]
(坏)和(([1,2,3]++)([4]++)([5]++)[]
(好)之间存在差异。构建函数链本身是线性的,是的,但是它创建了一个完全访问的二次结构
但是(([1,2,3]++)([5]++)([4]++)[]
变成了([1,2,3]++)([5]++)([4]++)([4]++)[])
,通常请参见。在差异列表中只允许(x:)
和(xs++)
。(注意,参数在左边。)差异列表是有效的,因为它们将每个操作都强制到前置。那么差异列表
和foldl'
之间的区别是什么?在每次迭代时,foldl'
是否会将新值附加到缓冲区
,从而使我们免于thunks
?请参阅关于foldl的内容(免责声明:答案由我提供)foldl'(++)
构建了一条左倾的(([1]++[2]++[3]++[4]++[5]
。对于你的新问题,(f4.f5)$a=(:4)。(:5))[1,2,3]=4:5:[1,2,3]
。如果你的意思是(++[4])(++[5])[1,2,3]=(++[4])([1,2,3]++[5])=([1,2,3]++[5])++[4]
,那么它再次构建了一个左倾链,即不好(二次型)。瞧,到底什么是不清楚的?