COMPUTER SCIENCE FUNDAMENTALS SERIES

Binary Trees

Traversals · Tree properties · LCA ·
Morris traversal · Expression trees · Serialisation
Mid-level software engineer track  ·  20 slides
02

Tree Terminology

TermDefinition
RootTopmost node — has no parent
ParentNode directly above another in the hierarchy
ChildNode directly below — each node may have zero or more children
LeafNode with no children (external node)
Internal nodeNode with at least one child
EdgeLink between a parent and a child
DepthNumber of edges from root to node — root has depth 0
HeightLongest path from node down to a leaf — leaf has height 0
LevelSet of all nodes at the same depth
SubtreeA node and all its descendants
A B C D E F G H root leaf leaf

A tree with n nodes always has exactly n - 1 edges.

03

Binary Tree Definition & Properties

Definition

A binary tree is a tree in which every node has at most two children, conventionally labelled left and right.

Key properties

  • Maximum nodes at depth d: 2d
  • Maximum total nodes with height h: 2h+1 - 1
  • Minimum height for n nodes: floor(log2(n))
  • NULL pointers in tree with n nodes: n + 1
  • Full binary tree with n internal nodes has n + 1 leaves

Node structure

class TreeNode:
    def __init__(self, val=0,
                 left=None,
                 right=None):
        self.val   = val
        self.left  = left
        self.right = right

The recursive structure of TreeNode mirrors the recursive nature of every binary tree algorithm.

Why binary?

The two-child constraint yields elegant recursive decomposition: every problem on a binary tree reduces to solving left subtree + right subtree + combining at the root.

04

Full & Complete Binary Trees

Full binary tree

Every node has 0 or 2 children — no node has exactly one child.

  • With i internal nodes: i + 1 leaves, 2i + 1 total
  • With L leaves: 2L - 1 total nodes
        1
       / \
      2   3
     / \
    4   5

Complete binary tree

All levels filled except possibly the last, which is filled left to right.

  • Height: floor(log2(n)) — guarantees O(log n)
  • Array storage: node i has children at 2i+1 and 2i+2
  • Binary heaps are complete binary trees
        1
       / \
      2   3
     / \ /
    4  5 6

Complete trees give the most compact shape for a given number of nodes — this is why heaps use them.

05

Perfect, Balanced & Degenerate Trees

Perfect

Every internal node has 2 children. All leaves at the same depth.

  • Total nodes: 2h+1 - 1
  • Leaves: 2h
  • Both full and complete
      1
     / \
    2   3
   / \ / \
  4  5 6  7

Balanced

Height difference between left and right subtrees of every node is at most 1.

  • Guarantees O(log n) height
  • AVL, red-black trees maintain balance via rotations
      4
     / \
    2   6
   /   / \
  1   5   7

Degenerate

Every internal node has exactly one child — effectively a linked list.

  • Height = n - 1
  • All ops become O(n)
  • Why self-balancing trees exist
  1
   \
    2
     \
      3
       \
        4
06

Inorder Traversal

Left → Root → Right

Visit the left subtree, then the current node, then the right subtree. For a BST, inorder produces sorted output.

def inorder(root):
    if root is None:
        return
    inorder(root.left)
    visit(root.val)
    inorder(root.right)

Applications

  • Retrieve BST elements in sorted order
  • Expression tree evaluation (infix notation)
  • Validate whether a binary tree is a BST

Example

4 2 6 1 3 5 7

Inorder: 1, 2, 3, 4, 5, 6, 7  (sorted!)

Time: O(n). Space: O(h) for the recursion stack.

07

Preorder & Postorder Traversal

Preorder: Root → Left → Right

def preorder(root):
    if root is None:
        return
    visit(root.val)
    preorder(root.left)
    preorder(root.right)
  • First element is always the root
  • Used to serialise / copy a tree
  • Preorder sequence + structure info reconstructs the tree

Postorder: Left → Right → Root

def postorder(root):
    if root is None:
        return
    postorder(root.left)
    postorder(root.right)
    visit(root.val)
  • Last element is always the root
  • Delete a tree (children before parent)
  • Postfix expression evaluation
  • Compute directory sizes (children first)

Same tree:  Preorder: 4, 2, 1, 3, 6, 5, 7   |   Postorder: 1, 3, 2, 5, 7, 6, 4

08

Iterative Traversals

Recursion uses O(h) stack space implicitly. For very deep trees, this risks stack overflow. Iterative versions use an explicit stack.

Iterative inorder

def inorder_iterative(root):
    stack, current = [], root
    while stack or current:
        while current:
            stack.append(current)
            current = current.left
        current = stack.pop()
        visit(current.val)
        current = current.right

Iterative preorder

def preorder_iterative(root):
    if not root:
        return
    stack = [root]
    while stack:
        node = stack.pop()
        visit(node.val)
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)

Iterative postorder

Trickiest of the three. Two approaches: (1) use two stacks, or (2) reverse a modified preorder (Root → Right → Left) to get (Left → Right → Root). Both are O(n) time, O(h) space.

