尾递归实现Scala的List分区方法

时间:2017-09-26 15:17:56

标签: scala recursion functional-programming tail-recursion

出于练习目的,我一直在尝试以功能方式实现几个Scala的List方法,其中一个方法是partition。假设以下签名:

def partition[T](l: List[T], f: T => Boolean): (List[T], List[T])

它返回一个由两个列表组成的元组 - 第一个包含l中满足传递的谓词f的所有元素,另一个包含所有其他元素。

我提出了以下递归解决方案,遗憾的是它不是尾递归的:

  def partition[T](l: List[T], f: T => Boolean): (List[T], List[T]) = {
    l match {
      case Nil => (Nil, Nil)
      case head :: rest => {
        val (left, right) = partition(rest, f)
        if (f(head))
          (head :: left, right)
        else
          (left, head :: right)
      }
    }
  } 

在这个堆栈溢出问题(Can all recursive functions be re-written as tail-recursions?)中,清楚的是在某些情况下可以使用累加器。在给定的一个中,我会声称这是不可能的,因为它会以相反的方式返回最终列表。

你能给我一个尾递归解决方案吗?也许即使继续传递(我还没有真正理解它是如何工作的以及如何应用它)?

2 个答案:

答案 0 :(得分:1)

您需要保留两个累加器,一个用于left,另一个用于right。当您完成输入列表后,只需返回两个累加器(将它们反转以恢复原始顺序):

def partition[T](l: List[T], f: T => Boolean): (List[T], List[T]) = {
  @annotation.tailrec
  def aux(tl: List[T], left: List[T], right: List[T]): (List[T], List[T]) = tl match {
    case Nil => (left.reverse, right.reverse)
    case head :: rest => {
      if (f(head))
        aux(rest, head :: left, right)
      else
        aux(rest, left, head :: right)
    }
  }

  aux(l, List(), List())
} 

使用它:

scala> def partition[T](l: List[T], f: T => Boolean): (List[T], List[T]) = {
     |   @annotation.tailrec
     |   def aux(tl: List[T], left: List[T], right: List[T]): (List[T], List[T]) = tl match {
     |     case Nil => (left.reverse, right.reverse)
     |     case head :: rest => {
     |       if (f(head))
     |         aux(rest, head :: left, right)
     |       else
     |         aux(rest, left, head :: right)
     |     }
     |   }
     | 
     |   aux(l, List(), List())
     | } 
partition: [T](l: List[T], f: T => Boolean)(List[T], List[T])

scala> partition(List(1, 2, 3, 4, 5), (i: Int) => i%2 == 0)
res1: (List[Int], List[Int]) = (List(2, 4),List(1, 3, 5))

答案 1 :(得分:1)

你可以将一个元组作为累加器,并确保在返回之前反转列表:

def partition[T](l: List[T])(f: T => Boolean): (List[T], List[T]) = {
  @tailrec
  def partitionInternal(l: List[T])(acc: (List[T], List[T])): (List[T], List[T]) = {
    l match {
      case Nil => acc
      case head :: tail =>
        if (f(head)) partitionInternal(tail)(head :: acc._1, acc._2)
        else partitionInternal(tail)(acc._1, head :: acc._2)
    }
  }

  val (lf, r) = partitionInternal(l)((List.empty[T], List.empty[T]))
  (lf.reverse, r.reverse)
}

另一种解决方案是追加(:+)而不是前置(::),但是你为每个条目支付O(n)的价格。

另一个想法是使用ListBuffer[T]代替List[T]进行内部递归实现,该实现具有恒定时间附加。您需要做的就是在结尾处致电.toList

def partition[T](l: List[T])(f: T => Boolean): (List[T], List[T]) = {
  @tailrec
  def partitionInternal(l: List[T])(acc: (ListBuffer[T], ListBuffer[T])): (ListBuffer[T], ListBuffer[T])  = {
    l match {
      case Nil => acc
      case head :: tail =>
        val (leftAcc, rightAcc) = acc
        if (f(head)) partitionInternal(tail)((leftAcc += head, rightAcc))
        else partitionInternal(tail)((leftAcc, rightAcc += head))
    }
  }

  val (lf, r) = partitionInternal(l)((ListBuffer.empty[T], ListBuffer.empty[T]))
  (lf.toList, r.toList)
}

另外,请注意我为List[T]T => Boolean中的函数创建了单独的参数列表。这是为了帮助编译器在应用方法时推断出正确的类型参数,因为类型推断在参数列表之间流动。