实现尾递归List操作

时间:2018-02-06 11:05:11

标签: algorithm scala recursion tail-recursion

我担心我错过了“标准”的东西,但我确实尝试过,诚实!

我一直在努力了解如何在链表上实现高效的尾递归操作。 (我在Scala写作,但我怀疑这实际上是相关的。)

注意:我正在从算法理论的角度来研究这个问题,我对“使用预构建的库不感兴趣,它已经解决了这个问题”:)

因此,如果我执行一个简单的过滤器实现来处理列表:

  def filter[T](l: List[T])(f: T => Boolean): List[T] = l match {
      case h :: t => if (f(h)) h :: filter(t)(f) else filter(t)(f)
      case _ => List()
  }

这不是尾递归,但它的效率相当高(因为它只会将新项目添加到它构造的结果列表中)

然而,我提出了简单的尾递归变体:

  def filter[T](l: List[T])(f: T => Boolean): List[T] = {
    @tailrec
    def filterAcc[T](l: List[T], f: T => Boolean, acc: List[T]): List[T] = l match {
      case List() => acc
      case h :: t => if (f(h)) filterAcc(t, f, h :: acc) else filterAcc(t, f, acc)
    }
    filterAcc(l, f, List())
  }

撤消项目的顺序。 (当然,这并不奇怪!)

当然,我可以通过将过滤后的项目附加到累加器来修复顺序,但是这会使我认为这是一个O(n ^ 2)实现(因为每个附加都会强制构建)一个全新的列表,它是Scala不可变列表上的O(n)操作,列表中n个元素的重复次数为n次

我也可以通过在生成的列表上调用reverse来解决这个问题。我很欣赏这将是一个单独的O(n)操作,因此整体时间复杂度仍然是O(n),但它似乎很难看。

所以,我的问题就是这个;是否有一个尾递归的解决方案,从一开始就以正确的顺序累积,即O(n)并且可能涉及的工作量少于“向后捕获并反转它”选项?我错过了什么(这对我来说很正常,我担心:()

3 个答案:

答案 0 :(得分:2)

你无法避免反向的原因是因为标准库列表是一个带有指向头部的指针的链接列表:完全有可能用指向尾部的指针实现你自己的列表并避免调用反向。 / p>

但是,因为这不会从算法的角度带来任何改进,所以没有多大意义,也不能自己编写这个列表,也不能将它包含在标准库中

答案 1 :(得分:1)

不,你没有遗漏任何东西。积累一些结果然后reverse最终是完全正常的。如果您不喜欢它,那么您可以尝试通过标准操作的某些组合来表达您的计算,例如foldLeftmapflatMapfilter等。

有人说......如果你忘了private修饰符和不变性,那么你实际上可以编写一个尾递归filter,但它是真的不漂亮:

import scala.annotation.tailrec

def filter[T](l: List[T])(f: T => Boolean): List[T] = {

  val tailField = classOf[::[_]].getDeclaredField("tl")
  tailField.setAccessible(true)

  /* Appends a value in constant time by 
   * force-overriding the tail element of the first cons 
   * of the list `as`. If `as` is `Nil`, returns a new cons.
   *
   * @return the last cons of the new list
   */
  def forceAppend[A](as: List[A], lastCons: List[A], a: A): (List[A], List[A]) = as match {
    case Nil => {
      val newCons = a :: Nil
      (newCons, newCons) // not the same as (a :: Nil, a :: Nil) in this case!!!
    }
    case _ => {
      val newLast = a :: Nil
      tailField.set(lastCons, newLast)
      (as, newLast)
    }
  }

  @tailrec
  def filterAcc[T](l: List[T], f: T => Boolean, acc: List[T], lastCons: List[T]): List[T] = {
    l match {
      case List() => acc
      case h :: t => if (f(h)) {
        val (nextAcc, nextLastCons) = forceAppend(acc, lastCons, h) 
        filterAcc(t, f, nextAcc, nextLastCons)
      } else {
        filterAcc(t, f, acc, lastCons)
      }
    }
  }
  filterAcc(l, f, Nil, Nil)
}

val list = List("hello", "world", "blah", "foo", "bar", "baz")
val filtered = filter(list)(_.contains('o'))

println(filtered)

这里发生的是:我们只是假装我们在C中编写代码,并且我们希望直接使用构建数据结构的引用。这允许我们保持对列表的最后cons的引用,然后覆盖指向下一个cons的指针,而不是前置于头部。这暂时破坏了不变性,但在这种情况下它的确或多或少都可以,因为在我们构建它时,累加器不会泄漏到外面。

我非常怀疑这比直接实施更快。恰恰相反:它可能实际上更慢,因为代码更复杂,编译器更难以优化。

答案 2 :(得分:0)

也许使用:+语法来代替头部。

 def filter[T](l: List[T])(f: T => Boolean): List[T] = {
    @tailrec
    def filterAcc(l: List[T], f: T => Boolean, acc: List[T]): List[T] = l match {
      case Nil => acc
      case h :: t => if (f(h)) filterAcc(t, f, acc :+ h) else filterAcc(t, f, acc)
}

filterAcc(l, f, List())

}