09

Level-Order Traversal (BFS)

Breadth-first, level by level

Visit all nodes at depth d before depth d + 1. Uses a queue instead of a stack.

from collections import deque

def level_order(root):
    if not root:
        return []
    result, queue = [], deque([root])
    while queue:
        level_size = len(queue)
        level = []
        for _ in range(level_size):
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        result.append(level)
    return result

Example

4Level 0
26Level 1
1357Level 2

Output: [[4], [2, 6], [1, 3, 5, 7]]

Applications

  • Finding minimum depth of a tree
  • Zigzag (spiral) traversal
  • Connecting next pointers at same level
  • Serialisation with NULL markers

Time: O(n). Space: O(w) where w is max width.

10

Morris Traversal

O(1) space, no stack, no recursion. Achieves O(n) time by temporarily modifying the tree using threaded pointers.

def morris_inorder(root):
    current = root
    while current:
        if current.left is None:
            visit(current.val)
            current = current.right
        else:
            # Find inorder predecessor
            pred = current.left
            while (pred.right and
                   pred.right != current):
                pred = pred.right

            if pred.right is None:
                # Create thread
                pred.right = current
                current = current.left
            else:
                # Remove thread, visit
                pred.right = None
                visit(current.val)
                current = current.right

How it works

1. No left child → visit, move right

2. Left child exists → find inorder predecessor (rightmost in left subtree)

3. Predecessor's right is NULL → create temporary thread back to current, go left

4. Predecessor's right points to current → thread exists, remove it, visit current, go right

The tree is restored to its original shape. Each edge is traversed at most twice.

11

Constructing Trees from Traversals

Which pairs uniquely determine a tree?

PairUnique tree?
Inorder + PreorderYes
Inorder + PostorderYes
Inorder + Level-orderYes
Preorder + PostorderOnly for full binary trees

Inorder is essential — it tells you which nodes belong to the left vs right subtree.

Algorithm: inorder + preorder

def build_tree(preorder, inorder):
    if not inorder:
        return None
    root_val = preorder.pop(0)
    root = TreeNode(root_val)
    mid = inorder.index(root_val)
    root.left  = build_tree(
        preorder, inorder[:mid])
    root.right = build_tree(
        preorder, inorder[mid+1:])
    return root

Optimisation

Use a hash map for O(1) index lookup and a pointer into preorder instead of pop(0). Reduces time from O(n2) to O(n).

12

Lowest Common Ancestor

Definition

The LCA of two nodes p and q is the deepest node that is an ancestor of both.

def lca(root, p, q):
    if (root is None or
        root == p or root == q):
        return root
    left  = lca(root.left, p, q)
    right = lca(root.right, p, q)
    if left and right:
        return root       # split point
    return left or right  # same side

Complexity

Time: O(n). Space: O(h) recursion stack.

How it works

If the current node is p or q, return it upward.

Recurse left and right. If both sides return non-NULL, current node is the LCA — p and q are in different subtrees.

If only one side returns non-NULL, the LCA is deeper in that subtree.

BST optimisation

If both values < root, go left. If both > root, go right. Otherwise root is the LCA. Time: O(h).

13

Diameter of a Binary Tree

Definition

The diameter is the longest path between any two nodes, measured in edges. The path may or may not pass through the root.

def diameter(root):
    max_d = 0

    def height(node):
        nonlocal max_d
        if node is None:
            return -1
        lh = height(node.left)
        rh = height(node.right)
        max_d = max(max_d, lh + rh + 2)
        return max(lh, rh) + 1

    height(root)
    return max_d

Key insight

At each node, the longest path through that node is left_height + right_height + 2. The diameter is the maximum across all nodes.

4 2 6 1 3 7 Diameter = 4 (path: 1→2→4→6→7)

Post-order accumulation pattern

Compute a local answer at each node while returning info upward. This single-pass DFS pattern applies to many tree problems.

14

Serialisation & Deserialisation

Preorder with NULL markers

def serialise(root):
    if root is None:
        return "# "
    return (str(root.val) + " "
            + serialise(root.left)
            + serialise(root.right))

def deserialise(data):
    tokens = iter(data.split())
    def build():
        val = next(tokens)
        if val == "#":
            return None
        node = TreeNode(int(val))
        node.left  = build()
        node.right = build()
        return node
    return build()

Example

    1
   / \
  2   3
     / \
    4   5

Serialised: "1 2 # # 3 4 # # 5 # #"

Alternative approaches

MethodNotes
Level-orderNULL markers per level; used by LeetCode
Inorder + preorderNo NULLs needed but two sequences
Parenthesised1(2)(3(4)(5)) — human readable

Serialisation is O(n) time and space regardless of approach. Essential for distributed systems, caching, and persistence.

15

Threaded Binary Trees

The NULL pointer problem

