Подтвердить что ты не робот

Можно ли лениво пересечь рекурсивную структуру данных с использованием памяти O (1), оптимизировать хвостовой вызов?

Скажем, что мы имеем рекурсивную структуру данных, как двоичное дерево. Есть много способов пройти его, и у них разные профили использования памяти. Например, если бы мы просто напечатали значение каждого узла, используя псевдокод, например следующее обход в порядке...

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;
}

Хотя мы никогда бы не взорвали стек, теперь мы увидим, что использование кучи увеличивается линейно по отношению к размеру дерева.

Скажем, мы должны были попытаться лениво пройтись по дереву - вот где мои рассуждения размываются. Я думаю, что даже используя базовую ленивую стратегию оценки, мы будем выращивать память с той же скоростью, что и оптимизированная версия tailcall. Вот конкретный пример, использующий класс 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), оптимизированный обход хвостового двоичного дерева двоичного дерева, возможно, без изменчивого состояния?

4b9b3361

Ответ 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)

Это принимает O (1) вспомогательное пространство, если мы предположим, что сборщик мусора очистит любой Node который мы только что обработали, поэтому мы фактически заменим его с помощью правильной версии. Однако, если обрабатываемые узлы не могут быть немедленно собраны в мусор, то заключительное предложение может создать число узлов O (n) до того, как оно попадет на лист.

Если у вас есть родительские указатели, то это также выполнимо. Однако родительские указатели требуют изменчивого состояния и предотвращения совместного использования поддеревьев, поэтому они не очень функциональны. Если вы представляете итератор парой (cur, prev) которая изначально (root, nil), то вы можете выполнить итерацию, как описано здесь. Тем не менее, для выполнения этой работы вам нужен язык с сопоставлениями указателей.

Без родительских указателей и изменяемого состояния вам необходимо поддерживать некоторую структуру данных, которая, по крайней мере, отслеживает, где находится корень дерева и как туда добраться, поскольку вам понадобится такая структура в какой-то момент во время заказа или после заказа ВТП. Такая структура обязательно принимает Ω (d) пространство, где d - глубина дерева.

Ответ 2

Приятный ответ.

Мы можем использовать свободные монады для эффективного использования памяти.

    {-# LANGUAGE RankNTypes
               , MultiParamTypeClasses
               , FlexibleInstances
               , UndecidableInstances #-}

    class Algebra f x where
      phi :: f x -> x

Алгебра функтора f является функцией phi от fx до x для некоторого x. Например, любая монада имеет алгебру для любого объекта mx:

    instance (Monad m) => Algebra m (m x) where
      phi = join

Можно построить свободную монаду для любого функтора f (возможно, только какие-то функторы, такие как omega-cocomplete или некоторые такие, но все типы Haskell являются многочленными функторами, омега-кополными, поэтому утверждение, безусловно, верно для всех 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:

    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:

    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 - если дерево не хранится в памяти, узлы отбрасываются, как только они заменяются строкой, представляющей все поддерево.

Ответ 3

Учитывая, что у вас есть ссылки на родителей узлов, там есть хорошее решение, размещенное здесь. Замените цикл while хвостовым рекурсивным вызовом (проходящим last и current и это должно сделать это.

Встроенные обратные ссылки позволяют отслеживать порядок обхода. Без них я не могу придумать способ сделать это на (сбалансированном) дереве с вспомогательным пространством O(log(n)).

Ответ 4

Я не смог найти ответ, но у меня были некоторые указатели. Go посмотрите http://www.ics.uci.edu/~dan/pub.html, прокрутите вниз до

[33] Д. С. Хиршберг и С. С. Сейден, Алгоритм обхода ограниченного пространства, Письма по обработке информации 47 (1993)

Загрузите файл postscript, вам может потребоваться преобразовать его в PDF (мой просмотрщик ps не смог представить его правильно). Он упоминает на стр. 2 (Таблица 1) ряд алгоритмов и дополнительную литературу.