理解差异列表的概念

Ber*_*ian 4 optimization haskell difference-lists

当我阅读第 13 章时,Real World Haskell我想到了Difference Lists.
作者说,在命令式语言中,如果我们想将一个元素附加到列表中,成本将是O(1)因为我们将保留一个指向最后一个元素的指针。但是在 Haskell 中,我们有immutable对象,所以我们每次都需要遍历列表并将元素追加到末尾,因此0(n).

而不是[1,2,3]++[4]++[5]
我们可以使用部分应用程序:(++4).(++5) [1,2,3]

我不明白这是怎么更有效,因为:
-当我这样做(++[5]) [1,2,3]它仍然是O(n)接着又0(n)(++4)

引用

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
Run Code Online (Sandbox Code Playgroud)


我知道这种方法会很急切,所以yet to be applied methods我们不会small像作者所说的那样保留左操作数,但是......我们仍然对每个追加执行遍历。

给定一个列表:[1...100]并且想要追加1,2我仍然会遍历它2,因为我会:

  1. f(f([1..100]))= f([1..100,1]) - 遍历 1 次并附加 1

  2. [1..100,1,2] - 第二次遍历追加 2

有人能告诉我这在时间复杂度上如何更有效吗?(因为space-complexity 是由于不再有thunks,比如foldl'


聚苯乙烯

我知道规范的答案,我也阅读了这一章,我发现它非常有帮助。
我知道您可以O(1)通过使用 附加到左侧来实现复杂性:,但它不会类似于++.

如果我使用f=(:)a= [1,2,3]
(f 4 . f 5) $ a
我可以说,我有0(1)每个附加效率,因为我总是在附加到左边,但我不会[1,2,3,4,5],我会得到[5,4,1,2,3],所以如何在这种情况下,difference list对于追加的单一操作更快捷一个元素?

Wil*_*ess 5

你需要对时间更加小心,即这件事或那件事发生时。

我们不是从一个列表开始,而是从[1,2,3]不同的列表开始

f1 = ([1,2,3] ++)
Run Code Online (Sandbox Code Playgroud)

然后将 4、5“添加”到不断增长的差异列表的末尾,我们有

f2 = f1 . ([4] ++)
f3 = f2 . ([5] ++)
Run Code Online (Sandbox Code Playgroud)

每个这样的添加不断增长的差异列表的末尾都是O(1)

当我们最终完成构建时,我们将其转换为应用程序的“普通”列表

xs = f3 []    -- or f3 [6..10] or whatever
Run Code Online (Sandbox Code Playgroud)

然后,仔细地,我们得到

xs = ((([1,2,3] ++) . ([4] ++)) . ([5] ++)) []
   =  (([1,2,3] ++) . ([4] ++)) ( ([5] ++)  [] )
   =   ([1,2,3] ++) ( ([4] ++)  ( ([5] ++)  [] ))
   =     1:2:3:     (   4  :    (   5  :    [] ))
Run Code Online (Sandbox Code Playgroud)

根据 的定义(++)

一个规范的答案:为什么差异列表比常规串联更有效?


甚至a1 = (++ [4]) [1..] 它本身也是一个O(1)操作,就像a2 = (++ [5]) a1and 一样a3 = (++ [6]) a2,因为 Haskell 是懒惰的,而 thunk 创建是O(1)

只有当我们访问最终结果时,整个操作++才会变成二次的,因为访问结构不会重新排列它——它保持左嵌套,所以从顶部重复访问时是二次的。

通过应用左嵌套.结构将[]内部结构重新排列为右嵌套结构来转换为正常列表$,如规范答案中所述,因此从顶部重复访问此类结构是线性的。

所以区别在于((++ [5]) . (++ [4])) [1,2,3](坏)和((([1,2,3] ++) . ([4] ++)) . ([5] ++)) [](好)之间。构建函数链((++ [4]) . (++ [5]))本身是线性的,是的,但它创建了一个可以完全访问的二次结构。

而是((([1,2,3] ++) . ([5] ++)) . ([4] ++)) []变成([1,2,3] ++) (([5] ++) (([4] ++) []))