简体   繁体   中英

Recursive post order tree traversal without creating new nodes

I want to define a generalized tail recursive tree traversal that works for all kinds of multi-way trees. This works fine with pre-order and level-order, but I'm having trouble to implement post order traversals. Here is the multi-way tree I am working with:

多路树

Desired order: EKFBCGHIJDA

As long as I don't care about tail recursion post order traversal is easy:

 const postOrder = ([x, xs]) => { xs.forEach(postOrder); console.log(`${x}`); }; const Node = (x, ...xs) => ([x, xs]); const tree = Node("a", Node("b", Node("e"), Node("f", Node("k"))), Node("c"), Node("d", Node("g"), Node("h"), Node("i"), Node("j"))); postOrder(tree); 

The tail recursive approach, on the other hand, is quite cumbersome:

 const postOrder = (p, q) => node => { const rec = ({[p]: x, [q]: forest}, stack) => { if (forest.length > 0) { const [node, ...forest_] = forest; stack.unshift(...forest_, Node(x)); return rec(node, stack); } else { console.log(x); if (stack.length > 0) { const node = stack.shift(); return rec(node, stack); } else return null; } }; return rec(node, []); }; const Node = (x, ...xs) => ([x, xs]); const tree = Node("a", Node("b", Node("e"), Node("f", Node("k"))), Node("c"), Node("d", Node("g"), Node("h"), Node("i"), Node("j"))); postOrder(0, 1) (tree); 

In particular, I'd like to avoid creating new nodes so that I can traverse arbitrary trees without having to know anything about their constructors. Is there a way to do this and still remain tail recursive?

stack-safe

My first answer solves this problem by writing our own functional iterator protocol. Admittedly, I was eager to share this approach as it's something I've explored in the past. Writing your own data structures is really fun and it can yield creative solutions to your problem - and you'd be bored if I gave out the easy answers first, wouldn't you?

const Empty =
  Symbol ()

const isEmpty = x =>
  x === Empty

const postOrderFold = (f = (a, b) => a, acc = null, node = Empty) =>
{
  const loop = (acc, [ node = Empty, ...nodes ], cont) =>
    isEmpty (node)
      ? cont (acc)
      : ???
  return loop (acc, [ node ], identity)
}

const postOrderValues = (node = Empty) =>
  postOrderFold ((acc, node) => [ ...acc, Node.value (node) ], [], node)

console.log (postOrderValues (tree))
// [ 'e', 'k', 'f', 'b', 'c', 'g', 'h', 'i', 'j', 'd', 'a' ]

Full solution included below for other readers...

 const Node = (x, ...xs) => [ x, xs ] Node.value = ([ value, _ ]) => value Node.children = ([ _, children ]) => children const Empty = Symbol () const isEmpty = x => x === Empty const identity = x => x // tail recursive const postOrderFold = (f = (a, b) => a, acc = null, node = Empty) => { const loop = (acc, [ node = Empty, ...nodes ], cont) => isEmpty (node) ? cont (acc) : loop (acc, Node.children (node), nextAcc => loop (f (nextAcc, node), nodes, cont)) return loop (acc, [ node ], identity) } const postOrderValues = (node = Empty) => postOrderFold ((acc, node) => [ ...acc, Node.value (node) ], [], node) const tree = Node("a", Node("b", Node("e"), Node("f", Node("k"))), Node("c"), Node("d", Node("g"), Node("h"), Node("i"), Node("j"))) console.log (postOrderValues (tree)) // [ 'e', 'k', 'f', 'b', 'c', 'g', 'h', 'i', 'j', 'd', 'a' ] 

mutual recursion

Somehow it's your questions that allow me to canvas my most inspired works. Back in the headspace of tree traversals, I came up with this sort of pseudo-applicative sum type Now and Later .

Later does not have a proper tail call but I thought the solution was too neat not to share it

const Empty =
  Symbol ()

const isEmpty = x =>
  x === Empty

