为什么在Scala中追加到列表的时间复杂度为O(n)?


13

我刚刚读到,List(:+)的追加操作的执行时间随的大小线性增长List

追加到a List似乎很普通。为什么惯用的方法是在组件之前加上列表然后反转列表?这也不是设计失败,因为可以随时更改实现。

从我的角度来看,前置和附加都应为O(1)。

是否有任何正当理由?


2
取决于您对“合法”的定义。Scala大量使用不可变的数据结构,无处不在的匿名列表,功能组成等。默认列表实现(没有指向列表尾部的额外可变指针)适用于该样式。如果您需要一个更强大的列表,至少可以很容易地编写自己的容器,这与标准容器几乎没有区别。
Kilian Foth,

1
跨站点相关- 在Scala中将元素追加到列表的末尾 -其中有一些有关其性质的信息。这似乎是Scala中的一个列表是不可改变的,因此,你需要复制它,这是O(N)。

您可以使用Scala提供的O(1)附加时间(向量),使用许多可用的可变数据结构之一,或不可变的数据结构。List[T]假定您以纯函数式语言的方式使用它-通常从头开始进行解构和前置。
KChaloux

3
前置会将新头的下一个节点指针放置到现有的不可变列表中-不能更改。就是O(1)。

1
对于纯FP中数据结构复杂性测量的一般主题的开创性工作和分析,请阅读Okasaki的论文,该论文后来作为一本书出版。对于学习FP的任何人来说,理解如何思考如何在FP中组织数据,它都是备受推崇且非常好的阅读书。它也写得很好,易于阅读和遵循,因此完全是高质量的文本。
吉米·霍法2013年

Answers:


24

我将扩大我的评论。对List[T]数据结构scala.collection.immutable进行了优化,使其以一种更纯粹的功能编程语言中的不可变列表的方式工作。它的挂起时间非常快,并且假定您将为几乎所有访问而工作。

由于不可变列表将链接列表建模为一系列“约束单元”,因此它们的前置时间非常快。单元格定义一个值,以及指向下一个单元格的指针(经典的单链列表样式):

Cell [Value| -> Nil]

当您添加到列表之前时,实际上就是在创建一个新的单元格,而现有列表的其余部分则指向:

Cell [NewValue| -> [Cell[Value| -> Nil]]

由于列表是不可变的,因此您无需进行任何实际复制就可以放心地进行操作。不存在旧列表发生更改并导致新列表中的所有值变为无效的危险。但是,作为一种折衷办法,您将无法具有指向列表末尾的可变指针。

这很适合递归地处理列表。假设您定义了自己的版本filter

def deleteIf[T](list : List[T])(f : T => Boolean): List[T] = list match {
  case Nil => Nil
  case (x::xs) => f(x) match {
    case true => deleteIf(xs)(f)
    case false => x :: deleteIf(xs)(f)
  }
}

这是一个递归函数,仅在列表的开头起作用,并通过::提取器利用模式匹配。在Haskell等语言中,您会看到很多这种东西。

如果您真的想要快速追加,Scala提供了很多可变和不变的数据结构供您选择。在可变的方面,您可能会考虑ListBuffer。或者,Vectorscala.collection.immutable具有较快的附加时间。


现在我明白了!这完全有道理。
DPM

我不知道任何Scala,但这不是else一个无限循环吗?我认为应该是这样的x::deleteIf(xs)(f)
svick

@svick呃...是的。是的。我写的很快,并且没有验证我的代码,因为我有一个会议要去:p(现在应该解决!)
KChaloux

@Jubbat因为headtail用这种列表的访问是非常快-比使用任何基于散列的地图或阵列更快-它是递归函数优异的类型。这就是为什么列表成为大多数功能语言(例如Haskell或Scheme)中的核心类型的原因之一
bruce

极好的答案。我可能会添加一个TL; DR,它只是说“因为您应该在前面而不是在后面”(可能有助于清除大多数开发人员关于Lists和在后面加上/在前面的基本假设)。
Daniel B
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.