问题 创建二叉树scala的和树


对于家庭作业,我写了一些scala代码,其中我有以下类和对象(用于建模二叉树):

object Tree {
  def fold[B](t: Tree, e: B, n: (Int, B, B) => B): B = t match {
    case Node(value, l, r) => n(value,fold(l,e,n),fold(r,e,n))
    case _ => e
  }
  def sumTree(t: Tree): Tree = 
    fold(t, Nil(), (a, b: Tree, c: Tree) => {
      val left = b match {
        case Node(value, _, _) => value
        case _ => 0
      }
      val right = c match {
        case Node(value, _, _) => value
        case _ => 0
      }
      Node(a+left+right,b,c)
    })
}

abstract case class Tree
case class Node(value: Int, left: Tree, right: Tree) extends Tree
case class Nil extends Tree

我的问题是关于 sumTree 创建新树的函数,其中节点的值等于其子项的值加上它自己的值的总和。

我发现它看起来很难看,我想知道是否有更好的方法来做到这一点。如果我使用自顶向下工作的递归,这将更容易,但我无法想出这样的功能。

我必须实施 fold 函数,具有代码中的签名,以进行计算 sumTree

我觉得这可以用更好的方式实现,也许你有建议?


2914
2018-03-11 20:45


起源



答案:


首先,我相信,如果我可以这么说,你做得很好。我可以建议对您的代码进行一些细微的更改:

abstract class Tree 
case class Node(value: Int, left: Tree, right: Tree) extends Tree
case object Nil extends Tree
  1. Tree不需要是case-class,除了使用case-class作为非叶节点之外,由于可能出现的自动生成方法的错误行为而被弃用。
  2. Nil 是一个单例,最好定义为case-object而不是case-class。
  3. 另外考虑符合条件的超级班级 Tree 同 sealedsealed 告诉编译器该类只能从同一个源文件中继承。这使得编译器可以在以下匹配表达式不详尽时发出警告 - 换句话说,不包括所有可能的情况。

    密封的抽象类树

接下来的几个改进可以做到 sumTree

def sumTree(t: Tree) = {
  // create a helper function to extract Tree value
  val nodeValue: Tree=>Int = {
    case Node(v,_,_) => v
    case _ => 0
  }
  // parametrise fold with Tree to aid type inference further down the line
  fold[Tree](t,Nil,(acc,l,r)=>Node(acc + nodeValue(l) + nodeValue(r) ,l,r)) 
}

nodeValue 辅助函数也可以定义为(上面使用的替代符号是可能的,因为花括号中的一系列情况被视为函数文字):

def nodeValue (t:Tree) = t match {
  case Node(v,_,_) => v
  case _ => 0
}

接下来的一点改进是参数化 fold 方法用 Tree (fold[Tree])。因为Scala类型inferer按顺序从左到右通过表达式告诉它我们将要处理Tree's让我们在定义传递给的函数文字时省略类型信息 fold 进一步。

所以这里是完整的代码,包括建议:

sealed abstract class Tree
case class Node(value: Int, left: Tree, right: Tree) extends Tree
case object Nil extends Tree

object Tree {
  def fold[B](t: Tree, e: B, n: (Int, B, B) => B): B = t match {
    case Node(value, l, r) => n(value,fold(l,e,n),fold(r,e,n))
    case _ => e
  }
  def sumTree(t: Tree) = {
    val nodeValue: Tree=>Int = {
      case Node(v,_,_) => v
      case _ => 0
    }
    fold[Tree](t,Nil,(acc,l,r)=>Node(acc + nodeValue(l) + nodeValue(r) ,l,r)) 
  }
}

您提出的递归是唯一可能的方向,它允许您遍历树并生成不可变数据结构的修改副本。在添加到根之前,必须首先创建任何叶节点,因为树的各个节点是不可变的,构造节点所需的所有对象必须在构造之前知道:在创建root之前需要创建叶节点节点。


11
2018-03-12 04:59



