【问题标题】:What is the time and space complexity of a Scala's head/tail extractor?Scala 的头/尾提取器的时间和空间复杂度是多少?
【发布时间】:2015-09-27 00:21:59
【问题描述】:

这个的时间和空间复杂度是多少:

def isPalindrome[A](x: Seq[A]): Boolean = x match {
  case h +: middle :+ t => h == t && isPalindrome(middle)
  case _ => true
}

是否依赖于Seq的实现?既然IndexedSeq 应该有O(1) 尾部与O(n) 对应LinearSeqs?空间复杂度是O(n)是因为递归调用堆栈还是Scala自动进行尾调用优化?

import scala.annotation.tailrec

@tailrec def isPalindrome[A](x: Seq[A]): Boolean = x match {
  case h +: middle :+ t => h == t && isPalindrome(middle)
  case _ => true
}

【问题讨论】:

  • 很确定复杂性取决于实现。至于@tailrec,它用于强制执行尾递归(即,当注释但不是尾递归时抛出编译器错误)尾递归函数会自动优化。 Scala 没有完整的尾调用优化。
  • 根据scala-lang.org/api/2.11.5/… scala List“有 O(1) prepend 和 head/tail access”关于时间性能,而 tail 的空间成本没有,我认为 head 也是如此,尽管没有说明. docs.scala-lang.org/overviews/collections/… 有一个收集时间性能特征表,显示 ArraySeq 的时间性能与头部的 List 相同,但尾部的 seq 长度为线性。
  • 有没有我可以保留相同的代码并让它成为 O(n) 时间和 O(1) 空间?

标签: scala tail-recursion tail extractor unapply


【解决方案1】:

是否依赖于 Seq 的实现?既然 IndexedSeq 对于 LinearSeqs 应该有 O(1) tail vs O(n)?

我通常会这样假设,但提取器实际上是O(n)。任何Seq 的提取器是scala.collection.:+,其中O(n) 用于最后一个列表,O(n) 用于最后一个。这两个的代码如下:

  def init: Repr = {
    if (isEmpty) throw new UnsupportedOperationException("empty.init")
    var lst = head
    var follow = false
    val b = newBuilder
    b.sizeHint(this, -1)
    for (x <- this) { // O(n)
      if (follow) b += lst
      else follow = true
      lst = x
    }
    b.result
  }

  def last: A = {
    var lst = head
    for (x <- this) // O(n)
      lst = x
    lst
  }

空间复杂度是 O(n) 是因为递归调用堆栈还是 Scala 会自动进行尾调用优化?

我看到代码确实有这种优化。这是有道理的,因为 t &amp;&amp; isPalindrome(middle) 允许 Scala 关闭当前调用堆栈,将 t 传递到下一个堆栈以供 &amp;&amp; 使用,因此它可以进行尾递归优化。

恒定时间匹配

使用Vector我们可以实现O(1)时间:

object ends {
  def unapply[T](t: Vector[T]): Option[(T, Vector[T], T)] =
    if (t.length < 2) None
    else Some((t.head, t.drop(1).dropRight(1), t.last))
}

def isPalindrome[A](x: Vector[A]): Boolean = x match {
  case ends(i, middle, l) => i == l && isPalindrome(middle)
  case _ => true
}

【讨论】:

  • @bjfletcher "O(2n)" 不是真的。
  • @TravisBrown 我发现它在序列上执行两个循环非常令人惊讶,我认为这值得注意。你是怎么写的? “O(n) - 实际上它做了两个循环”?谢谢:)
  • 我怎样才能保持同样好的递归代码并在 O(n) 时间内完成?我知道我当然可以使用 IndexedSeqs 快速查看 start 和 end 并保持指针,但感觉更脏.. 为什么没有 O(1) 时间尾提取器?
  • @bjfletcher 我只想说“它遍历序列两次”。随着符号的滥用,“O(2n)”并没有那么糟糕,我猜,但你正在让自己变得狡猾(来自像我这样的人,抱歉 :))。
  • 你想让我用新的提取器想法更新答案吗?让我知道,我明天就做。 @Travis,别抱歉——我喜欢学习如何改进这种技术书法。 :)
猜你喜欢
  • 2012-04-09
  • 2020-05-03
  • 2022-01-13
  • 2015-06-06
  • 1970-01-01
  • 2018-07-13
  • 1970-01-01
  • 1970-01-01
  • 2020-09-10
相关资源
最近更新 更多