Algorithm 实现尾部递归列表操作

Algorithm 实现尾部递归列表操作,algorithm,scala,recursion,tail-recursion,Algorithm,Scala,Recursion,Tail Recursion,我担心我错过了一些“标准”,但我确实试过了,老实说 我一直在试图理解如何在链表上实现高效的尾部递归操作。(我是用Scala写的,但我怀疑这是否真的相关) 注意:我是从算法理论的角度对此进行研究的,我对“使用预先构建的库,它已经解决了这个问题”不感兴趣: 因此,如果我执行一个简单的过滤器实现来处理列表: def filter[T](l: List[T])(f: T => Boolean): List[T] = l match { case h :: t => if (f

我担心我错过了一些“标准”,但我确实试过了,老实说

我一直在试图理解如何在链表上实现高效的尾部递归操作。(我是用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),并且可能比“向后捕捉并反转”选项所涉及的工作量更少?我错过了什么吗(我担心这对我来说是正常的:()

之所以不能避免反转,是因为标准库列表是一个指针指向头部的链表:完全可以使用指针指向尾部实现自己的列表,并避免调用反转


然而,因为这不会从算法的角度带来任何改进,所以自己编写这个列表也没有多大意义,也没有把它包括在标准库中。不,你没有遗漏任何东西。积累一些结果,然后最后将其反转是完全正常的。如果你不喜欢然后,您可以尝试通过一些标准操作的组合来表示您的计算,如
foldLeft
map
flatMap
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
的指针,而不是预先添加到h这暂时破坏了不变性,但在这种情况下,它或多或少是正常的,因为在我们构建它时,累加器不会泄漏到外部


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

也许可以使用:+语法来附加head

 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())

}

事后反转是相当标准的。不确定你看到的“丑陋”是什么。或者,使用
Queue
Vector
之类的东西,因为它们提供(大部分)恒定时间附加。好吧,如果没有必要,它将是丑陋的:)但如果有必要,那就这样吧!你可以这样看。过滤本身需要一次遍历(如果您有一个带有可变链接的单链表,您可以将其追加到末尾,这样一次遍历就足够了)。但是,您需要构建一个不可变的数据结构作为结果。第二次遍历(用于反转)可以被看作是为获得一个好的、不可变的、线程安全的、持久的数据结构而付出的代价。所以,这通常是值得的。这很有意义,我隐约记得读到Scala列表做了某种写时复制的技巧,允许它是可变的,并且在某种方式/情况下尾部可追加,这改进了一些东西。我怀疑这种“如果你把它藏起来的话,脏亚麻布还不算太坏”的说法可能也有类似的哲学意义。但是如果我没有错过明显的/标准,我很高兴。这是低效的-
:+
O(n)
,如果你这样做
n
次,整个算法是
O(n^2)
,这是真的。由于Scala使用单链表作为不可变列表实现,因此无法实现O(n)算法。好问题。。抱歉,我没有完全阅读。也许您可以创建自己的列表实现,作为双链接列表或循环链接列表。这将使O(n)算法成为可能。在不变的世界中,双链表是不可能的