非常感谢,特别是你答案的最后一点。 - roelio
@Vlad这真的很有帮助,但我真的不明白为什么需要一个方法 val nodeValue: Tree=>Int。任何人都可以解释为什么必须这样做? - Sander
@Sander, nodeValue 摘要重复代码,如果你查看问题中的原始代码,它包含两个独立的匹配表达式:首先是左边的,然后是右边的子树。在这一点上,可能会有点难以遵循代码,因为作者的意图是详细的混乱。使用具有描述性名称的单个辅助函数替换重复代码可以更好地反映意图并将代码切片为更易于管理的单元。关于Scala的好处是添加一个小帮助函数并将其范围限制在应用程序的直接位置是多么容易。 - Vlad Gudim
@VladGudim我明白它取代了匹配表达式。 (顺便说一句,我正在做同样的练习)。我很好奇为什么我不能使用我之前定义的求和方法(对所有节点值求和)。我之前使用它来实现sumTree而不使用fold方法,而是使用模式匹配。每当我尝试使用sum方法时,一些节点似乎被计算两次?据我了解nodeValue函数,它只返回当前一个节点正下方的2个节点的节点值? - Sander
@Sander,我不知道这个练习上下文,所以无法帮助。也许值得提交一个单独的问题,详细信息并可能链接到这个问题? - Vlad Gudim


答案:


首先,我相信,如果我可以这么说,你做得很好。我可以建议对您的代码进行一些细微的更改:

abstract class Tree 
case class Node(value: Int, left: Tree, right: Tree) extends Tree
case object Nil extends Tree
  1. Tree不需要是case-class,除了使用case-class作为非叶节点之外,由于可能出现的自动生成方法的错误行为而被弃用。
  2. Nil 是一个单例,最好定义为case-object而不是case-class。
  3. 另外考虑符合条件的超级班级 Tree 同 sealedsealed 告诉编译器该类只能从同一个源文件中继承。这使得编译器可以在以下匹配表达式不详尽时发出警告 - 换句话说,不包括所有可能的情况。

    密封的抽象类树

接下来的几个改进可以做到 sumTree

def sumTree(t: Tree) = {
  // create a helper function to extract Tree value
  val nodeValue: Tree=>Int = {
    case Node(v,_,_) => v
    case _ => 0
  }
  // parametrise fold with Tree to aid type inference further down the line
  fold[Tree](t,Nil,(acc,l,r)=>Node(acc + nodeValue(l) + nodeValue(r) ,l,r)) 
}

nodeValue 辅助函数也可以定义为(上面使用的替代符号是可能的,因为花括号中的一系列情况被视为函数文字):

def nodeValue (t:Tree) = t match {
  case Node(v,_,_) => v
  case _ => 0
}

接下来的一点改进是参数化 fold 方法用 Tree (fold[Tree])。因为Scala类型inferer按顺序从左到右通过表达式告诉它我们将要处理Tree's让我们在定义传递给的函数文字时省略类型信息 fold 进一步。

所以这里是完整的代码,包括建议:

sealed abstract class Tree
case class Node(value: Int, left: Tree, right: Tree) extends Tree
case object Nil extends Tree

object Tree {
  def fold[B](t: Tree, e: B, n: (Int, B, B) => B): B = t match {
    case Node(value, l, r) => n(value,fold(l,e,n),fold(r,e,n))
    case _ => e
  }
  def sumTree(t: Tree) = {
    val nodeValue: Tree=>Int = {
      case Node(v,_,_) => v
      case _ => 0
    }
    fold[Tree](t,Nil,(acc,l,r)=>Node(acc + nodeValue(l) + nodeValue(r) ,l,r)) 
  }
}

您提出的递归是唯一可能的方向,它允许您遍历树并生成不可变数据结构的修改副本。在添加到根之前,必须首先创建任何叶节点,因为树的各个节点是不可变的,构造节点所需的所有对象必须在构造之前知道:在创建root之前需要创建叶节点节点。


11
2018-03-12 04:59



