【问题标题】:Implement tail-recursive List operations实现尾递归列表操作
【发布时间】:2018-02-06 11:05:11
【问题描述】:

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

我一直在尝试了解如何在链表上实现高效的尾递归操作。 (我正在用 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),但它看起来很难看。

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

【问题讨论】:

  • 之后反转是相当标准的。不知道你看到什么“丑陋”。或者,使用 QueueVector 之类的东西,因为它们提供(大部分)恒定时间追加。
  • 好吧,如果没有必要,那就太丑了:) 但如果有必要,那就这样吧!
  • 可以这样看。过滤本身需要单次遍历(如果您有一个带有可变链接的单链表,您可以追加到末尾,单次遍历就足够了)。但是,您想要构建一个不可变数据结构作为结果。第二次遍历(用于反转)可以看作是为获得一个好的、不可变的、线程安全的、持久的数据结构而付出的代价。所以,这通常是值得的。

标签: algorithm scala recursion tail-recursion


【解决方案1】:

之所以不能避免reverse,是因为标准库List是一个指向head的Linked List:完全可以实现自己的带有指向tail的list,避免调用reverse。

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

【讨论】:

    【解决方案2】:

    不,你没有错过任何东西。积累一些结果然后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 的指针,而不是在头部之前。这暂时破坏了不变性,但在这种情况下它或多或少还可以,因为在我们构建累加器时它不会泄漏到外部。

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

    【讨论】:

    • 这很有道理,我隐约记得读过 Scala List 做了某种写时复制技巧,以使其可变,并以某种方式/情况尾部附加,这改进了一些东西。我怀疑这种“如果你把它藏起来,脏亚麻布也不错”之类的东西可能是类似的哲学路线。但如果我没有错过明显/标准,我很高兴。
    【解决方案3】:

    也许使用 :+ 语法来代替 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())
    

    }

    【讨论】:

    • 这是低效的 - :+O(n) 如果你这样做 n 次整个算法是 O(n^2)
    • 确实如此。鉴于 Scala 使用单链表作为其不可变列表实现,因此无法实现 O(n) 算法。好问题..抱歉,我没有完整阅读。
    • 也许您可以创建自己的 List 实现作为双向链表或循环链表。这将使 O(n) 算法成为可能。
    • 双链表在不可变世界中是不可能的
    猜你喜欢
    • 1970-01-01
    • 2019-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-13
    • 2019-11-05
    • 2023-03-16
    • 1970-01-01
    相关资源
    最近更新 更多