【问题标题】:How to make tree mapping tail-recursive?如何使树映射尾递归?
【发布时间】:2019-07-29 06:47:41
【问题描述】:

假设我有一个像这样的树形数据结构:

trait Node { val name: String }
case class BranchNode(name: String, children: List[Node]) extends Node
case class LeafNode(name: String) extends Node

假设我还有一个映射叶子的函数:

def mapLeaves(root: Node, f: LeafNode => LeafNode): Node = root match {
  case ln: LeafNode => f(ln)
  case bn: BranchNode => BranchNode(bn.name, bn.children.map(ch => mapLeaves(ch, f)))
}

现在我正在尝试使这个函数 tail-recursive 但很难弄清楚如何去做。我已经阅读了这个answer,但仍然不知道如何使二叉树解决方案适用于多路树。

您将如何重写 mapLeaves 以使其尾递归?

【问题讨论】:

  • 我认为您需要一些中间格式,正如您在提到的“答案”中看到的 - 有一个列表(这也是与您的问题不同的结果)
  • 感谢您的建议。我同意我需要一些“待办事项清单”(如上述答案),但我不知道该怎么做。
  • 你能用cats或scalaz吗?
  • 我不希望(在这种特殊情况下)。

标签: scala recursion functional-programming tree tail-recursion


【解决方案1】:

使用称为trampoline 的技术可能更容易实现它。 如果您使用它,您将能够使用两个函数调用自身进行相互递归(使用tailrec,您只能使用一个函数)。与tailrec 类似,此递归将转换为普通循环。

蹦床在 scala.util.control.TailCalls 的 scala 标准库中实现。

import scala.util.control.TailCalls.{TailRec, done, tailcall}

def mapLeaves(root: Node, f: LeafNode => LeafNode): Node = {

  //two inner functions doing mutual recursion

  //iterates recursively over children of node
  def iterate(nodes: List[Node]): TailRec[List[Node]] = {
     nodes match {
       case x :: xs => tailcall(deepMap(x)) //it calls with mutual recursion deepMap which maps over children of node 
         .flatMap(node => iterate(xs).map(node :: _)) //you can flat map over TailRec
       case Nil => done(Nil)
     }
  }

  //recursively visits all branches
  def deepMap(node: Node):  TailRec[Node] = {
    node match {
      case ln: LeafNode => done(f(ln))
      case bn: BranchNode => tailcall(iterate(bn.children))
         .map(BranchNode(bn.name, _)) //calls mutually iterate
    }
  }

  deepMap(root).result //unwrap result to plain node
}

除了TailCalls,您还可以使用Cats 中的Evalscalaz 中的Trampoline

使用该实现功能没有问题:

def build(counter: Int): Node = {
  if (counter > 0) {
    BranchNode("branch", List(build(counter-1)))
  } else {
    LeafNode("leaf")
  }
}

val root = build(4000)

mapLeaves(root, x => x.copy(name = x.name.reverse)) // no problems

当我使用您的实现运行该示例时,它按预期导致了java.lang.StackOverflowError

【讨论】:

  • 感谢您介绍蹦床。我需要了解它们。
【解决方案2】:

“调用堆栈”和“递归”只是流行的设计模式,后来被整合到大多数编程语言中(因此变得大多“不可见”)。没有什么可以阻止您使用堆数据结构重新实现两者。所以,这是“显而易见的”1960 年代 TAOCP 复古风格的解决方案:

trait Node { val name: String }
case class BranchNode(name: String, children: List[Node]) extends Node
case class LeafNode(name: String) extends Node

def mapLeaves(root: Node, f: LeafNode => LeafNode): Node = {
  case class Frame(name: String, mapped: List[Node], todos: List[Node])
  @annotation.tailrec
  def step(stack: List[Frame]): Node = stack match {
    // "return / pop a stack-frame"
    case Frame(name, done, Nil) :: tail => {
      val ret = BranchNode(name, done.reverse)
      tail match {
        case Nil => ret
        case Frame(tn, td, tt) :: more => {
          step(Frame(tn, ret :: td, tt) :: more)
        }
      }
    }
    case Frame(name, done, x :: xs) :: tail => x match {
      // "recursion base"
      case l @ LeafNode(_) => step(Frame(name, f(l) :: done, xs) :: tail)
      // "recursive call"
      case BranchNode(n, cs) => step(Frame(n, Nil, cs) :: Frame(name, done, xs) :: tail)
    }
    case Nil => throw new Error("shouldn't happen")
  }
  root match {
    case l @ LeafNode(_) => f(l)
    case b @ BranchNode(n, cs) => step(List(Frame(n, Nil, cs)))
  }
}

尾递归step 函数采用带有“堆栈帧”的具体堆栈。 “堆栈帧”存储当前正在处理的分支节点的名称、已处理的子节点列表以及以后仍需处理的剩余节点的列表。这大致对应于递归 mapLeaves 函数的实际堆栈帧。

有了这个数据结构,

  • 从递归调用返回对应于解构Frame 对象,或者返回最终结果,或者至少使stack 缩短一帧。
  • 递归调用对应于将Frame 添加到stack 的步骤
  • 基本情况(在叶子上调用 f)不会创建或删除任何帧

一旦理解了通常不可见的堆栈帧是如何明确表示的,翻译就很简单,而且大多是机械的。

例子:

val example = BranchNode("x", List(
  BranchNode("y", List(
    LeafNode("a"),
    LeafNode("b")
  )),
  BranchNode("z", List(
    LeafNode("c"),
    BranchNode("v", List(
      LeafNode("d"),
      LeafNode("e")
    ))
  ))
))

println(mapLeaves(example, { case LeafNode(n) => LeafNode(n.toUpperCase) }))

输出(缩进):

BranchNode(x,List(
  BranchNode(y,List(
    LeafNode(A),
    LeafNode(B)
  )),
  BranchNode(z, List(
    LeafNode(C),
    BranchNode(v,List(
      LeafNode(D),
      LeafNode(E)
    ))
  ))
))

【讨论】:

  • 非常感谢您提供如此详细的回答。我了解如何显式实现调用堆栈。现在我只是想知道如何简化这个解决方案。
猜你喜欢
  • 2011-07-24
  • 1970-01-01
  • 1970-01-01
  • 2011-07-04
  • 2017-12-31
  • 2020-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多