Data Structures & Algorithms Cheatsheet

Trees and Traversals

Use this Data Structures & Algorithms reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Complexity

For a tree of n nodes and height h. A balanced binary tree has h = O(log n), a degenerate one has h = n.

OperationTimeSpace
any full traversalO(n)O(h) recursive, O(n) with a queue
height or depthO(n)O(h)
lowest common ancestor (binary tree)O(n)O(h)
level order (BFS)O(n)O(w), w = max width
Morris traversalO(n)O(1)
serialize or deserializeO(n)O(n)
path sum enumerationO(n) per root, O(n²) all pairsO(h)

Recursive depth is the hidden space cost. Python's default recursion limit is around 1,000, so a 10,000-node skewed tree overflows unless you raise the limit or go iterative.

Node Definitions

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode(int val) { this.val = val; }
}

Vocabulary that shows up in problem statements:

TermMeaning
depth of a nodeedges from the root to it, root depth 0
height of a nodeedges to its deepest descendant, leaf height 0
full binary treeevery node has 0 or 2 children
complete binary treeall levels full except possibly the last, filled left to right
perfect binary treeall levels completely full, exactly 2^h+1 − 1 nodes
balancedleft and right heights differ by at most 1 at every node
leafno children

The Four Traversal Orders

The prefix names the position of the root visit relative to the recursive calls.

def preorder(root, out):     # root, left, right
    if not root:
        return
    out.append(root.val)
    preorder(root.left, out)
    preorder(root.right, out)

def inorder(root, out):      # left, root, right -> sorted for a BST
    if not root:
        return
    inorder(root.left, out)
    out.append(root.val)
    inorder(root.right, out)

def postorder(root, out):    # left, right, root -> children before parent
    if not root:
        return
    postorder(root.left, out)
    postorder(root.right, out)
    out.append(root.val)
OrderUse it for
preorderserialization, copying a tree, prefix expressions, path-from-root problems
inordersorted output of a BST, validating a BST, kth smallest
postorderdeleting a tree, computing sizes and heights, any answer built from children
level ordershortest path in an unweighted tree, per-level aggregates, right-side view

Choosing the order is the whole design decision. If the answer at a node needs its children's answers, it is postorder. If it needs information from above, it is preorder with a parameter passed down.

Iterative Traversals

def preorder_iter(root):
    if not root:
        return []
    out, st = [], [root]
    while st:
        node = st.pop()
        out.append(node.val)
        if node.right:
            st.append(node.right)     # right first, so left pops first
        if node.left:
            st.append(node.left)
    return out

def inorder_iter(root):
    out, st, cur = [], [], root
    while cur or st:
        while cur:
            st.append(cur)
            cur = cur.left            # dive left as far as possible
        cur = st.pop()
        out.append(cur.val)
        cur = cur.right
    return out

def postorder_iter(root):
    """Reverse of a modified preorder: root, right, left, then reverse."""
    if not root:
        return []
    out, st = [], [root]
    while st:
        node = st.pop()
        out.append(node.val)
        if node.left:
            st.append(node.left)
        if node.right:
            st.append(node.right)
    return out[::-1]

def morris_inorder(root):
    """O(1) space by threading temporary links back to the successor."""
    out, cur = [], root
    while cur:
        if not cur.left:
            out.append(cur.val)
            cur = cur.right
        else:
            pre = cur.left
            while pre.right and pre.right is not cur:
                pre = pre.right
            if not pre.right:
                pre.right = cur       # thread
                cur = cur.left
            else:
                pre.right = None      # unthread, restore the tree
                out.append(cur.val)
                cur = cur.right
    return out

Level Order And Its Variants

from collections import deque

def level_order(root):
    if not root:
        return []
    out, q = [], deque([root])
    while q:
        level = []
        for _ in range(len(q)):       # freeze the size, one level per round
            n = q.popleft()
            level.append(n.val)
            if n.left:
                q.append(n.left)
            if n.right:
                q.append(n.right)
        out.append(level)
    return out

def zigzag(root):
    levels = level_order(root)
    return [lv if i % 2 == 0 else lv[::-1] for i, lv in enumerate(levels)]

def right_side_view(root):
    return [lv[-1] for lv in level_order(root)]

def min_depth(root):
    """BFS returns as soon as the first leaf is found."""
    if not root:
        return 0
    q, depth = deque([root]), 1
    while q:
        for _ in range(len(q)):
            n = q.popleft()
            if not n.left and not n.right:
                return depth
            if n.left:
                q.append(n.left)
            if n.right:
                q.append(n.right)
        depth += 1