const postOrderFold = (f = (a, b) => a, acc = null, node = Empty) =>
{
  const Now = node =>
    (acc, nodes) =>
      loop (f (acc, node), nodes)

  const Later = node =>
    (acc, nodes) =>
      loop (acc, [ ...Node.children (node) .map (Later), Now (node), ...nodes ])

  const loop = (acc, [ reducer = Empty, ...rest ]) =>
    isEmpty (reducer)
      ? acc
      : reducer (acc, rest)

  // return loop (acc, [ ...Node.children (node) .map (Later), Now (node) ])
  // or more simply ...
  return Later (node) (acc, [])
}

Mutual recursion demonstration

 const Node = (x, ...xs) => [ x, xs ] Node.value = ([ value, _ ]) => value Node.children = ([ _, children ]) => children const Empty = Symbol () const isEmpty = x => x === Empty const postOrderFold = (f = (a, b) => a, acc = null, node = Empty) => { const Now = node => (acc, nodes) => loop (f (acc, node), nodes) const Later = node => (acc, nodes) => loop (acc, [ ...Node.children (node) .map (Later), Now (node), ...nodes ]) const loop = (acc, [ reducer = Empty, ...rest ]) => isEmpty (reducer) ? acc : reducer (acc, rest) // return loop (acc, [ ...Node.children (node) .map (Later), Now (node) ]) // or more simply ... return Later (node) (acc, []) } const postOrderValues = (node = Empty) => postOrderFold ((acc, node) => [ ...acc, Node.value (node) ], [], node) const tree = Node("a", Node("b", Node("e"), Node("f", Node("k"))), Node("c"), Node("d", Node("g"), Node("h"), Node("i"), Node("j"))) console.log (postOrderValues (tree)) // [ 'e', 'k', 'f', 'b', 'c', 'g', 'h', 'i', 'j', 'd', 'a' ] 

We start by writing Node.value and Node.children which get the two values from your Node

// -- Node -----------------------------------------------

const Node = (x, ...xs) =>
  [ x, xs ]

Node.value = ([ value, _ ]) =>
  value

Node.children = ([ _, children ]) =>
  children

Next, we create a generic Iterator type. This one imitates the native iterable behavior, only our iterators are persistent (immutable)

// -- Empty ----------------------------------------------

const Empty =
  Symbol ()

const isEmpty = x =>
  x === Empty

// -- Iterator -------------------------------------------

const Yield = (value = Empty, it = Iterator ()) =>
  isEmpty (value)
    ? { done: true }
    : { done: false, value, next: it.next }

const Iterator = (next = Yield) =>
  ({ next })

const Generator = function* (it = Iterator ())
{
  while (it = it.next ())
    if (it.done)
      break
    else
      yield it.value
}

Lastly, we can implement PostorderIterator

const PostorderIterator = (node = Empty, backtrack = Iterator (), visit = false) =>
  Iterator (() =>
    visit
      ? Yield (node, backtrack)
      : isEmpty (node)
        ? backtrack.next ()
        : Node.children (node)
            .reduceRight ( (it, node) => PostorderIterator (node, it)
                         , PostorderIterator (node, backtrack, true)
                         )
            .next ())

And we can see it working with your tree here

// -- Demo ---------------------------------------------

const tree =
  Node ("a",
    Node ("b",
      Node ("e"),
      Node ("f",
        Node ("k"))),
    Node ("c"),
    Node ("d",
      Node ("g"),
      Node ("h"),
      Node ("i"),
      Node ("j")));

const postOrderValues =
  Array.from (Generator (PostorderIterator (tree)), Node.value)

console.log (postOrderValues)
// [ 'e', 'k', 'f', 'b', 'c', 'g', 'h', 'i', 'j', 'd', 'a' ]

Program demonstration

 // -- Node ---------------------------------------------- const Node = (x, ...xs) => [ x, xs ] Node.value = ([ value, _ ]) => value Node.children = ([ _, children ]) => children // -- Empty --------------------------------------------- const Empty = Symbol () const isEmpty = x => x === Empty // -- Iterator ------------------------------------------ const Yield = (value = Empty, it = Iterator ()) => isEmpty (value) ? { done: true } : { done: false, value, next: it.next } const Iterator = (next = Yield) => ({ next }) const Generator = function* (it = Iterator ()) { while (it = it.next ()) if (it.done) break else yield it.value } const PostorderIterator = (node = Empty, backtrack = Iterator (), visit = false) => Iterator (() => visit ? Yield (node, backtrack) : isEmpty (node) ? backtrack.next () : Node.children (node) .reduceRight ( (it, node) => PostorderIterator (node, it) , PostorderIterator (node, backtrack, true) ) .next ()) // -- Demo -------------------------------------------- const tree = Node ("a", Node ("b", Node ("e"), Node ("f", Node ("k"))), Node ("c"), Node ("d", Node ("g"), Node ("h"), Node ("i"), Node ("j"))); const postOrderValues = Array.from (Generator (PostorderIterator (tree)), Node.value) console.log (postOrderValues) // [ 'e', 'k', 'f', 'b', 'c', 'g', 'h', 'i', 'j', 'd', 'a' ] 

The variadic children field makes the algorithm a little more complicated compare to a Node type that only has left and right fields

The simplified implementation of these iterators makes them a bit easier to compare. Writing support for variadic children in the other iterators is left as an exercise to the reader

// -- Node ---------------------------------------------

const Node = (value, left = Empty, right = Empty) =>
  ({ value, left, right })

// -- Iterators ----------------------------------------

const PreorderIterator = (node = Empty, backtrack = Iterator ()) =>
  Iterator (() =>
    isEmpty (node)
      ? backtrack.next ()
      : Yield (node,
          PreorderIterator (node.left,
            PreorderIterator (node.right, backtrack))))

const InorderIterator = (node = Empty, backtrack = Iterator (), visit = false) =>
  Iterator (() =>
    visit
      ? Yield (node, backtrack)
      : isEmpty (node)
        ? backtrack.next ()
        : InorderIterator (node.left,
            InorderIterator (node,
              InorderIterator (node.right, backtrack), true)) .next ())

const PostorderIterator = (node = Empty, backtrack = Iterator (), visit = false) =>
  Iterator (() =>
    visit
      ? Yield (node, backtrack)
      : isEmpty (node)
        ? backtrack.next ()
        : PostorderIterator (node.left,
            PostorderIterator (node.right,
              PostorderIterator (node, backtrack, true))) .next ())

And a very special LevelorderIterator , just because I think you can handle it

const LevelorderIterator = (node = Empty, queue = Queue ()) =>
  Iterator (() =>
    isEmpty (node)
      ? queue.isEmpty ()
        ? Yield ()
        : queue.pop ((x, q) =>
            LevelorderIterator (x, q) .next ())
      : Yield (node,
          LevelorderIterator (Empty,
            queue.push (node.left) .push (node.right))))

// -- Queue ---------------------------------------------

const Queue = (front = Empty, back = Empty) => ({
  isEmpty: () =>
    isEmpty (front),
  push: x =>
    front
      ? Queue (front, Pair (x, back))
      : Queue (Pair (x, front), back),
  pop: k =>
    front ? front.right ? k (front.left, Queue (front.right, back))
                        : k (front.left, Queue (List (back) .reverse () .pair, Empty))
          : k (undefined, undefined)
})

// -- List ----------------------------------------------

const List = (pair = Empty) => ({
  pair:
    pair,
  reverse: () =>
    List (List (pair) .foldl ((acc, x) => Pair (x, acc), Empty)),
  foldl: (f, acc) =>
    {  
      while (pair)
        (acc = f (acc, pair.left), pair = pair.right)
      return acc
    }
})

// -- Pair ----------------------------------------------
const Pair = (left, right) =>
  ({ left, right })

Over-engineered? Guilty. You can swap out the interfaces above for nothing but JavaScript primitives. Here we trade the lazy stream for an eager array of values

const postOrderValues = (node = Empty, backtrack = () => [], visit = false) =>
  () => visit
      ? [ node, ...backtrack () ]
      : isEmpty (node)
        ? backtrack ()
        : Node.children (node)
            .reduceRight ( (bt, node) => postOrderValues (node, bt)
                         , postOrderValues (node, backtrack, true)
                         )
            ()

postOrderValues (tree) () .map (Node.value)
// [ 'e', 'k', 'f', 'b', 'c', 'g', 'h', 'i', 'j', 'd', 'a' ]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM