![](/img/trans.png)
[英]Why is this tail-call optimized method not recognized as such by the Scala compiler?
[英]Is it possible to lazily traverse a recursive data-structure with O(1) memory usage, tail-call optimized?
假設我們有一個遞歸數據結構,就像二叉樹一樣。 有許多方法可以遍歷它,它們具有不同的內存使用配置文件。 例如,如果我們只是打印每個節點的值,使用偽代碼,如下面的有序遍歷...
visitNode(node) {
if (node == null) return;
visitNode(node.leftChild);
print(node.value);
visitNode(node.rightChild);
}
...我們的內存使用量是常量,但由於遞歸調用,我們會增加調用堆棧的大小。 在非常大的樹上,這可能會溢出它。
假設我們決定針對調用堆棧大小進行優化; 假設這種語言能夠進行適當的尾調,我們可以將其重寫為以下預先遍歷...
visitNode(node, nodes = []) {
if (node != null) {
print(node.value);
visitNode(nodes.head, nodes.tail + [node.left, node.right]);
} else if (node == null && nodes.length != 0 ) {
visitNode(nodes.head, nodes.tail);
} else return;
}
雖然我們永遠不會破壞堆棧,但我們現在看到堆使用量相對於樹的大小線性增加。
假設我們當時試圖懶洋洋地遍歷樹 - 這是我的推理變得模糊的地方。 我認為即使使用基本的懶惰評估策略,我們也會以與尾部優化版本相同的速度增長內存。 下面是使用Scala的Stream類的具體示例,它提供了延遲評估:
sealed abstract class Node[A] {
def toStream: Stream[Node[A]]
def value: A
}
case class Fork[A](value: A, left: Node[A], right: Node[A]) extends Node[A] {
def toStream: Stream[Node[A]] = this #:: left.toStream.append(right.toStream)
}
case class Leaf[A](value: A) extends Node[A] {
def toStream: Stream[Node[A]] = this #:: Stream.empty
}
雖然只對流的頭部進行了嚴格的評估,但left.toStream.append(right.toStream)
評估了left.toStream.append(right.toStream)
, 我認為這實際上會評估左右流的頭部。 即使它沒有(由於追加聰明), 我認為以遞歸方式構建這個thunk(從Haskell借用一個術語)本質上會以相同的速率增長內存。 我不是說“將這個節點放在要遍歷的節點列表中”,而是基本上說,“這是評估的另一個值,它會告訴你下一步要穿什么”,但結果是一樣的; 線性記憶增長。
我能想到的唯一策略是避免這種情況,即每個節點都有可變狀態,聲明已經遍歷了哪些路徑。 這將允許我們有一個引用透明的函數,它說:“給定一個節點,我將告訴你下一個應該遍歷的單個節點”,我們可以用它來構建一個O(1)迭代器。
有沒有另一種方法可以實現O(1),二進制樹的尾調優化遍歷,可能沒有可變狀態?
有沒有另一種方法可以實現O(1),二進制樹的尾調優化遍歷,可能沒有可變狀態?
正如我在評論中所述,如果樹不需要在遍歷中存活,您就可以這樣做。 這是一個Haskell示例:
data T = Leaf | Node T Int T
inOrder :: T -> [Int]
inOrder Leaf = []
inOrder (Node Leaf x r) = x : inOrder r
inOrder (Node (Node l x m) y r) = inOrder $ Node l x (Node m y r)
如果我們假設垃圾收集器將清理我們剛剛處理的任何Node
,那么這需要O(1)輔助空間,因此我們有效地用右旋轉版本替換它。 但是,如果我們處理的節點不能立即被垃圾收集,那么final子句可能會在它到達葉子之前建立O( n )個節點數。
如果你有父指針,那么它也是可行的。 但是,父指針需要可變狀態,並且防止共享子樹,因此它們實際上不起作用。 如果您通過最初(root, nil)
對(cur, prev)
表示迭代器,那么您可以執行此處概述的迭代。 但是,您需要一種帶指針比較的語言才能使其工作。
如果沒有父指針和可變狀態,您需要維護一些數據結構,至少跟蹤樹根的位置以及如何到達那里,因為在順序或后序中的某個時刻您需要這樣的結構遍歷。 這種結構必然需要Ω( d )空間,其中d是樹的深度。
一個奇特的答案。
我們可以使用免費的monad來獲得有效的內存利用率限制。
{-# LANGUAGE RankNTypes
, MultiParamTypeClasses
, FlexibleInstances
, UndecidableInstances #-}
class Algebra f x where
phi :: f x -> x
仿函數f
代數是對於某些x
從fx
到x
的函數phi
。 例如,任何monad都有任何對象mx
的代數:
instance (Monad m) => Algebra m (m x) where
phi = join
任何仿函數f
都可以構造一個免費的monad(可能只有某種類型的仿函數,比如omega-cocomplete,或者其他類似的東西;但是所有的Haskell類型都是多項式函子,它們是omega-cocomplete,所以這個陳述對所有人來說都是正確的Haskell仿函數):
data Free f a = Free (forall x. Algebra f x => (a -> x) -> x)
runFree g (Free m) = m g
instance Functor (Free f) where
fmap f m = Free $ \g -> runFree (g . f) m
wrap :: (Functor f) => f (Free f a) -> Free f a
wrap f = Free $ \g -> phi $ fmap (runFree g) f
instance (Functor f) => Algebra f (Free f a) where
phi = wrap
instance (Functor f) => Monad (Free f) where
return a = Free ($ a)
m >>= f = fjoin $ fmap f m
fjoin :: (Functor f) => Free f (Free f a) -> Free f a
fjoin mma = Free $ \g -> runFree (runFree g) mma
現在我們可以使用Free
為functor T a
構建免費monad:
data T a b = T a b b
instance Functor (T a) where
fmap f (T a l r) = T a (f l) (f r)
對於這個仿函數,我們可以為對象[a]
定義代數
instance Algebra (T a) [a] where
phi (T a l r) = l++(a:r)
一棵樹是一個免費的monad over functor T a
:
type Tree a = Free (T a) ()
它可以使用以下函數構造(如果定義為ADT,它們是構造函數名稱,所以沒什么特別的):
tree :: a -> Tree a -> Tree a -> Tree a
tree a l r = phi $ T a l r -- phi here is for Algebra f (Free f a)
-- and translates T a (Tree a) into Tree a
leaf :: Tree a
leaf = return ()
為了演示這是如何工作的:
bar = tree 'a' (tree 'b' leaf leaf) $ tree 'r' leaf leaf
buz = tree 'b' leaf $ tree 'u' leaf $ tree 'z' leaf leaf
foo = tree 'f' leaf $ tree 'o' (tree 'o' leaf leaf) leaf
toString = runFree (\_ -> [] :: String)
main = print $ map toString [bar, buz, foo]
當runFree遍歷樹以用[]
替換leaf ()
時,所有上下文中T a [a]
的代數是構造表示樹的有序遍歷的字符串的代數。 因為仿函數T ab
構造了一個新的樹,它必須具有與larsmans引用的解決方案相同的內存消耗特性 - 如果樹沒有保存在內存中,一旦它們被表示的字符串替換,節點就會被丟棄整個子樹。
既然你不得不節點家長參考,有發布了很好的解決方案在這里 。 用尾遞歸調用替換while循環(傳入last
和current
,應該這樣做。
內置的后引用允許您跟蹤遍歷排序。 沒有這些,我想不出一種方法在一個小於O(log(n))
輔助空間的(平衡)樹上做到這一點。
我無法找到答案,但我得到了一些指示。 去看看http://www.ics.uci.edu/~dan/pub.html ,向下滾動到
[33] DS Hirschberg和SS Seiden,有界空間樹遍歷算法,信息處理快報47(1993)
下載postscript文件,您可能需要將其轉換為PDF(我的ps查看器無法正確顯示)。 它在第2頁(表1)中提到了許多算法和其他文獻。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.