In a binary tree with n nodes, there are n + 1 NULL pointers (wasted space). Threaded binary trees repurpose these NULLs to point to inorder predecessors/successors.

TypeDescription
Single-threadedRight NULLs → inorder successor
Double-threadedLeft NULLs → predecessor, right NULLs → successor

Node structure

class ThreadedNode:
    def __init__(self, val):
        self.val = val
        self.left  = None
        self.right = None
        self.left_thread  = False
        self.right_thread = False

Benefits

  • Inorder traversal without stack/recursion — O(1) space
  • Inorder successor/predecessor in O(1) amortised
  • Morris traversal creates temporary threads on the fly

Threaded trees trade insertion/deletion complexity for traversal efficiency. Rarely used in practice but conceptually important — they inspired Morris traversal.

16

Expression Trees

Definition

A binary tree representing an arithmetic expression. Leaves are operands, internal nodes are operators.

* + - 3 4 5 2 (3 + 4) * (5 - 2) = 21

Traversals produce different notations

TraversalNotationOutput
InorderInfix3+4*5-2 (needs parens)
PreorderPrefix (Polish)* + 3 4 - 5 2
PostorderPostfix (RPN)3 4 + 5 2 - *

Building from postfix

def build_expr_tree(postfix):
    stack = []
    ops = {'+', '-', '*', '/'}
    for token in postfix:
        node = TreeNode(token)
        if token in ops:
            node.right = stack.pop()
            node.left  = stack.pop()
        stack.append(node)
    return stack[0]
17

Applications — File Systems

Directory trees

File systems are naturally tree-structured. Each directory is an internal node; files are leaves.

/
├── home/
│   ├── alice/
│   │   ├── docs/
│   │   │   └── report.pdf
│   │   └── .bashrc
│   └── bob/
│       └── code/
│           └── main.py
├── etc/
│   └── config.yaml
└── var/
    └── log/
        └── syslog

Tree operations in file systems

OperationTree algorithm
ls -RPreorder DFS
du -sPostorder DFS — children before parent
findBFS or DFS
rm -rfPostorder — delete children first
Path resolutionRoot-to-leaf traversal

Every cd, ls, and mkdir is a tree operation. Understanding tree traversals helps you reason about file system performance.

18

Applications — DOM & Decision Trees

The DOM (Document Object Model)

Every web page is a tree. The browser parses HTML into a tree of nodes.

document
└── html
    ├── head
    │   ├── title
    │   └── link
    └── body
        ├── div#header
        │   └── h1
        └── div#content
            ├── p
            └── ul
                ├── li
                └── li
  • CSS selectors traverse the tree
  • getElementById() is a tree search
  • DOM diffing (React, Vue) compares two trees

Decision trees

Each internal node is a yes/no question. Each leaf is a classification.

          Is temp > 30?
         /             \
       Yes              No
    Is humid?       Is windy?
     /    \          /     \
   Yes    No      Yes      No
  Stay  Beach    Stay     Park
 inside          inside
  • Each root-to-leaf path is a decision rule
  • Training: split data to maximise information gain
  • Random forests: ensemble of many decision trees
  • Used in medicine, finance, recommendations
19

Complexity Cheat Sheet

Time complexity for common operations

OperationAverageWorst (unbalanced)Balanced
SearchO(log n)O(n)O(log n)
InsertO(log n)O(n)O(log n)
DeleteO(log n)O(n)O(log n)
Traversal (any)O(n)O(n)O(n)
Find min/maxO(log n)O(n)O(log n)
LCAO(n)O(n)O(log n) with parent ptrs

Space complexity of traversals

MethodSpace
Recursive DFSO(h) — recursion stack
Iterative DFSO(h) — explicit stack
BFS (level-order)O(w) — queue width, up to O(n)
Morris traversalO(1) — no extra space

Interview tip

Always state whether the tree is balanced. It changes the answer from O(n) to O(log n) for most operations.

20

Summary & Further Reading

Key takeaways

  • A binary tree is defined by the constraint of at most two children per node
  • Tree shape matters: balanced gives O(log n), degenerate gives O(n)
  • Master all three DFS traversals in both recursive and iterative forms
  • Level-order (BFS) solves level-aware problems naturally
  • Morris traversal achieves O(1) space via temporary threads
  • LCA and diameter are canonical post-order accumulation problems
  • Serialisation converts structure to string and back — essential for distributed systems
  • Expression trees connect traversals to notation systems
  • Trees are everywhere: file systems, DOM, decision trees, compiler ASTs, database indexes

Recommended reading

SourceDescription
CLRSIntroduction to Algorithms — Ch. 12 (BSTs), Ch. 13 (Red-Black Trees)
SkienaThe Algorithm Design Manual — tree traversal and applications
LeetCodeTree tag — 200+ problems, easy to hard
KnuthTAOCP Vol. 1 — fundamental tree algorithms, threaded trees
SedgewickAlgorithms — BSTs, balanced trees, applications

Inorder Preorder Postorder BFS Morris LCA Serialise