如果我知道节点及其父节点,如何构建树?
How to build the tree if I know nodes and their parents?
说我得到了一些节点和它们的直接父节点,比如:
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
方法?想了想,还是得到了一个优雅的解决方案。
更新:希望看到不可变数据的解决方案
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())
}
另一个实现是:
- 高效
- stack-not-overflowing
- 纯函数
.
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
}
与xiefei方案的主要区别在于:
- 每个节点只构建一次,毕竟他的children已经
已经构建,即没有
copy
调用
- 检测循环引用
- 所有发现都是通过高效的
Map
和 Set
操作实现的
所以它可能不是最简单的,但 50% 的生产就绪。
说我得到了一些节点和它们的直接父节点,比如:
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
方法?想了想,还是得到了一个优雅的解决方案。
更新:希望看到不可变数据的解决方案
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())
}
另一个实现是:
- 高效
- stack-not-overflowing
- 纯函数
.
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
}
与xiefei方案的主要区别在于:
- 每个节点只构建一次,毕竟他的children已经
已经构建,即没有
copy
调用 - 检测循环引用
- 所有发现都是通过高效的
Map
和Set
操作实现的
所以它可能不是最简单的,但 50% 的生产就绪。