尾递归与尾递归重构

发布于 2024-10-22 05:09:26 字数 1705 浏览 7 评论 0原文

我有这个方法,

  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 新手。

谢谢

I have this method

  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)
      }
    }
  }

I am tempted to rewrite it thus

  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)
      }
    }
  }

My questions are

  1. Is the first one tail recursive?
  2. How does it work? I am calling the method twice in the last line. Does it evaluate to separate method calls what?
  3. Which version is more efficient, or how can I write it more efficiently.

Pardon me if the code smells I am new to scala.

Thanks

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

陪你搞怪i 2024-10-29 05:09:26

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

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

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

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

Neither of these is tail recursion. That only happens when recursive calls occur only as the very last item along some execution path. If you could replace the call by a jump to the start of the code, saving no state but relabeling the input variables, then it's tail recursion. (Internally, that's exactly what the compiler does.)

To convert an ordinary recursive function into a tail recursive function, when doing so is possible, you need to pass forward any stored data, like so:

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)
  }
}

Tail-recursive versions are more efficient than non-tail-recursive versions, if all else is equal. (In this case, it might not be since the list has to be reversed at the end, but it has the huge advantage that it won't overflow the stack if you use a large len.)

Calling a method twice always runs it twice. There is no automatic memoization of results of method calls--this would be extremely difficult to do automatically.

再见回来 2024-10-29 05:09:26

为了使其更加“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
}

Just to make it more 'Scala'y, you could define the tail-recursive function internally to the getAddresses like so

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
}
吃→可爱长大的 2024-10-29 05:09:26

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

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

Since all of your inputs are immutable, and your resulting lists will both always have the same length, I thought of this solution instead.

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. Instead of returning a pair of lists, it returns a list of pairs. This makes it easy to recurse once. If you need separate lists, you can use map to extract them, but you might be able to restruture other parts of your program to more cleanly work with a list of pairs rather than taking 2 lists as a parameter all over.

  2. Instead of returning a list, it returns a stream which is lazily evaluated. This isn't tail recursion, but the way that streams are lazily evaluated also prevents stack overflows. If you need a strict list, you can call toList on the result of this function.

  3. The demonstrates other useful techniques, for example the use of span with pattern matching to compute byteAddress and newData in a single line of code. You can add back some of the vals that I removed if it's useful to have their names for readability.

遮云壑 2024-10-29 05:09:26
  1. 否。如果给定执行路径中的最后一个调用是递归调用,则方法是尾递归的。这里评估的最后一个调用将是 ::,它不是递归调用,因此它不是尾递归。
  2. 是的,如果你调用一个方法两次,它就会被评估两次。
  3. 第二个更有效,因为在这里您只调用该方法一次。
  1. No. A method is tail-recursive if the last call in a given execution path is a recursive call. The last call evaluated here will be ::, which is not a recursive call, so it's not tail recursive.
  2. Yes, if you call a method twice, it will be evaluated twice.
  3. The second one is more efficient as here you're only calling the method once.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文