非常感谢,特别是你答案的最后一点。 - roelio
@Vlad这真的很有帮助,但我真的不明白为什么需要一个方法 val nodeValue: Tree=>Int。任何人都可以解释为什么必须这样做? - Sander
@Sander, nodeValue 摘要重复代码,如果你查看问题中的原始代码,它包含两个独立的匹配表达式:首先是左边的,然后是右边的子树。在这一点上,可能会有点难以遵循代码,因为作者的意图是详细的混乱。使用具有描述性名称的单个辅助函数替换重复代码可以更好地反映意图并将代码切片为更易于管理的单元。关于Scala的好处是添加一个小帮助函数并将其范围限制在应用程序的直接位置是多么容易。 - Vlad Gudim
@VladGudim我明白它取代了匹配表达式。 (顺便说一句,我正在做同样的练习)。我很好奇为什么我不能使用我之前定义的求和方法(对所有节点值求和)。我之前使用它来实现sumTree而不使用fold方法,而是使用模式匹配。每当我尝试使用sum方法时,一些节点似乎被计算两次?据我了解nodeValue函数,它只返回当前一个节点正下方的2个节点的节点值? - Sander
@Sander,我不知道这个练习上下文,所以无法帮助。也许值得提交一个单独的问题,详细信息并可能链接到这个问题? - Vlad Gudim


正如弗拉德所写,你的解决方案是关于这种折叠的唯一一般形状。

仍有一种方法可以摆脱节点值匹配,而不仅仅是将其排除在外。我个人更喜欢这样。

你用 比赛 因为并非你从递归折叠得到的每一个结果都带有它的总和。是的,不是每棵树都能携带它,Nil没有价值的地方,但你的折叠不仅限于树木,是吗?

所以我们有:

case class TreePlus[A](value: A, tree: Tree)

现在我们可以像这样折叠它:

def sumTree(t: Tree) = fold[TreePlus[Int]](t, TreePlus(0, Nil), (v, l, r) => {
    val sum = v+l.value+r.value
    TreePlus(sum, Node(sum, l.tree, r.tree))
}.tree

当然了 TreePlus 因为我们拥有规范产品,所以并不是真的需要 Tuple2 在标准库中。


2
2018-03-12 08:45





您的解决方案可能更有效(当然使用更少的堆栈),但这是一个递归解决方案,fwiw

def sum( tree:Tree):Tree ={
  tree match{
    case Nil =>Nil
    case Tree(a, b, c) =>val left = sum(b)
                         val right = sum(c)
                         Tree(a+total(left)+total(right), left, right)
  }
}

def total(tree:Tree):Int = {
  tree match{
    case Nil => 0
    case Tree(a, _, _) =>a
}

1
2018-03-11 23:13



谢谢你的回答,我在另一个任务中做了类似的事情,我没有使用 fold 功能。但是使用了 fold 签名 (Tree, B, (Int, B, B) => B) => Tree 是这项任务的要求 - roelio
@Dave Griffith这两个解决方案都是递归的,但不会被Scala编译器优化,并且会使用相同数量的堆栈帧。在Roelio的解决方案中,递归涉及两者之间的交替调用 fold并传递给匿名函数 fold 作为第三个参数 - 另一个Scala编译器无法通过尾部调用消除进行优化的场景。 - Vlad Gudim


你可能已经完成了你的作业,但我认为仍然值得指出你的代码(以及其他人的答案中的代码)看起来像是你如何建模二叉树的直接结果。如果,而不是使用代数数据类型(TreeNodeNil),你已经使用了递归类型定义,你不必使用模式匹配来分解你的二叉树。这是我对二叉树的定义:

case class Tree[A](value: A, left: Option[Tree[A]], right: Option[Tree[A]])

如你所见,没有必要 Node 要么 Nil 在这里(后者只是得到了荣耀 null 无论如何 - 你的代码中不需要这样的东西,不是吗?)。

有了这样的定义, fold 本质上是一个单行:

def fold[A,B](t: Tree[A], z: B)(op: (A, B, B) => B): B =
  op(t.value, t.left map (fold(_, z)(op)) getOrElse z, t.right map (fold(_, z)(op)) getOrElse z)

sumTree 也短而甜:

def sumTree(tree: Tree[Int]) = fold(tree, None: Option[Tree[Int]]) { (value, left, right) =>
  Some(Tree(value + valueOf(left, 0) + valueOf(right, 0), left , right))
}.get

哪里 valueOf 帮助器定义为:

def valueOf[A](ot: Option[Tree[A]], df: A): A = ot map (_.value) getOrElse df

任何地方都不需要模式匹配 - 所有这些都是因为二进制树的递归定义很好。


1
2018-03-14 03:13