Postorder Aggregation Problems

These all share one shape: recurse, combine the two children, update a global, return the value the parent needs. The trick is that the value returned to the parent is often not the answer.

def max_depth(root):
    if not root:
        return 0
    return 1 + max(max_depth(root.left), max_depth(root.right))

def is_balanced(root):
    """Return height, or -1 to signal unbalanced. O(n) single pass."""
    def h(node):
        if not node:
            return 0
        lh = h(node.left)
        if lh == -1:
            return -1
        rh = h(node.right)
        if rh == -1 or abs(lh - rh) > 1:
            return -1
        return 1 + max(lh, rh)
    return h(root) != -1

def diameter(root):
    """Longest path in edges between any two nodes."""
    best = 0
    def depth(node):
        nonlocal best
        if not node:
            return 0
        l, r = depth(node.left), depth(node.right)
        best = max(best, l + r)       # path THROUGH this node
        return 1 + max(l, r)          # but the parent only gets one side
    depth(root)
    return best

def max_path_sum(root):
    """Values may be negative, so clamp a negative branch to 0."""
    best = float("-inf")
    def gain(node):
        nonlocal best
        if not node:
            return 0
        l = max(gain(node.left), 0)
        r = max(gain(node.right), 0)
        best = max(best, node.val + l + r)
        return node.val + max(l, r)
    gain(root)
    return best

def count_univalue_subtrees(root):
    count = 0
    def walk(node):
        nonlocal count
        if not node:
            return True, None
        lu, lv = walk(node.left)
        ru, rv = walk(node.right)
        ok = (lu and ru
              and (lv is None or lv == node.val)
              and (rv is None or rv == node.val))
        if ok:
            count += 1
        return ok, node.val
    walk(root)
    return count

LCA, Paths, And Serialization

def lca(root, p, q):
    """Lowest common ancestor in a plain binary tree. O(n)."""
    if not root or root is p or root is q:
        return root
    left = lca(root.left, p, q)
    right = lca(root.right, p, q)
    if left and right:
        return root            # p and q split here
    return left or right

def path_sum_all(root, target):
    """All root-to-leaf paths summing to target. Backtracking."""
    out, path = [], []
    def dfs(node, remaining):
        if not node:
            return
        path.append(node.val)
        if not node.left and not node.right and remaining == node.val:
            out.append(path[:])            # copy, path is mutated after
        else:
            dfs(node.left, remaining - node.val)
            dfs(node.right, remaining - node.val)
        path.pop()                          # undo before returning
    dfs(root, target)
    return out

def serialize(root):
    """Preorder with explicit null markers."""
    out = []
    def go(n):
        if not n:
            out.append("#")
            return
        out.append(str(n.val))
        go(n.left)
        go(n.right)
    go(root)
    return ",".join(out)

def deserialize(data):
    vals = iter(data.split(","))
    def go():
        v = next(vals)
        if v == "#":
            return None
        node = TreeNode(int(v))
        node.left = go()
        node.right = go()
        return node
    return go()

def invert(root):
    if not root:
        return None
    root.left, root.right = invert(root.right), invert(root.left)
    return root
// Java: the same postorder aggregation shape, using a field for the global
private int best = 0;
public int diameter(TreeNode root) { depth(root); return best; }
private int depth(TreeNode n) {
    if (n == null) return 0;
    int l = depth(n.left), r = depth(n.right);
    best = Math.max(best, l + r);
    return 1 + Math.max(l, r);
}

Gotchas

  • Always handle None first. The base case is the first line of every tree function.
  • Copy the path before recording it. out.append(path) stores a live reference that later mutations wreck. Use path[:] or list(path).
  • Pop after recursing, unconditionally, including on the leaf branch. A missed pop() corrupts every later path.
  • nonlocal in Python, a field in Java. Reassigning a plain outer variable inside a nested function creates a new local instead.
  • Diameter returns and records different things. The global tracks l + r, the return value gives the parent 1 + max(l, r). Conflating them is the standard bug.
  • Freeze len(q) at the top of each BFS round or levels blend together.
  • Preorder alone does not determine a tree. Reconstruction needs preorder plus inorder, or preorder with null markers.
  • Recursion limit. sys.setrecursionlimit(10 ** 6) in Python for deep trees, or convert to an explicit stack.
  • Height versus depth off-by-one. Some problems count nodes, others count edges. Read the examples before committing.