Scala 使用while循环+;堆叠

Scala 使用while循环+;堆叠,scala,Scala,我有点不好意思承认这一点,但我似乎被一个简单的编程问题难住了。我正在构建一个决策树实现,并一直在使用递归来获取一个标记样本列表,递归地将列表一分为二,然后将其转换为一棵树 不幸的是,对于deeptrees,我遇到了堆栈溢出错误(ha!),所以我的第一个想法是使用continuations将其转换为尾部递归。不幸的是Scala不支持这种TCO,所以唯一的解决方案就是使用蹦床。蹦床似乎有点低效,我希望能有一些简单的基于堆栈的命令式解决方案来解决这个问题,但我很难找到它 递归版本看起来有点像(简化):

我有点不好意思承认这一点,但我似乎被一个简单的编程问题难住了。我正在构建一个决策树实现,并一直在使用递归来获取一个标记样本列表,递归地将列表一分为二,然后将其转换为一棵树

不幸的是,对于deeptrees,我遇到了堆栈溢出错误(ha!),所以我的第一个想法是使用continuations将其转换为尾部递归。不幸的是Scala不支持这种TCO,所以唯一的解决方案就是使用蹦床。蹦床似乎有点低效,我希望能有一些简单的基于堆栈的命令式解决方案来解决这个问题,但我很难找到它

递归版本看起来有点像(简化):

基本上,我会根据数据的某些特征,递归地将列表分为两部分,并传递一个使用过的特征列表,这样我就不会重复了——所有这些都是在“getSplittingFeature”函数中处理的,所以我们可以忽略它。代码非常简单!尽管如此,我还是很难找到一个基于堆栈的解决方案,它不只是使用闭包,而是有效地成为一个蹦床。我知道我们至少必须在堆栈中保留一些参数的“框架”,但我希望避免闭包调用

我明白了,我应该在递归解决方案中显式地写出调用堆栈和程序计数器为我隐式处理的内容,但是如果没有连续性,我就很难做到这一点。在这一点上,我只是好奇,这几乎与效率无关。所以,请不要提醒我,过早优化是万恶之源,基于蹦床的解决方案可能会很好地工作。我知道它可能会-这基本上是一个谜,因为它自己的缘故

有谁能告诉我,这类事情的规范while循环和基于堆栈的解决方案是什么

更新:基于Thipor Kong的优秀解决方案,我编写了一个基于while循环/堆栈/哈希表的算法实现,它应该是递归版本的直接翻译。这正是我想要的:

最后更新:我使用了顺序整数索引,并将所有内容放回数组而不是映射以提高性能,添加了maxDepth支持,最后有了一个与递归版本具有相同性能的解决方案(不确定内存使用情况,但我猜会更少):

