Создание дерева сумм двоичного дерева 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

У меня появилось ощущение, что это может быть реализовано лучше, возможно, у вас есть предложения?

Ответ 1

Прежде всего, я верю, и если я могу так сказать, вы проделали очень хорошую работу. Я могу предложить пару небольших изменений в вашем коде:

abstract class Tree 
case class Node(value: Int, left: Tree, right: Tree) extends Tree
case object Nil extends Tree
  • Дерево не обязательно должно быть классом case, кроме использования case-класса, поскольку не-лист node устарел из-за возможного ошибочного поведения автоматически генерируемых методов.
  • Nil является одноэлементным и лучше всего определяется как case-объект вместо case-class.
  • Дополнительно рассмотрим квалификационный суперкласс Tree с sealed. sealed сообщает компилятору, что класс может наследоваться только из одного и того же исходного файла. Это позволяет компилятору выдавать предупреждения всякий раз, когда следующее выражение соответствия не является исчерпывающим - другими словами, не включает все возможные случаи.

    закрытое дерево абстрактных деревьев

Следующие два улучшения можно было бы сделать в 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 type inferer работает через выражение последовательно слева направо, говоря ему рано, что мы собираемся иметь дело с Tree, позволяет нам опустить информацию о типе при определении функционального литерала, который далее передается в 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)) 
  }
}

Рекурсия, с которой вы столкнулись, - это единственное возможное направление, которое позволяет вам пересекать дерево и создавать измененную копию неизменяемой структуры данных. Любые листовые узлы должны быть созданы сначала перед добавлением в корень, потому что отдельные узлы дерева неизменяемы, и все объекты, необходимые для построения node, должны быть известны до начала строительства: необходимо создать листовые узлы, прежде чем вы сможете создайте root node.

Ответ 2

Как пишет Влад, ваше решение имеет единственную общую форму, которую вы можете иметь с такой складкой.

Тем не менее существует способ избавиться от соответствия значения node, а не только его фактора. И лично я бы предпочел это именно так.

Вы используете соответствие, потому что не каждый результат, полученный из рекурсивной складки, несет с собой сумму. Да, не каждое дерево может нести его, Нилу не место для значения, но ваша своя не ограничивается деревьями, не так ли?

Итак, пусть:

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.

Ответ 3

Ваше решение, вероятно, более эффективно (конечно, использует меньше стека), но здесь рекурсивное решение, 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
}

Ответ 4

Вы, вероятно, уже начали свою домашнюю работу, но я думаю, что все же стоит отметить, что способ, которым ваш код (и код в других ответах) выглядит, является прямым результатом того, как вы моделировали двоичные деревья. Если вместо использования типа алгебраических данных (Tree, Node, Nil) вы прошли с определением рекурсивного типа, вам не пришлось бы использовать сопоставление шаблонов для разложения ваших двоичных деревьев. Здесь мое определение двоичного дерева:

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

Никакое сопоставление шаблонов не требуется везде - все из-за хорошего рекурсивного определения бинарных деревьев.