</> MAANG.io
coding interview · 201

Intermediate

Advanced coding interview preparation covering complex algorithms, system design, and optimization techniques for senior-level positions.

0/170 solved 0% complete

LCA and Restructuring

What is this?

Two people trace their family trees back through their parents. If they walk at the same pace they will not meet, because one may be further from the common ancestor than the other. But if each, on reaching the top, switches to the other's starting point, they will have walked identical total distances — and they meet exactly at the first ancestor they share.

That is the first problem. The second is quieter and trickier: remove some nodes from a tree and hand back the pieces that survive, with no dangling references left behind.

flowchart TD A["Parent pointers available?"] -->|yes| B["Two pointers walk upward
switch to the other's start on hitting null"] B --> C["they meet at the LCA
after equal total distance"] A -->|"no — deleting nodes"| D["post-order: recurse first"] D --> E["node.left = helper(node.left)
node.right = helper(node.right)"] E --> F{"is this node deleted?"} F -->|yes| G["surviving children become roots
return null to the parent"] F -->|no| H["return the node itself"]

💡 Fun fact: Lowest Common Ancestor III — the variant where nodes carry parent pointers — is the linked-list intersection problem in disguise. Each node's chain of ancestors is a linked list ending at the root, and two such chains merge at the LCA exactly as two lists merge at their intersection. The pointer-switching trick that equalises path lengths is identical, which is why solving one teaches the other for free.

🔓 The 2 problems in this chapter are free. Sign in with Google or Microsoft to start solving.


The one-line idea: when parent links exist, an ancestor query becomes a two-list intersection. When you delete nodes, a recursive call must return what should replace the node it was given, and the parent must reassign — that reassignment is what keeps the tree consistent.


1. Meeting in the middle

def lowest_common_ancestor(p, q):
    a, b = p, q
    while a is not b:
        a = a.parent if a else q       # exhausted → restart from the other side
        b = b.parent if b else p
    return a                            # meet at the LCA (or None if unrelated)

Why it terminates: if p is d1 steps from the LCA and q is d2, then pointer a walks d1 + (depth of q) and pointer b walks d2 + (depth of p) — and both totals are equal. They arrive together. If the two nodes are in different trees entirely, both pointers reach None simultaneously and the loop exits with None, which is the correct answer.

The alternative — collect p's ancestors into a set, then walk up from q until a member is found — is O(h) space and perfectly acceptable to offer first.

2. Return the replacement

The deletion problem is where a small habit prevents a subtle bug. A helper must return the node that should occupy this position after processing, and the caller must store it:

def helper(node, to_delete, forest):
    if not node: return None
    node.left  = helper(node.left,  to_delete, forest)     # reassign, always
    node.right = helper(node.right, to_delete, forest)
    if node.val in to_delete:
        if node.left:  forest.append(node.left)            # orphans become roots
        if node.right: forest.append(node.right)
        return None                                        # detach from the parent
    return node

Two ordering details matter. Recurse before deciding, so children are already processed and any new roots among them are recorded before the current node vanishes. And the original root is a root only if it was not itself deleted — a case worth checking explicitly rather than assuming.


3. A 30-second worked example (delete and collect)

        1
       / \
      2   3          delete {2, 4}
     / \   \
    4   5   6

post-order:
  node 4 → in delete set, no children      → returns null, contributes nothing
  node 5 → survives                        → returns 5
  node 2 → children processed: left=null, right=5
           2 is in the delete set → 5 becomes a root, return null
  node 6 → survives                        → returns 6
  node 3 → survives with right=6           → returns 3
  node 1 → survives, left=null, right=3    → returns 1, and is itself a root

forest = [5, 1]

Node 5 was promoted because its parent disappeared after it had been processed — which is exactly why the recursion must be post-order.


4. Where you'll actually meet this

  • Version control. git merge-base is lowest common ancestor over the commit graph, and commits carry parent pointers, so the two-pointer variant is the natural fit.
  • Org and permission hierarchies. "Who is the lowest manager over both of these people?" and "remove this department, keep its teams" are these two problems verbatim.
  • Filesystem operations. Deleting a directory while preserving its contents as new top-level items is the forest problem.
  • Taxonomy and category trees. Finding the nearest shared category for two products powers recommendation and breadcrumb logic.
  • DOM manipulation. Removing an element while re-parenting its children requires the same "return the replacement" discipline.

5. Problems in this chapter

▶ Lowest Common Ancestor of a Binary Tree III

Find the LCA when nodes have parent pointers. Two pointers walking upward and switching starts, meeting after equal distance.
Pattern: two-list intersection. Target: O(h) time, O(1) space (versus the O(h)-space ancestor-set approach).

▶ Delete Nodes And Return Forest

Delete a set of values and return the roots of the remaining trees. Post-order, reassign children, promote orphans, and remember the original root when it survives.
Pattern: return-the-replacement recursion. Target: O(n) time, O(h) space.


6. Common pitfalls 🚫

  • Not reassigning the child. helper(node.left) without node.left = leaves the parent pointing at a deleted node — and most test cases will not catch it.
  • Deciding before recursing. If you delete a node before processing its children, their promotion to roots is lost.
  • Forgetting the original root. If it survives, it belongs in the forest; if it is deleted, it must not be.
  • Using a list for the delete set. Membership tests become O(k) each; a set makes them O(1).
  • Advancing only one pointer in the LCA walk. Both must move each round or the distances never equalise.
  • Assuming the two nodes are related. If they sit in different trees, both pointers must reach null together and the answer is None — the loop handles it, but only if the null-switch is written correctly.
  • Comparing values instead of identity when checking whether the pointers have met — duplicates in the tree would break value comparison.

7. Key takeaways

  1. Ancestor chains are linked lists. With parent pointers, LCA is list intersection and the pointer-switch equalises path lengths.
  2. Return what replaces the node, and make the caller reassign. This one habit removes an entire class of tree-mutation bug.
  3. Post-order is mandatory for deletion — children must be settled before their parent disappears.
  4. Promote orphans at the moment of deletion, when both children are still reachable.
  5. Handle the root explicitly. It is the one node with no parent to reassign it.
  6. Why interviewers like it: these look easy and fail on the details. Whether you reassign, and whether you recurse before deciding, tells them how carefully you reason about mutation.

Order: Lowest Common Ancestor of a Binary Tree III → Delete Nodes And Return Forest.

Lowest Common Ancestor of a Binary Tree III

Core idea: When every node knows its parent, you never need the root. Walk upward from p and from q. Each node sits at the bottom of a single path that climbs to the root, so the two paths are really two linked lists that share a tail — and that tail begins at the LCA. The classic two-pointer trick from Intersection of Two Linked Lists (when one pointer hits the top, restart it at the other node) makes them meet at that junction in O(1) extra space.

The Problem, Rephrased

You're given two nodes, p and q, from a binary tree. You are not given the root. What you are given is that each node carries a parent pointer (the root's parent is None). Return their lowest common ancestor — the deepest node that is an ancestor of both p and q (a node is allowed to be an ancestor of itself).

Because you can only climb via parent, the natural move is to walk up from each node toward the root. The first node both upward walks share is the answer.

A fresh scenario

Think of a family tree where you only have "who is my parent" links — no master chart, no index. You and your cousin both want to find your nearest shared ancestor. Neither of you can see the whole family; each of you can only point at your own parent, then their parent, and so on. If you both keep walking up, your two trails of names eventually merge at one person — your most recent common ancestor. That merge point is the LCA.

Input / Output

p (value) q (value) Tree shape (parent links) Output (LCA value)
5 1 3 is parent of 5 and 1 3
5 4 5 is an ancestor of 4 5
6 4 both under subtree rooted at 5 5
2 2 any tree, p == q 2

Sign in to continue reading

The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.

Sign in to read the full lesson

Sign in to MAANG.io

Use your Google or Microsoft account — no password to remember.

Continue with Google Continue with Microsoft

Please accept the terms above to continue.