簡體   English   中英

是否可以通過O(1)內存使用延遲遍歷遞歸數據結構,尾部調用優化?

[英]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代數是對於某些xfxx的函數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循環(傳入lastcurrent ,應該這樣做。

內置的后引用允許您跟蹤遍歷排序。 沒有這些,我想不出一種方法在一個小於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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM