【问题标题】:Tail recursive fold on a binary tree in ScalaScala中二叉树上的尾递归折叠
【发布时间】:2017-01-03 09:32:46
【问题描述】:

我正在尝试为二叉树找到尾递归折叠函数。给定以下定义:

// From the book "Functional Programming in Scala", page 45
sealed trait Tree[+A]
case class Leaf[A](value: A) extends Tree[A]
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]

实现一个非尾递归函数非常简单:

def fold[A, B](t: Tree[A])(map: A => B)(red: (B, B) => B): B =
  t match {
    case Leaf(v)      => map(v)
    case Branch(l, r) => 
      red(fold(l)(map)(red), fold(r)(map)(red))
  }

但现在我正在努力寻找一个尾递归折叠函数,以便可以使用注解@annotation.tailrec

在我的研究中,我发现了几个例子,其中树上的尾递归函数可以例如使用自己的堆栈计算所有叶子的总和,然后基本上是List[Tree[Int]]。但据我所知,在这种情况下,它仅适用于加法,因为您首先评估运算符的左侧还是右侧并不重要。但对于广义的折叠,它是非常相关的。为了展示我的意图,这里有一些示例树:

val leafs = Branch(Leaf(1), Leaf(2))
val left = Branch(Branch(Leaf(1), Leaf(2)), Leaf(3))
val right = Branch(Leaf(1), Branch(Leaf(2), Leaf(3)))
val bal = Branch(Branch(Leaf(1), Leaf(2)), Branch(Leaf(3), Leaf(4)))
val cmb = Branch(right, Branch(bal, Branch(leafs, left)))
val trees = List(leafs, left, right, bal, cmb)

基于这些树,我想使用给定的折叠方法创建一个深层副本,例如:

val oldNewPairs = 
  trees.map(t => (t, fold(t)(Leaf(_): Tree[Int])(Branch(_, _))))

然后证明平等的条件适用于所有创建的副本:

val conditionHolds = oldNewPairs.forall(p => {
  if (p._1 == p._2) true
  else {
    println(s"Original:\n${p._1}\nNew:\n${p._2}")
    false
  }
})
println("Condition holds: " + conditionHolds)

有人可以指点一下吗?

您可以在 ScalaFiddle 找到此问题中使用的代码:https://scalafiddle.io/sf/eSKJyp2/15

【问题讨论】:

    标签: scala tree binary-tree tail-recursion fold


    【解决方案1】:

    如果您停止使用函数调用堆栈并开始使用由您的代码和累加器管理的堆栈,您可能会得到尾递归解决方案:

    def fold[A, B](t: Tree[A])(map: A => B)(red: (B, B) => B): B = {
    
      case object BranchStub extends Tree[Nothing]
    
      @tailrec
      def foldImp(toVisit: List[Tree[A]], acc: Vector[B]): Vector[B] =
        if(toVisit.isEmpty) acc
        else {
          toVisit.head match {
            case Leaf(v) =>
              val leafRes = map(v)
              foldImp(
                toVisit.tail,
                acc :+ leafRes
              )
            case Branch(l, r) =>
              foldImp(l :: r :: BranchStub :: toVisit.tail, acc)
            case BranchStub =>
              foldImp(toVisit.tail, acc.dropRight(2) ++   Vector(acc.takeRight(2).reduce(red)))
          }
        }
    
      foldImp(t::Nil, Vector.empty).head
    
    }
    

    这个想法是从左到右累积值,通过引入存根节点来跟踪父关系,并在存根节点出现时使用累加器的最后两个元素使用 red 函数减少结果在探索中发现。

    这个解决方案可以优化,但它已经是一个尾递归函数实现。

    编辑:

    可以通过将累加器数据结构更改为被视为堆栈的列表来稍微简化:

    def fold[A, B](t: Tree[A])(map: A => B)(red: (B, B) => B): B = {
    
      case object BranchStub extends Tree[Nothing]
    
      @tailrec
      def foldImp(toVisit: List[Tree[A]], acc: List[B]): List[B] =
        if(toVisit.isEmpty) acc
        else {
          toVisit.head match {
            case Leaf(v) =>
              foldImp(
                toVisit.tail,
                map(v)::acc 
              )
            case Branch(l, r) =>
              foldImp(r :: l :: BranchStub :: toVisit.tail, acc)
            case BranchStub =>
              foldImp(toVisit.tail, acc.take(2).reduce(red) :: acc.drop(2))
          }
        }
    
      foldImp(t::Nil, Nil).head
    
    }
    

    【讨论】:

    • 对于 ScalaFiddle 中的示例,您的程序不会终止。当我尝试在 Ammonite 中为 leafs 运行它时,我得到了 java.lang.OutOfMemoryError: Java heap space 错误。
    • @adamwy 你是对的,Branch 的情况下存在拼写错误,堆栈没有被消耗。我已经编辑了答案
    • 现在我在 ScalaFiddle 中得到了这个:Original: Branch(Leaf(1),Branch(Leaf(2),Leaf(3))) New: Branch(Branch(Leaf(1),Leaf(2)),Leaf(3)) Condition holds: false
    • @adamwy 此外,它没有实现相同的功能,而是叶子遍历。我会删除它,直到我修复它。
    • 谢谢,这对我有用。在scalafiddle.io/sf/D9fH7Qn/0 上查看更新的 ScalaFiddle
    猜你喜欢
    • 2014-12-17
    • 1970-01-01
    • 2018-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-19
    • 2014-03-29
    • 1970-01-01
    相关资源
    最近更新 更多