DSA DSA In Depth
0%
lesson binary-treestraversalsdfs-bfs

Tree Representations & Recursive vs Iterative Pre/In/Post/Level-Order Traversals

7 min read Module 3.1: Binary Trees, Traversals, BSTs & Priority Heaps Updated Oct 23, 2026
📄 Cheat Sheet
Binary Tree Algorithms for Technical Interviews — Full Course
freeCodeCamp.org (Alvin Zablan) Binary Tree Algorithms for Technical Interviews — Full Course Watch on YouTube ↗

Erik Demaine establishes rigorous pointer representation semantics, subtree navigation invariants, and traversal orders as foundational primitive operations on hierarchical graphs.

0:00 / --:--

Memory Layouts: Explicit Pointers vs LCRS vs Eytzinger Implicit Arrays

Before analyzing traversal algorithms, a systems engineer must account for the memory footprint and cache behavior of the underlying node representation. In 64-bit architectures, every memory address consumes 8 bytes. A naive binary tree node storing a 64-bit key/payload plus left, right, and parent pointers occupies 32 bytes (plus allocator metadata header overhead of 8–16 bytes), meaning 60% to 75% of L1 cache capacity is spent storing pointers rather than keys.

Four structural representations dominate production systems:

  • Tri-Pointer Binary Node (left, right, parent): Requires $3N$ pointers. Out of the $2N$ child pointers in an $N$-node binary tree, exactly $N + 1$ are null (since only $N - 1$ nodes have a parent). The explicit parent pointer enables $O(1)$ amortized tree-walk iteration without an auxiliary stack.
  • Bi-Pointer Binary Node (left, right): Strips the parent pointer to save 8 bytes per node, requiring an explicit $O(h)$ stack for upward navigation or ancestor updates.
  • Left-Child Right-Sibling (LCRS / Knuth Transform): Encodes an arbitrary $k$-ary tree into a binary tree where node.firstChild points to the leftmost child and node.nextSibling points to the immediate right sibling. This avoids variable-length child vectors (std::vector<Node*>) in DOM trees and ASTs.
  • Implicit 1-Indexed Array (Eytzinger / Complete Binary Layout): Places the root at index $1$, with $\text{parent}(i) = \lfloor i/2 \rfloor$, $\text{left}(i) = 2i$, and $\text{right}(i) = 2i + 1$. Requires zero pointers and achieves optimal spatial locality for nearly-complete trees, though sparse skewed trees of height $h$ waste $O(2^h)$ space.
Operation / Traversal Time Complexity Auxiliary Space (Balanced) Auxiliary Space (Skewed Worst-Case) Hardware Bottleneck
Recursive DFS (Pre / In / Post) O(N) O(log N) O(N) 32–64B call-frame allocation + pointer chasing
Iterative Explicit Stack DFS O(N) O(log N) O(N) Contiguous heap vector push/pop (L1 resident)
Parent-Pointer Iterative Walk O(N) O(1) O(1) 8B extra pointer per node; zero stack traffic
Level-Order (BFS Queue) O(N) O(W) = O(N) O(1) Ring-buffer queue width reaches $\lceil N/2 \rceil$ at leaf level

Formal Specification: Null-Link Lemma & Euler Tour Tri-Visit

Null-Link Theorem: In any non-empty binary tree $T$ with $N$ nodes, each node contributes exactly 2 outgoing child slots, yielding $2N$ total pointer fields. Every node except the root is the target of exactly 1 child pointer, consuming $N - 1$ non-null pointers. Therefore, the number of null child pointers is strictly invariant regardless of tree topology:

$$\text{NullPointers}(T) = 2N - (N - 1) = N + 1$$

Euler Tour Unification: A depth-first walk traverses every tree edge $(u, v)$ exactly twice—once downward $(u \to v)$ and once upward $(v \to u)$, totaling $2(N - 1)$ edge transitions. Recording node $u$ upon first entry yields Pre-Order; recording $u$ upon returning from its left subtree yields In-Order; recording $u$ upon returning from its right subtree yields Post-Order.

Visualizing the Euler Tour Perimeter Walk

The SVG diagram below traces the continuous Euler contour around a binary tree. Notice how Pre-Order (green entry markers), In-Order (amber underside markers), and Post-Order (purple exit markers) are simply three phase-shifted projections of the exact same walk.

A B C D E Euler Sequence: A B D D D B E E E B A C C C A ● Pre-Order (1st visit): [A, B, D, E, C] ● In-Order (2nd visit): [D, B, E, A, C] ● Post-Order (3rd visit): [D, E, B, C, A] ● Level-Order (BFS Queue): [A, B, C, D, E] Null Child Links: 2(5) - 4 = 6 null pointers
flowchart LR subgraph IterativePostOrder["Single-Stack Post-Order State Machine"] A["curr != null ?"] -- "Yes" --> B["stack.push(curr) curr = curr.left"] A -- "No" --> C["peek = stack.top()"] C --> D["peek.right != null AND lastVisited != peek.right ?"] D -- "Yes (descend right)" --> E["curr = peek.right"] D -- "No (both subtrees done)" --> F["visit(peek) lastVisited = stack.pop()"] end
Morris O(1)-Space Threaded In-Order & Euler Tour Subtree Flattening
O(N) Time · O(1) Aux Space / Contiguous [tin, tout]

