假设我们有一个递归数据结构,就像二叉树一样。有许多方法可以遍历它,它们具有不同的内存使用配置文件。例如,如果我们只是打印每个节点的值,使用伪代码,如下面的有序遍历......
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)
,我认为这实际上会评估左右流的头部。即使它没有(由于追加聪明),我认为递归地构建这个thunk(从Haskell借用一个术语)本质上会以相同的速率增长内存。我不是说“将这个节点放在要遍历的节点列表中”,而是基本上说,“这是评估的另一个值,它会告诉你下一步要穿什么”,但结果是一样的;线性记忆增长。
我能想到的唯一策略是避免这种情况,即在每个节点中都有可变状态,声明已经遍历了哪些路径。这将允许我们有一个引用透明的函数,它说:“给定一个节点,我将告诉你下一个应该遍历的单个节点”,我们可以用它来构建一个O(1)迭代器。
是否有另一种方法可以实现O(1),对二叉树进行尾调优化遍历,可能没有可变状态?
答案 0 :(得分:12)
是否有另一种方法可以实现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 )个节点。
如果你有父指针,那么它也是可行的。但是,父指针需要可变状态,并且防止共享子树,因此它们实际上不起作用。如果您通过最初(cur, prev)
的{{1}}对表示迭代器,则可以按照here概述执行迭代。但是,你需要一种带指针比较的语言来实现这一目的。
如果没有父指针和可变状态,您需要维护一些数据结构,至少跟踪树根的位置以及如何到达那里,因为在有序或某些时候您需要这样的结构。后序遍历。这样的结构必然需要Ω( d )空间,其中<em> d 是树的深度。
答案 1 :(得分:8)
一个奇特的答案。
我们可以使用免费的monad来获得有效的内存利用率。
{-# LANGUAGE RankNTypes
, MultiParamTypeClasses
, FlexibleInstances
, UndecidableInstances #-}
class Algebra f x where
phi :: f x -> x
对于某些f
,仿函数phi
的代数是从f x
到x
的函数x
。例如,任何monad都有任何对象m x
的代数:
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
为仿函数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)
树是游戏T a
上的免费monad:
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 a b
构造了一个新的树,它必须具有与larsmans引用的解决方案相同的内存消耗特性 - 如果树没有保存在内存中,节点一被替换就被丢弃表示整个子树的字符串。
答案 2 :(得分:1)
鉴于你有节点父母的引用,有一个很好的解决方案here。用尾递归调用替换while循环(传入last
和current
,这应该这样做。
内置的反向引用允许您跟踪遍历排序。如果没有这些,我就无法想到在辅助空间小于O(log(n))
的(平衡)树上做到这一点的方法。
答案 3 :(得分:0)
我无法找到答案,但我得到了一些指示。去看看http://www.ics.uci.edu/~dan/pub.html,向下滚动到
[33] D.S. Hirschberg和S.S.Seiden,有界空间树遍历算法,信息处理快报47(1993)
下载postscript文件,您可能需要convert它到PDF(我的ps查看器无法正确显示)。它在第2页(表1)中提到了许多算法和其他文献。