【问题标题】:Understanding the concept of difference lists了解差异列表的概念
【发布时间】:2019-02-06 08:25:49
【问题描述】:

在阅读Real World Haskell 的第 13 章时,我想到了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


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

给定一个列表:[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'


附言

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

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

【问题讨论】:

  • 一般见canonical answer
  • 只有(x :)(xs ++) 允许在差异列表中。 (请注意,参数在左侧。)差异列表是有效的,因为它们强制每个操作进入前置。
  • 那么 difference listfoldl ' 有什么区别? foldl ' 不是在每次迭代时都将新值附加到buffer,从而使我们免于thunks 吗?
  • 请参阅 stackoverflow.com/questions/14938584/… 关于 foldl (免责声明:答案由我提供)。 foldl' (++) 构建了((([1]++[2])++[3])++[4])++[5] 的左倾链。
  • 回答您的新问题,(f 4 . f 5) $ 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],那么它再次构建了一个左倾链,即不好(二次)。 IOW,到底是什么不清楚?

标签: haskell optimization difference-lists


【解决方案1】:

您需要更加小心时间,即何时这或那件事正在发生。

我们不是从列表[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  :    [] ))

根据(++)的定义。

规范答案:Why are difference lists more efficient than regular concatenation?


即使a1 = (++ [4]) [1..] 本身也是一个 O(1) 操作,a2 = (++ [5]) a1a3 = (++ [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] ++) []))

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    • 2013-12-08
    • 2010-12-01
    • 2011-06-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多