scala中的拓扑排序

scala中的拓扑排序,scala,graph-algorithm,Scala,Graph Algorithm,我正在寻找一个很好的scala实现 溶液应稳定: 如果输入已排序,则输出应保持不变 算法应该是确定性的(hashCode不起作用) 我怀疑有些库可以做到这一点,但我不想因此而添加非平凡的依赖项 示例问题: case class Node(name: String)(val referenced: Node*) val a = Node("a")() val b = Node("b")(a) val c = Node("c")(a) val d = Node("d")(b, c) val e

我正在寻找一个很好的scala实现

溶液应稳定:

  • 如果输入已排序,则输出应保持不变

  • 算法应该是确定性的(hashCode不起作用)

我怀疑有些库可以做到这一点,但我不想因此而添加非平凡的依赖项

示例问题:

case class Node(name: String)(val referenced: Node*)

val a = Node("a")()
val b = Node("b")(a)
val c = Node("c")(a)
val d = Node("d")(b, c)
val e = Node("e")(d)
val f = Node("f")()

assertEquals("Previous order is kept", 
   Vector(f, a, b, c, d, e), 
   topoSort(Vector(f, a, b, c, d, e)))

assertEquals(Vector(a, b, c, d, f, e), 
   topoSort(Vector(d, c, b, f, a, e)))
这里定义的顺序是,如果节点是引用其他声明的编程语言中的声明,那么结果顺序将是
在声明之前不使用任何声明。

这里是我自己的解决方案。此外,它还返回在输入中检测到的可能循环

节点的格式不是固定的,因为调用者提供的访问者 将获取一个节点和一个回调,并为每个引用的节点调用回调

如果循环报告不是必需的,那么它应该很容易删除

import scala.collection.mutable

// Based on https://en.wikipedia.org/wiki/Topological_sorting?oldformat=true#Depth-first_search
object TopologicalSort {

  case class Result[T](result: IndexedSeq[T], loops: IndexedSeq[IndexedSeq[T]])

  type Visit[T] = (T) => Unit

  // A visitor is a function that takes a node and a callback.
  // The visitor calls the callback for each node referenced by the given node.
  type Visitor[T] = (T, Visit[T]) => Unit

  def topoSort[T <: AnyRef](input: Iterable[T], visitor: Visitor[T]): Result[T] = {

    // Buffer, because it is operated in a stack like fashion
    val temporarilyMarked = mutable.Buffer[T]()

    val permanentlyMarked = mutable.HashSet[T]()

    val loopsBuilder = IndexedSeq.newBuilder[IndexedSeq[T]]

    val resultBuilder = IndexedSeq.newBuilder[T]

    def visit(node: T): Unit = {
      if (temporarilyMarked.contains(node)) {

        val loopStartIndex = temporarilyMarked.indexOf(node)
        val loop = temporarilyMarked.slice(loopStartIndex, temporarilyMarked.size)
          .toIndexedSeq
        loopsBuilder += loop

      } else if (!permanentlyMarked.contains(node)) {

        temporarilyMarked += node

        visitor(node, visit)

        permanentlyMarked += node
        temporarilyMarked.remove(temporarilyMarked.size - 1, 1)
        resultBuilder += node
      }
    }

    for (i <- input) {
      if (!permanentlyMarked.contains(i)) {
        visit(i)
      }
    }

    Result(resultBuilder.result(), loopsBuilder.result())
  }
}
关于复杂性的一些思考: 此解决方案的最坏情况复杂度实际上高于O(n+m),因为对每个节点扫描临时标记的
数组

如果将
临时标记的
替换为例如
哈希集
,则渐近复杂性将得到改善

如果将标记直接存储在节点内部,则可以实现真正的O(n+m),但将它们存储在外部会使编写通用解决方案更容易

我没有运行任何性能测试,但我怀疑扫描临时标记的
数组不是问题,即使是在大型图形中,只要它们不是很深

Github上的示例代码和测试 我有非常相似的代码也是。这对于实验和探索实现是有用的

为什么要检测循环 检测循环可能很有用,例如在序列化情况下,大多数数据可以作为DAG处理,但循环可以通过某种特殊的安排处理


上一节中链接到的Github代码中的测试套件包含多个循环的各种情况。

这里是一个纯功能实现,仅当图形是非循环的时才返回拓扑顺序

case class Node(label: Int)
case class Graph(adj: Map[Node, Set[Node]]) {
  case class DfsState(discovered: Set[Node] = Set(), activeNodes: Set[Node] = Set(), tsOrder: List[Node] = List(),
                      isCylic: Boolean = false)

  def dfs: (List[Node], Boolean) = {
    def dfsVisit(currState: DfsState, src: Node): DfsState = {
      val newState = currState.copy(discovered = currState.discovered + src, activeNodes = currState.activeNodes + src,
        isCylic = currState.isCylic || adj(src).exists(currState.activeNodes))

      val finalState = adj(src).filterNot(newState.discovered).foldLeft(newState)(dfsVisit(_, _))
      finalState.copy(tsOrder = src :: finalState.tsOrder, activeNodes = finalState.activeNodes - src)
    }

    val stateAfterSearch = adj.keys.foldLeft(DfsState()) {(state, n) => if (state.discovered(n)) state else dfsVisit(state, n)}
    (stateAfterSearch.tsOrder, stateAfterSearch.isCylic)
  }

  def topologicalSort: Option[List[Node]] = dfs match {
    case (topologicalOrder, false) => Some(topologicalOrder)
    case _ => None
  }
}

后来,我制作了一个版本的代码,当数据中存在循环时,该代码在多次运行时是稳定的,但是由于它没有吸引任何投票,所以我不想在这里添加它。如果您需要更好地处理循环,请添加注释。我很想了解更多关于您的拓扑排序版本的信息。那么,它如何比现有的解决方案更好呢?循环是否总是以相同的顺序报告?如果有多个循环会发生什么?@user1276782很抱歉回答得太晚,但我在回答的末尾添加了一些东西,以防其他人感到奇怪。sbt依赖逻辑代码在检测循环的同时包含一些示例
case class Node(label: Int)
case class Graph(adj: Map[Node, Set[Node]]) {
  case class DfsState(discovered: Set[Node] = Set(), activeNodes: Set[Node] = Set(), tsOrder: List[Node] = List(),
                      isCylic: Boolean = false)

  def dfs: (List[Node], Boolean) = {
    def dfsVisit(currState: DfsState, src: Node): DfsState = {
      val newState = currState.copy(discovered = currState.discovered + src, activeNodes = currState.activeNodes + src,
        isCylic = currState.isCylic || adj(src).exists(currState.activeNodes))

      val finalState = adj(src).filterNot(newState.discovered).foldLeft(newState)(dfsVisit(_, _))
      finalState.copy(tsOrder = src :: finalState.tsOrder, activeNodes = finalState.activeNodes - src)
    }

    val stateAfterSearch = adj.keys.foldLeft(DfsState()) {(state, n) => if (state.discovered(n)) state else dfsVisit(state, n)}
    (stateAfterSearch.tsOrder, stateAfterSearch.isCylic)
  }

  def topologicalSort: Option[List[Node]] = dfs match {
    case (topologicalOrder, false) => Some(topologicalOrder)
    case _ => None
  }
}