【问题标题】:Tail Recursion Vs. Refactoring尾递归与。重构
【发布时间】:2011-03-16 02:06:59
【问题描述】:

我有这个方法

  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. 哪个版本更高效,或者我怎样才能更高效地编写它。

如果代码有异味请原谅我是 scala 的新手。

谢谢

【问题讨论】:

  • 在你的函数中添加@tailrecannotation 会让编译器告诉你它是否不是尾递归的。

标签: scala recursion tail-recursion


【解决方案1】:

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

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

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,它不会溢出堆栈。)

调用一个方法两次总是运行它两次。方法调用的结果没有自动记忆——这很难自动完成。

【讨论】:

  • 我仍然不完全理解这一点,但我很感激我很感激待办事项
  • 另一个问题。只是为了清楚这个尾递归是这个函数尾递归吗? def fact(n: Int): Int = { if(n <= 1) 1 else n * fact(n - 1) }。如果不是,为了让它尾递归,我必须做这样的事情def fact(n: Int, acc: Int = 1): Int = { if(n <= 1) acc else { val thisAcc = acc * n; fact(n -1, thisAcc) } }
  • @Aardvocate Akintayo Olusegun - 这是正确的 - 您的原始阶乘不是尾递归,但您的修改版本是。
【解决方案2】:

为了让它更“Scala”,你可以像这样在 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
}

【讨论】:

【解决方案3】:

由于您的所有输入都是不可变的,并且您的结果列表将始终具有相同的长度,因此我想到了这个解决方案。

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。您可以添加一些我删除的vals,如果它们的名称对可读性有用的话。

【讨论】:

  • 有趣的是我的老板打电话给我,并给了我相同的建议,即使用元组列表而不是相反。您的代码看起来很整洁,直到现在我才考虑使用 Stream。穆哈斯·格拉西亚斯
【解决方案4】:
  1. 没有。如果给定执行路径中的最后一个调用是递归调用,则该方法是尾递归的。此处评估的最后一个调用将是 ::,这不是递归调用,因此不是尾递归。
  2. 是的,如果您调用一个方法两次,它将被计算两次。
  3. 第二个效率更高,因为这里您只调用该方法一次。

【讨论】:

  • 1. ... 如果最后一次调用 ... 是该路径上的 only 递归调用。
  • @ziggystar:如果有多个递归调用并且最后一个是尾调用,那么该调用仍然会被 TCO 优化掉,不是吗?
  • 我想大多数人都对将递归函数调用变成循环的可能性感兴趣。当然,你是对的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-03-03
  • 2019-01-10
  • 2019-10-30
  • 1970-01-01
  • 2019-06-02
  • 1970-01-01
  • 2016-03-21
相关资源
最近更新 更多