尾递归与重构

时间:2011-03-15 14:41:19

标签: scala recursion tail-recursion

我有这个方法

  private def getAddresses(data: List[Int], count: Int, len: Int): Tuple2[List[Address], List[Int]] = {
    if (count == len) {
      (List.empty, List.empty)
    } else {
      val byteAddress = data.takeWhile(_ != 0)
      val newData = data.dropWhile(_ != 0).tail
      val newCount = count + 1
      val newPosition = byteAddress.length + 1
      val destFlag = byteAddress.head
      if (destFlag == SMEAddressFlag) {
        (SMEAddress().fromBytes(byteAddress) :: getAddresses(newData, newCount, len)._1, newPosition :: getAddresses(newData, newCount, len)._2)
      } else {
        (DistributionList().fromBytes(byteAddress) :: getAddresses(newData, newCount, len)._1, newPosition :: getAddresses(newData, newCount, len)._2)
      }
    }
  }

我很想重写它

  private def getAddresses(data: List[Int], count: Int, len: Int): Tuple2[List[Address], List[Int]] = {
    if (count == len) {
      (List.empty, List.empty)
    } else {
      val byteAddress = data.takeWhile(_ != 0)
      val newData = data.dropWhile(_ != 0).tail
      val newCount = count + 1
      val newPosition = byteAddress.length + 1
      val destFlag = byteAddress.head
      val nextIter = getAddresses(newData, newCount, len)
      if (destFlag == SMEAddressFlag) {
        (SMEAddress().fromBytes(byteAddress) :: nextIter._1, newPosition :: nextIter._2)
      } else {
        (DistributionList().fromBytes(byteAddress) :: nextIter._1, newPosition :: nextIter._2)
      }
    }
  }

我的问题是

  1. 第一个尾递归吗?
  2. 它是如何工作的?我在最后一行调用了两次方法。是否评估将方法调用分开?
  3. 哪个版本效率更高,或者我如何更有效地编写它。
  4. 请原谅我,如果代码闻起来我是scala的新手。

    由于

4 个答案:

答案 0 :(得分:6)

这些都不是尾递归。只有当递归调用仅作为某个执行路径中的最后一个项时才会发生。如果您可以通过跳转到代码的开头来替换调用,保存没有状态但重新标记输入变量,那么它是尾递归。 (在内部,这正是编译器所做的。)

要将普通的递归函数转换为尾递归函数,当这样做时,您需要传递任何存储的数据,如下所示:

private def getAddresses(
  data: List[Int], count: Int, len: Int,    // You had this already
  addresses: List[Address] = Nil,           // Build this as we go, starting with nothing
  positions: List[Int] = Nil                // Same here
): (List[Address], List[Int]) {
  if (count==len) {
    (addresses.reverse, positions.reverse)  // Return what we've built, reverse to fix order
  }    
  else {
    val (addr,rest) = data.span(_ != 0)
    val newdata = rest.tail
    val position = addr.length + 1
    val flag = addr.head
    val address = (
      if (flag) SMEAddress().fromBytes(addr)
      else DistributionList().fromBytes(addr)
    )
    getAddresses(newdata, count+1, len, address :: addresses, position :: positions)
  }
}

如果其他所有版本相同,则尾递归版本比非尾递归版本更有效。 (在这种情况下,它可能不是因为列表必须在最后反转,但它具有巨大的优势,如果你使用大的len它不会溢出堆栈。)

两次调用方法总是运行两次。没有方法调用结果的自动记忆 - 这将是非常难以自动完成的。

答案 1 :(得分:1)

只是为了让它更加'Scala'y,你可以在getAddresses内部定义尾递归函数,就像这样

def getAddresses(data: List[Int], len: Int) = {
  def inner(count: Int, addresses: List[Address] = Nil, positions: List[Int] = Nil): (List[Address], List[Int]) = {
    if (count == len) {
     (addresses.reverse, positions.reverse)
    } else {
      val (byteAddress, rest) = data.span(_ != 0)
      val newData = rest.tail
      val newPosition = byteAddress.length + 1
      val destFlag = byteAddress.head
      val newAddress = (if (destFlag == SMEAddressFlag) SMEAddress else DistributionList()).fromBytes(byteAddress)
      inner(count+1, newAddress :: addresses, newPosition :: positions)
    }
  }

  inner(0)   //Could have made count have a default too
}

答案 2 :(得分:1)

由于您的所有输入都是不可变的,并且您生成的列表总是具有相同的长度,所以我想到了这个解决方案。

private def getAddresses(data:List[Int], count:Int, len:Int):Stream[(Address,Int)] = {
   if (count == len) {
      Stream.empty
   }else{
      val (byteAddress, _::newData) = data.span(_ != 0)
      val newAddress =
         if (byteAddress.head == SMEAddressFlag) SMEAddress().fromBytes(byteAddress)
         else DistributionList().fromBytes(byteAddress)

      (newAddress, byteAddress.length + 1) #:: getAddresses(newData, count+1, len)
}
  1. 它不返回一对列表,而是返回一对列表。这样可以轻松递归一次。如果您需要单独的列表,可以使用map来提取它们,但是您可以重新构造程序的其他部分,以便更清晰地使用对列表,而不是将2个列表作为参数全部使用。

  2. 它不返回列表,而是返回一个懒惰评估的流。此不是尾递归,但是对流进行延迟计算的方式也可以防止堆栈溢出。如果您需要严格的列表,可以在此函数的结果上调用toList

  3. 演示了其他有用的技巧,例如使用span模式匹配来计算单行代码中的byteAddressnewData。您可以添加一些我删除的val,如果它们的名称具有可读性是有用的。

答案 3 :(得分:0)

  1. 否。如果给定执行路径中的最后一次调用是递归调用,则该方法是尾递归的。此处评估的最后一个调用将是::,这不是递归调用,因此它不是尾递归。
  2. 是的,如果你两次调用一个方法,它将被评估两次。
  3. 第二个更高效,因为在这里你只调用一次方法。