private def trainTreeNoMaxDepth(startingSamples:Seq[Sample],startingMaxDepth:Int):DTree={
//使用arraybuffer作为稠密的可变整数索引映射-无IndexOutOfBoundsException,只需展开以适应
类型DenseIntMap[T]=ArrayBuffer[T]
def updateIntMap[@specialized T](ab:DenseIntMap[T],idx:Int,item:T,dfault:T=null.asInstanceOf[T])={

如果(ab.length只将二叉树存储在一个数组中,如上所述:对于节点
i
,左边的子节点进入
2*i+1
,右边的子节点进入
2*i+2
。当执行“向下”操作时,您会保留一个TODO集合,这些TODO仍然必须被拆分才能到达一个叶子。一旦只有叶子,就要向上(在数组中从右到左)要构建决策节点:

更新:一个经过清理的版本,它还支持存储在分支中的功能(类型参数B),更具功能性/完全纯粹,并支持ron建议的带有映射的稀疏树

更新2-3:节约节点ID的名称空间,并对ID类型进行抽象,以允许使用大型树。从流中获取节点ID

sealed trait DTree[A, B]
case class DTLeaf[A, B](a: A, b: B) extends DTree[A, B]
case class DTBranch[A, B](left: DTree[A, B], right: DTree[A, B], b: B) extends DTree[A, B]

def mktree[A, B, Id](a: A, b: B, split: (A, B) => Option[(A, A, B)], ids: Stream[Id]) = {
  @tailrec
  def goDown(todo: Seq[(A, B, Id)], branches: Seq[(Id, B, Id, Id)], leafs: Map[Id, DTree[A, B]], ids: Stream[Id]): (Seq[(Id, B, Id, Id)], Map[Id, DTree[A, B]]) =
    todo match {
      case Nil => (branches, leafs)
      case (a, b, id) :: rest =>
        split(a, b) match {
          case None =>
            goDown(rest, branches, leafs + (id -> DTLeaf(a, b)), ids)
          case Some((left, right, b2)) =>
            val leftId #:: rightId #:: idRest = ids
            goDown((right, b2, rightId) +: (left, b2, leftId) +: rest, (id, b2, leftId, rightId) +: branches, leafs, idRest)
        }
    }

  @tailrec
  def goUp[A, B](branches: Seq[(Id, B, Id, Id)], nodes: Map[Id, DTree[A, B]]): Map[Id, DTree[A, B]] =
    branches match {
      case Nil => nodes
      case (id, b, leftId, rightId) :: rest =>
        goUp(rest, nodes + (id -> DTBranch(nodes(leftId), nodes(rightId), b)))
    }

  val rootId #:: restIds = ids
  val (branches, leafs) = goDown(Seq((a, b, rootId)), Seq(), Map(), restIds)
  goUp(branches, leafs)(rootId)
}

// try it out

def split(xs: Seq[Int], b: Int) =
  if (xs.size > 1) {
    val (left, right) = xs.splitAt(xs.size / 2)
    Some((left, right, b + 1))
  } else {
    None
  }

val tree = mktree(0 to 1000, 0, split _, Stream.from(0))
println(tree)

从概念上讲,卸载到基于堆栈的实现(堆栈位于堆上)不是和蹦床一样吗?有点像,但蹦床意味着你要保持一个满是闭包的堆栈,我希望有一个解决方案,只使用满是数据的堆栈。可能标记数据,如StepOne(a,b,c),StepTwo(a,b,c)或多个堆栈或其他,但不涉及函数调用。对我的代码进行了另一次更改。节点ID的名称空间使用更经济,您可以插入自己类型的节点ID(或BigInt,如果您愿意)。当然,我忽略了您应用程序的所有内容。但是,当前使用决策树时,我对StackOverflow感到有点惊讶。这当然意味着您的树确实不平衡。这通常是过度训练的暗示。您应该防止树的扩展,因为树叶中的实例数量太少,没有任何意义。绝对同意。决策树中的stackoverflow意味着您几乎肯定把事情搞砸了。不过,拥有处理任意深度的选项还是不错的——我的代码读取方式基本上是如果(maxDepth<1000)buildTreeRecursive()或者buildTreeOperational(),那么每个DTBranch都需要一个“featureIndex”呢?这使得它变得相当棘手,因为要将所有的叶子变成树枝,我们需要它们的featureIndex,然后将这些树枝组合在一起,我们需要它们的featureIndex,等等。我认为这是一个正确的想法,所以我将使用它。你在下降时将featureIndex放入堆中(而不是无),以便在再次启动时将其用于创建DTBranch。太棒了!我将在一小时内尝试并将您的答案标记为答案。请注意,将二叉树存储为数组只有在树平衡的情况下才有效。严重倾斜的树不会很好地利用数组。但是可以使用基于关联数组的变体。我确认了这一点这将得到与非尾部递归算法相同的结果。谢谢!
private def trainTreeNoMaxDepth(startingSamples: Seq[Sample], startingMaxDepth: Int): DTree = {
  // Use arraybuffer as dense mutable int-indexed map - no IndexOutOfBoundsException, just expand to fit
  type DenseIntMap[T] = ArrayBuffer[T]
  def updateIntMap[@specialized T](ab: DenseIntMap[T], idx: Int, item: T, dfault: T = null.asInstanceOf[T]) = {
    if (ab.length <= idx) {ab.insertAll(ab.length, Iterable.fill(idx - ab.length + 1)(dfault)) }
    ab.update(idx, item)
  }
  var currentChildId = 0 // get childIdx or create one if it's not there already
  def child(childMap: DenseIntMap[Int], heapIdx: Int) =
    if (childMap.length > heapIdx && childMap(heapIdx) != -1) childMap(heapIdx)
    else {currentChildId += 1; updateIntMap(childMap, heapIdx, currentChildId, -1); currentChildId }
  // go down
  val leftChildren, rightChildren = new DenseIntMap[Int]() // heapIdx -> childHeapIdx
  val todo = Stack((startingSamples, Set.empty[Int], startingMaxDepth, 0)) // samples, usedFeatures, maxDepth, heapIdx
  val branches = new Stack[(Int, Int)]() // heapIdx, featureIdx
  val nodes = new DenseIntMap[DTree]() // heapIdx -> node
  while (!todo.isEmpty) {
    val (samples, usedFeatures, maxDepth, heapIdx) = todo.pop()
    if (shouldStop(samples) || maxDepth == 0) {
      updateIntMap(nodes, heapIdx, DTLeaf(makeProportions(samples)))
    } else {
      val featureIdx = getSplittingFeature(samples, usedFeatures)
      val (statsWithFeature, statsWithoutFeature) = samples.partition(hasFeature(featureIdx, _))
      todo.push((statsWithFeature, usedFeatures + featureIdx, maxDepth - 1, child(leftChildren, heapIdx)))
      todo.push((statsWithoutFeature, usedFeatures + featureIdx, maxDepth - 1, child(rightChildren, heapIdx)))
      branches.push((heapIdx, featureIdx))
    }
  }
  // go up
  while (!branches.isEmpty) {
    val (heapIdx, featureIdx) = branches.pop()
    updateIntMap(nodes, heapIdx, DTBranch(nodes(child(leftChildren, heapIdx)), nodes(child(rightChildren, heapIdx)), featureIdx))
  }
  nodes(0)
}
sealed trait DTree[A, B]
case class DTLeaf[A, B](a: A, b: B) extends DTree[A, B]
case class DTBranch[A, B](left: DTree[A, B], right: DTree[A, B], b: B) extends DTree[A, B]

def mktree[A, B, Id](a: A, b: B, split: (A, B) => Option[(A, A, B)], ids: Stream[Id]) = {
  @tailrec
  def goDown(todo: Seq[(A, B, Id)], branches: Seq[(Id, B, Id, Id)], leafs: Map[Id, DTree[A, B]], ids: Stream[Id]): (Seq[(Id, B, Id, Id)], Map[Id, DTree[A, B]]) =
    todo match {
      case Nil => (branches, leafs)
      case (a, b, id) :: rest =>
        split(a, b) match {
          case None =>
            goDown(rest, branches, leafs + (id -> DTLeaf(a, b)), ids)
          case Some((left, right, b2)) =>
            val leftId #:: rightId #:: idRest = ids
            goDown((right, b2, rightId) +: (left, b2, leftId) +: rest, (id, b2, leftId, rightId) +: branches, leafs, idRest)
        }
    }

  @tailrec
  def goUp[A, B](branches: Seq[(Id, B, Id, Id)], nodes: Map[Id, DTree[A, B]]): Map[Id, DTree[A, B]] =
    branches match {
      case Nil => nodes
      case (id, b, leftId, rightId) :: rest =>
        goUp(rest, nodes + (id -> DTBranch(nodes(leftId), nodes(rightId), b)))
    }

  val rootId #:: restIds = ids
  val (branches, leafs) = goDown(Seq((a, b, rootId)), Seq(), Map(), restIds)
  goUp(branches, leafs)(rootId)
}

// try it out

def split(xs: Seq[Int], b: Int) =
  if (xs.size > 1) {
    val (left, right) = xs.splitAt(xs.size / 2)
    Some((left, right, b + 1))
  } else {
    None
  }

val tree = mktree(0 to 1000, 0, split _, Stream.from(0))
println(tree)