Algorithm 实现尾部递归列表操作
我担心我错过了一些“标准”,但我确实试过了,老实说 我一直在试图理解如何在链表上实现高效的尾部递归操作。(我是用Scala写的,但我怀疑这是否真的相关) 注意:我是从算法理论的角度对此进行研究的,我对“使用预先构建的库,它已经解决了这个问题”不感兴趣: 因此,如果我执行一个简单的过滤器实现来处理列表: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
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)算法成为可能。在不变的世界中,双链表是不可能的