Trace how Morris Traversal temporarily rewires each in-order predecessor’s null right pointer (pred.right = curr) to climb back up without a call stack, and how Euler Tour timestamps flatten every subtree into a contiguous array slice [tin[u], tout[u]].

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Phase 1 (Create Thread): At curr=4, in-order predecessor in left subtree is pred=3. Since pred.right == null, set pred.right = curr(4) and advance curr = curr.left(2).
curr=4 2 5 1 pred=3 Thread: pred(3).right = curr(4)
Pointer Action
pred(3).right = curr(4)
Thread State
1 Active Thread (3 → 4)
In-Order Output
[]
Aux Call-Stack Space
O(1) Words
[Step 1] Phase 1 (Create Thread): At curr=4, in-order predecessor in left subtree is pred=3. Since pred.right == null, set pred.right = curr(4) and advance curr = curr.left(2).

Hardware Reality: Stack Frame Bloat vs Heap Vector & Pointer Chasing

A recursive function call on x86-64 pushes an 8-byte return address, saves frame/callee-saved registers (%rbp, %rbx, etc.), and aligns the stack pointer %rsp to a 16-byte ABI boundary—consuming 32 to 48 bytes per recursion level. On a degenerate tree of $N = 200{,}000$ nodes, recursive traversal consumes $\approx 9.6\text{ MB}$ of stack space, immediately crashing default Linux ($8\text{ MB}$) or Windows ($1\text{ MB}$) thread stacks with SIGSEGV. By contrast, an explicit std::vector<const Node*> stack stores only 8 bytes per node on the heap, runs $4\times$ denser inside L1 cache lines (8 node pointers per 64-byte line), and supports hardware linear stream prefetching during stack unwinds.

Production Implementations: Iterative Single-Stack Post-Order & Parent-Pointer $O(1)$ Space Walk

While iterative Pre-Order and In-Order are straightforward, single-stack Post-Order requires tracking lastVisited to distinguish returning from a left child versus returning from a right child. Furthermore, if nodes carry a parent pointer (as in std::map / Linux rb_node), we can traverse the entire tree in $O(N)$ time and strict $O(1)$ auxiliary memory without modifying any tree pointers.

// C++20: Single-Stack Iterative Post-Order + O(1) Auxiliary Space Parent-Pointer In-Order
#include <vector>

struct TreeNode {
    int key;
    TreeNode *left = nullptr, *right = nullptr, *parent = nullptr;
};

// 1. Single-Stack Iterative Post-Order (Zero recursion, 8B per active ancestor)
std::vector<int> postorderIterative(const TreeNode* root) {
    std::vector<int> out;
    std::vector<const TreeNode*> st;
    const TreeNode* curr = root;
    const TreeNode* lastVisited = nullptr;

    while (curr != nullptr || !st.empty()) {
        if (curr != nullptr) {
            st.push_back(curr);
            curr = curr->left;
        } else {
            const TreeNode* peek = st.back();
            // If right child exists and we haven't just returned from it, descend right
            if (peek->right != nullptr && lastVisited != peek->right) {
                curr = peek->right;
            } else {
                out.push_back(peek->key);
                lastVisited = peek;
                st.pop_back();
            }
        }
    }
    return out;
}

// 2. O(1) Space In-Order Walk using Parent Pointers (Read-only, thread-safe)
std::vector<int> inorderParentWalk(const TreeNode* root) {
    std::vector<int> out;
    const TreeNode* prev = nullptr;
    const TreeNode* curr = root;

    while (curr != nullptr) {
        const TreeNode* next = nullptr;
        if (prev == curr->parent) {               // Arrived from parent: go left first
            if (curr->left != nullptr) {
                next = curr->left;
            } else {
                out.push_back(curr->key);          // No left subtree: visit In-Order now
                next = (curr->right != nullptr) ? curr->right : curr->parent;
            }
        } else if (prev == curr->left) {           // Returned from left child: visit In-Order
            out.push_back(curr->key);
            next = (curr->right != nullptr) ? curr->right : curr->parent;
        } else {                                   // Returned from right child: ascend
            next = curr->parent;
        }
        prev = curr;
        curr = next;
    }
    return out;
}

Failure Modes & Adversarial Pitfalls

  • BFS vs DFS Pathological Asymmetry: Engineers often assume BFS (Level-Order) uses more memory than DFS. That holds on balanced trees where height $h = \lfloor \log_2 N \rfloor$ ($O(\log N)$ DFS stack) and leaf width $W = (N+1)/2$ ($O(N)$ BFS queue). On an adversarial skewed chain of $N$ nodes, however, the roles invert completely: BFS queue width is strictly $W = 1$ ($O(1)$ memory), whereas recursive or stack-based DFS requires $O(N)$ depth!
  • Two-Stack Post-Order Memory Double-Allocation: Implementing Post-Order by reversing a modified Pre-Order (Root, Right, Left) into a second stack allocates $O(N)$ peak auxiliary memory instead of $O(h)$. Always use the lastVisited single-stack state machine above to preserve the $O(h)$ auxiliary memory guarantee.
Check yourself Question 1 of 4
Concept Check Exact Count of Null Child Pointers in an N-Node Binary Tree recall

In any non-empty binary tree with N nodes where each node stores two child pointers (left and right), how many child pointer fields across the entire tree are null?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

A service parses untrusted user-supplied JSON ASTs into binary trees of N = 250,000 nodes. How do the peak auxiliary memory requirements of Iterative DFS vs. Queue-based BFS (Level-Order) compare across a perfectly balanced tree versus a degenerate skewed chain?

Key Takeaways

Every N-node binary tree contains exactly N + 1 null child pointers (2N total slots minus N - 1 incoming edges).
Pre-Order, In-Order, and Post-Order are phase-shifted projections of a single 2(N - 1)-edge Euler Tour perimeter walk.
Single-stack iterative Post-Order tracks a single lastVisited pointer to bound auxiliary space to O(h) rather than O(N).
DFS uses O(h) memory while BFS uses O(W) memory: balanced trees favor DFS space (O(log N) vs O(N)), whereas degenerate chains favor BFS space (O(1) vs O(N)).
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue