【问题标题】:Tricky :: re-write a function to be tail-recursiveTricky :: 将函数重写为尾递归
【发布时间】:2014-01-01 01:58:35
【问题描述】:

我有这个 scala 函数,由于性能问题,需要重写为尾递归。在处理不太大的数据集时堆栈会爆炸,因此我得出结论,只有通过使其尾递归才能修复它。 这是函数::

private def carve2(x: Int, y: Int) {

    var rand: Int = random.nextInt(4)

    (1 to 4) foreach { _ =>
        val (x1, y1, x2, y2) = randomize(x, y, rand)

        if (canUpdate(x1, y1, x2, y2)) {
           maze(y1)(x1) = 0
           maze(y2)(x2) = 0
           carve2(x2, y2)
       }
       rand = (rand + 1) % 4
    }

}

主要问题是::

> How to get rid of the foreach/for loop

为此,我尝试了一种递归方法,但要获得正确的语义很棘手,特别是因为在 if 块内自调用之后,rand var 的值发生了变化...

我尝试的是将rand的状态修改推出身体,并在作为参数传递时对其进行变异::

def carve3(x: Int, y: Int, rand: Int)  {

  for (i <- 1 to 4) {

    val (x1, y1, x2, y2) = randomize(x, y, rand)

    if (canUpdate(x1, y1, x2, y2)) {
       maze(y1)(x1) = 0
       maze(y2)(x2) = 0
      if (i == 1) carve3(x2, y2, random.nextInt(4))
      else carve3(x2, y2, (rand + 1) % 4)
    }
 }
}

这不起作用...

还有一件事,我知道这种编码方法不起作用,但我正在努力实现……这是我尝试重构的代码。 此外,randomize 和 canUpdate 函数与此上下文无关。

有什么建议吗? 提前非常感谢...

【问题讨论】:

    标签: scala recursion functional-programming tail-recursion


    【解决方案1】:

    如果一个函数多次调用自身,它不能转换为尾递归函数。一个函数只有在递归调用是它做的最后一件事时才可以是尾递归的,所以它不需要记住任何东西。

    解决此类问题的标准技巧是使用堆而不是堆栈,方法是将要计算的任务保留在队列中。例如:

    private def carve2(x: Int, y: Int) {
        carve2(Seq((x, y)));
    }
    
    @annotation.tailrec
    private def carve2(coords: Seq[(Int,Int)]) {
        // pick a pair of coordinates
        val ((x, y), rest) = coords match {
          case Seq(x, xs@_*) => (x, xs);
          case _             => return; // empty
        }
        // This is functional approach, although perhaps slower.
        // Using a `while` loop instead would result in faster code.
        val rand: Int = random.nextInt(4)
        val add: Seq[(Int,Int)] =
          for(i <- 1 to 4;
              (x1, y1, x2, y2) = randomize(x, y, (i + rand) % 4);
              if (canUpdate(x1, y1, x2, y2))
             ) yield {
            // do something with `maze`
            // ...
            // return the new coordinates to be added to the queue
            (x2, y2)
          }
        // the tail-recursive call
        carve2(rest ++ add);
    }
    

    (我没有尝试编译代码,因为您发布的代码示例不是独立的。)

    这里carve2 在尾递归循环中运行。每次传递都可能在队列末尾添加新坐标,并在队列为空时结束。

    【讨论】:

    • 谢谢彼得!我会尽快尝试你的方法:-)
    【解决方案2】:

    我假设您的分析器已将其识别为热点 :)

    正如您正确推断的那样,您的问题是 for-comprehension,它为每次循环添加了额外的间接级别。仅 4 次传递的成本可以忽略不计,但我可以看到该方法以递归方式调用自身......

    不会做的是首先尝试重构以使用尾递归,您可以先尝试两个更好的选择:

    1. foreach 更改为for-comprehension,然后使用optimize 标志进行编译,这会导致编译器发出while 循环。

    2. 如果这没有帮助,请手动将理解转换为 while 循环。

    然后...只有到那时,您可能想尝试尾递归,看看它是否比 while 循环更快。很有可能不会。

    更新

    无论如何,我正朝着 Petr 的解决方案前进 :)

    所以这是整理后的完整内容,使其更加惯用:

    private def carve2(x: Int, y: Int) {
      carve2(List((x, y)))
    }
    
    @tailrec private def carve2(coords: List[(Int,Int)]) = coords match {
      case (x,y) :: rest =>
        val rand: Int = random.nextInt(4)
    
        //note that this won't necessarily yield four pairs of co-ords
        //due to the guard condition
        val add = for {
          i <- 1 to 4
          (x1, y1, x2, y2) = randomize(x, y, (i + rand) % 4)
          if canUpdate(x1, y1, x2, y2)
        } yield {
          maze(y1)(x1) = 0
          maze(y2)(x2) = 0
          (x2, y2)
        }
    
        // tail recursion happens here...
        carve2(rest ++ add)
    
      case _ => 
    }
    

    【讨论】:

    • 四个递归调用,让它尾递归真的有帮助吗?
    • 不...但是这里的递归意味着它可能是 lot 超过 4 次
    • 哦不,我意识到调用的实际数量 total 超过 4,我建议尾递归优化只会删除 3 个调用中的一个,无论如何,其他人都必须在堆栈中进行管理。我想这值得检查,但我认为转换为尾递归不会有太大作用。
    • 谢谢凯文,我会试试你的建议。至于我遇到的问题 - 在处理不太大的数据集时堆栈会爆炸,因此我得出结论只能通过使其尾递归来修复它......我将编辑我的帖子。
    • 好的,这是一个不同的问题。我会相应地更新答案:)
    猜你喜欢
    • 2020-09-16
    • 2019-09-14
    • 2016-01-06
    • 1970-01-01
    • 2017-12-01
    • 2013-05-05
    • 1970-01-01
    • 2016-01-14
    相关资源
    最近更新 更多