【问题标题】:How to build the tree if I know nodes and their parents?如果我知道节点及其父母,如何构建树?
【发布时间】:2015-05-20 20:23:39
【问题描述】:

假设我已经获得了一些节点及其直接父节点,例如:

case class Mapping(name: String, parents: Seq[String] = Nil)

val mappings = Seq(
  Mapping("aaa"),
  Mapping("bbb"),
  Mapping("ccc"),
  Mapping("ddd", Seq("aaa", "bbb")),
  Mapping("eee", Seq("ccc")),
  Mapping("fff", Seq("ddd")),
  Mapping("ggg", Seq("aaa", "fff")),
  Mapping("hhh")
)

如何在 Scala 中编写一个基于它们构建树的函数?

def buildTrees(data: Seq[Mapping]): Seq[Node] = ???

case class Node(name: String, children: Seq[Node] = Nil)

val trees = buildTrees(mappings)

private val expectedTree = Seq(
  Node("aaa", Seq(
    Node("ggg"),
    Node("ddd", Seq(
      Node("fff", Seq(
        Node("ggg")
      ))))
  )),
  Node("bbb", Seq(
    Node("ddd", Seq(
      Node("fff", Seq(
        Node("ggg")
      ))))
  )),
  Node("ccc", Seq(
    Node("eee")
  )),
  Node("hhh", Seq())
)

if (trees == expectedTree) {
  println("OK")
} else {
  println("Not equal")
}

如何实现buildTrees方法?我想了一会儿,但可以得到一个优雅的解决方案。


更新:希望看到不可变数据的解决方案

【问题讨论】:

  • 这看起来很奇怪,因为可以在输出中复制相同的源节点。
  • 谢谢,问题已解决
  • 这听起来更像是一个有向无环图 (DAG) - 在树中,一个节点有 0 或 1 个父节点,而不是多个父节点。
  • @Bergi 这是一个有效的问题,因为结果数据绝对是一棵树。源数据中的重复项被克隆到不同的分支。
  • 听起来你不是在建树。映射和预期树都没有描述树(每个节点最多 1 个父节点)。

标签: scala tree


【解决方案1】:

另一个实现是:

  • 高效
  • 堆栈不溢出
  • 纯功能

.

import scala.collection.immutable.Queue

class CyclicReferences(val nodes: Seq[String])
  extends RuntimeException(f"elements withing cycle detected: ${nodes mkString ","}")

def buildTrees(data: Seq[Mapping]): Seq[Node] = {
  val parents = data.map(m => (m.name, m.parents)).toMap withDefaultValue Seq.empty
  val children = data.flatMap(m => m.parents map ((_, m.name))).groupBy(_._1).mapValues(_.map(_._2))

  def loop(queue: Queue[String], unresolved: Map[String, Set[String]], nodes: Map[String, Node]): TraversableOnce[Node] = queue match {
    case Seq() => if (unresolved.isEmpty) nodes.values else throw new CyclicReferences(unresolved.keys.toSeq)
    case key +: rest =>
      val (newQueue, newUnresolved) = ((rest, unresolved) /: parents(key)) { (pair, parent) =>
        val (queue, children) = pair
        val ch = children(parent) - key
        if (ch.isEmpty) (queue :+ parent, children - parent)
        else (queue, children.updated(parent, ch))
      }
      val node = Node(key, children.getOrElse(key, Seq.empty) map nodes)
      loop(newQueue, newUnresolved, nodes + (key -> node))
  }
  val initial = Queue(parents.keys.filter(key => !children.contains(key)).toSeq: _*)
  val unresolved = children mapValues (_.toSet) withDefaultValue Set.empty
  loop(initial, unresolved, Map()).filter(node => parents(node.name).isEmpty).toIndexedSeq
}

与协飞方案的主要区别在于:

  • 每个节点只构建一次,毕竟他的所有孩子都已经 已经构建,即没有copy 调用
  • 检测循环引用
  • 所有发现都通过高效的MapSet 操作实现

所以它可能不是最简单的,但 50% 的生产就绪。

【讨论】:

  • 不得不说代码复杂到我一步步调试也能看懂
  • @Freewind 好吧,对不起。我可以稍后尝试将其拆分为几个简单的函数,而不是折叠和 tailrec 的大组合
  • 现在我了解了基本思想。你从叶子开始,一棵一棵地处理,把新的小树放到nodes,把unresolved的叶子去掉,然后把新的叶子从unresolved放到queue,然后重复。谢谢~
  • @Freewind 完全正确。感谢您尽我所能解释它
【解决方案2】:
def buildTrees(data: Seq[Mapping]): Seq[Node] = {
  def attachToParents(newChild: Mapping, parents: Seq[Node]): Seq[Node] = {
    for (parent <- parents) yield {
      val attachedChildren = attachToParents(newChild, parent.children)
      if (newChild.parents.contains(parent.name))
        parent.copy(children = Node(newChild.name) +: attachedChildren)
      else 
        parent.copy(children = attachedChildren)
    }
  }

  @tailrec
  def helper(xs: Seq[Mapping], accu: Seq[Node]): Seq[Node] = xs match {
    case Seq() => accu
    case head +: tail => head match {
      case Mapping(name, Seq()) => helper(tail, accu :+ Node(name))
      case Mapping(name, parents) => helper(tail, attachToParents(head, accu))
    }
  }
  helper(data, Seq())
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-08
    • 2016-05-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多