我有點尷尬地承認這一點,但我似乎很難被一個簡單的編程問題困住。我正在構建一個決策樹實現,並一直使用遞歸來獲取標記樣本的列表,遞歸地將列表分成兩半,然後將它變成一棵樹。使用while循環+堆棧編碼遞歸樹創建
不幸的是,對於深度樹,我遇到了堆棧溢出錯誤(ha!),所以我的第一個想法是使用continuation將其變爲尾遞歸。不幸的是,Scala不支持這種TCO,所以唯一的解決方案是使用蹦牀。蹦牀看起來效率不高,我希望有一些簡單的基於堆棧的命令式解決方案來解決這個問題,但是我很難找到它。
遞歸版本看起來有點像(簡體):
private def trainTree(samples: Seq[Sample], usedFeatures: Set[Int]): DTree = {
if (shouldStop(samples)) {
DTLeaf(makeProportions(samples))
} else {
val featureIdx = getSplittingFeature(samples, usedFeatures)
val (statsWithFeature, statsWithoutFeature) = samples.partition(hasFeature(featureIdx, _))
DTBranch(
trainTree(statsWithFeature, usedFeatures + featureIdx),
trainTree(statsWithoutFeature, usedFeatures + featureIdx),
featureIdx)
}
}
所以基本上我根據一些數據的功能遞歸細分名單一分爲二,並通過所使用的特徵列表,所以傳遞我不重複 - 這些都是在「getSplittingFeature」函數中處理的,所以我們可以忽略它。代碼非常簡單!不過,我很難找出一個基於堆棧的解決方案,它不僅僅使用閉包並且有效地成爲蹦牀。我知道我們至少必須在堆棧中保留很少的參數「框架」,但我想避免關閉調用。
我得到的是,我應該明確地寫出了callstack和程序計數器在遞歸解決方案中隱含地處理了什麼,但是如果沒有延續,我會遇到麻煩。在這一點上,它甚至不是效率,我只是好奇。所以,請不要提醒我,過早優化是萬惡之源,而基於蹦牀的解決方案可能會工作得很好。我知道它可能會 - 它本身就是一個難題。
任何人都可以告訴我什麼規範的while-loop-and-stack-based解決方案是這樣的嗎?
更新:基於Thipor Kong出色的解決方案,我編寫了一個基於while-loops/stacks/hashtable的算法實現,該算法應該是遞歸版本的直接轉換。這正是我正在尋找的:
最終更新:我已經使用了順序整數索引,以及將所有內容都放回到數組中,而不是用於性能的映射,增加了maxDepth支持,並最終獲得了相同的解決方案性能遞歸版本(不知道內存使用情況,但我猜以下):
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)
}
不是卸載到基於堆棧的實現(堆棧在堆上)在概念上與蹦牀相同嗎? – ron
排序,但蹦牀意味着你正在保持堆棧充滿關閉,我希望有一個解決方案,只使用堆棧的數據。也許標記爲StepOne(a,b,c),StepTwo(a,b,c)或多個堆棧或類似的數據,但不涉及函數調用。 – lvilnis
對我的代碼進行了另一項更改。節點id的名稱空間更經濟,您可以插入自己的節點id類型(或BigInt,如果您喜歡)。 –