10

N ツリー データ構造からウィジェットのリストを返そうとしています。私の単体テストでは、それぞれが 1 つの依存関係を持つ約 2000 個のウィジェットがある場合、スタック オーバーフローが発生します。私が考えているのは、for ループが原因でツリー トラバーサルが末尾再帰にならなくなっていることです。これをscalaで書くより良い方法は何ですか? これが私の機能です:

protected def getWidgetTree(key: String) : ListBuffer[Widget] = {
  def traverseTree(accumulator: ListBuffer[Widget], current: Widget) : ListBuffer[Widget] = {
    accumulator.append(current)

    if (!current.hasDependencies) {
      accumulator
    }  else {
      for (dependencyKey <- current.dependencies) {
        if (accumulator.findIndexOf(_.name == dependencyKey) == -1) {
          traverseTree(accumulator, getWidget(dependencyKey))
        }
      }

      accumulator
    }
  }

  traverseTree(ListBuffer[Widget](), getWidget(key))
}
4

3 に答える 3

10

末尾再帰ではない理由は、関数内で複数の再帰呼び出しを行っているためです。末尾再帰にするために、再帰呼び出しは関数本体の最後の式のみにすることができます。結局のところ、要点は、これが while ループのように機能する (したがって、ループに変換できる) ということです。ループは、1 回の繰り返しで自分自身を複数回呼び出すことはできません。

このようなツリー トラバーサルを行うには、キューを使用して、アクセスする必要があるノードを繰り越すことができます。

次のツリーがあるとします。

//        1
//       / \  
//      2   5
//     / \
//    3   4

次の単純なデータ構造で表されます。

case class Widget(name: String, dependencies: List[String]) {
  def hasDependencies = dependencies.nonEmpty
}

そして、各ノードを指すこのマップがあります。

val getWidget = List(
  Widget("1", List("2", "5")),
  Widget("2", List("3", "4")),
  Widget("3", List()),
  Widget("4", List()),
  Widget("5", List()))
  .map { w => w.name -> w }.toMap

これで、メソッドを末尾再帰に書き換えることができます。

def getWidgetTree(key: String): List[Widget] = {
  @tailrec
  def traverseTree(queue: List[String], accumulator: List[Widget]): List[Widget] = {
    queue match {
      case currentKey :: queueTail =>        // the queue is not empty
        val current = getWidget(currentKey)  // get the element at the front
        val newQueueItems =                  // filter out the dependencies already known
          current.dependencies.filterNot(dependencyKey => 
            accumulator.exists(_.name == dependencyKey) && !queue.contains(dependencyKey))
        traverseTree(newQueueItems ::: queueTail, current :: accumulator) // 
      case Nil =>                            // the queue is empty
        accumulator.reverse                  // we're done
    }
  }

  traverseTree(key :: Nil, List[Widget]())
}

そしてそれをテストしてください:

for (k <- 1 to 5)
  println(getWidgetTree(k.toString).map(_.name))

プリント:

ListBuffer(1, 2, 3, 4, 5)
ListBuffer(2, 3, 4)
ListBuffer(3)
ListBuffer(4)
ListBuffer(5)
于 2012-10-23T23:51:23.410 に答える
4

@dhgの回答と同じ例の場合、可変状態(ListBuffer)のない同等の末尾再帰関数は次のようになります。

case class Widget(name: String, dependencies: List[String])

val getWidget = List(
  Widget("1", List("2", "5")),
  Widget("2", List("3", "4")),
  Widget("3", List()),
  Widget("4", List()),
  Widget("5", List())).map { w => w.name -> w }.toMap

def getWidgetTree(key: String): List[Widget] = {
  def addIfNotAlreadyContained(widgetList: List[Widget], widgetNameToAdd: String): List[Widget] = {
    if (widgetList.find(_.name == widgetNameToAdd).isDefined) widgetList
    else                                                      widgetList :+ getWidget(widgetNameToAdd)
  }

  @tailrec
  def traverseTree(currentWidgets: List[Widget], acc: List[Widget]): List[Widget] = currentWidgets match {
    case Nil                                => {
      // If there are no more widgets in this branch return what we've traversed so far
      acc 
    }
    case Widget(name, Nil) :: rest          => {
      // If the first widget is a leaf traverse the rest and add the leaf to the list of traversed
      traverseTree(rest, addIfNotAlreadyContained(acc, name)) 
    }
    case Widget(name, dependencies) :: rest => {
      // If the first widget is a parent, traverse it's children and the rest and add it to the list of traversed
      traverseTree(dependencies.map(getWidget) ++ rest, addIfNotAlreadyContained(acc, name))
    } 
  }

  val root = getWidget(key)
  traverseTree(root.dependencies.map(getWidget) :+ root, List[Widget]())
}

同じテストケースの場合

for (k <- 1 to 5)
  println(getWidgetTree(k.toString).map(_.name).toList.sorted)

あなたにあげる:

List(2, 3, 4, 5, 1)
List(3, 4, 2)
List(3)
List(4)
List(5)

これはポストオーダーであり、プレオーダートラバーサルではないことに注意してください。

于 2012-10-24T00:43:29.010 に答える
1

素晴らしい!ありがとう。@tailrec アノテーションについて知りませんでした。それはかなりクールな小さな宝石です。自己参照を持つウィジェットが無限ループを引き起こしたため、ソリューションを少し調整する必要がありました。また、traverseTree への呼び出しがリストを予期していた場合、newQueueItems は Iterable だったので、そのビットを toList にする必要がありました。

def getWidgetTree(key: String): List[Widget] = {
  @tailrec
  def traverseTree(queue: List[String], accumulator: List[Widget]): List[Widget] = {
    queue match {
      case currentKey :: queueTail =>        // the queue is not empty
        val current = getWidget(currentKey)  // get the element at the front
        val newQueueItems =                  // filter out the dependencies already known
          current.dependencies.filter(dependencyKey =>
            !accumulator.exists(_.name == dependencyKey) && !queue.contains(dependencyKey)).toList
        traverseTree(newQueueItems ::: queueTail, current :: accumulator) //
      case Nil =>                            // the queue is empty
        accumulator.reverse                  // we're done
    }
  }

  traverseTree(key :: Nil, List[Widget]())
}
于 2012-10-24T20:56:03.677 に答える