Создание суммы дерева бинарного дерева scala - PullRequest
12 голосов
/ 12 марта 2012

Для домашнего задания я написал некоторый 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

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

Ответы [ 4 ]

12 голосов
/ 12 марта 2012

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

abstract class Tree 
case class Node(value: Int, left: Tree, right: Tree) extends Tree
case object Nil extends Tree
  1. Дерево не обязательно должно быть case-классом, кроме того, использование case-класса в качестве неконечного узла устарело из-за возможного ошибочного поведения автоматически сгенерированных методов.
  2. Nil является синглтоном и лучше всего определяется как case-объект вместо case-class.
  3. Дополнительно рассмотрим квалификационный суперкласс 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 последовательно обрабатывает выражение слева направо, сообщая заранее, что мы будем иметь дело с древовидной структурой, мы опускаем информацию о типе при определении литерала функции, который передается в 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)) 
  }
}

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

2 голосов
/ 12 марта 2012

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

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

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

Итак, давайте:

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 в стандартной библиотеке.

1 голос
/ 14 марта 2012

Возможно, вы уже выполнили домашнее задание, но я думаю, что все же стоит указать, что то, как выглядит ваш код (и код в ответах других людей), является прямым результатом того, как вы смоделировали двоичные деревья. Если бы вместо использования алгебраического типа данных (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

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

1 голос
/ 12 марта 2012

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