How does finding a lowest common ancestor differ when nodes carry parent pointers versus when they do not?
answer
- can you move upward at all?
- two upward chains must eventually merge
- unequal depths make cursors miss each other
- downward: the node where both sides report a hit
- the call stack counts as space
basics
~20 sWith parent pointers, equalize the two nodes' depths, then step both upward in lockstep until they meet: O(h) time, O(1) extra space. Without them, recurse downward post-order and take the node where the two targets surface from different subtrees: O(n).
solid answer
~50 sParent pointers turn the problem into a merge of two upward chains. Compute both depths, lift the deeper cursor until the depths match, then advance both one step at a time; the first node they share is the answer. That is `O(h)` time and `O(1)` extra space, or `O(h)` space if you instead put one node's ancestors in a set and walk the other up until you hit a member. Without parent pointers you can only go down, so you recurse post-order: a call returns non-null if its subtree contains a target, and the node whose two child calls both return non-null is the lowest common ancestor. That costs `O(n)` time — you cannot rule out a subtree without entering it — and `O(h)` stack space. The downward version quietly assumes both targets exist; if only one does, it returns that one, which callers mistake for a real ancestor.
go deeper
Know both shapes exist and what decides between them: whether a node can reach its parent. Be able to state the meeting-point idea for the upward walk without writing the code.
Explain why depth equalization is required, and describe what the downward recursion returns at each of its three cases. State time and space for both, stack included.
Bring up the absent-target failure and how you would make the result trustworthy. Be ready to argue whether adding a parent field is worth the maintenance burden it puts on every write path.
Own the interface decision: an ancestor query's cost is set by a data-model choice made long before the query exists. Weigh the per-node memory and the new invariant against measured query volume rather than elegance.
## Two different worlds The lowest common ancestor of two nodes is the deepest node whose subtree contains both, with a node counted as its own ancestor. Which algorithm you reach for is decided by one structural fact: **can you move upward?** That single bit changes the complexity, the space profile and the failure modes. A useful setting: a revision store keeps a history where every revision records the single revision it was derived from, and no revision has two parents. That history is a tree. Given two revisions, you want their nearest common ancestor — the last shared state, the base you would diff both sides against. ## When nodes carry parent pointers Each node knows its parent, so each node determines a unique chain up to the root. Two chains starting from different nodes must merge somewhere (at worst at the root) and, once merged, never separate again. The answer is the merge point. The naive attempt — advance both cursors upward one step at a time and wait for them to coincide — is wrong when the depths differ: the deeper cursor is permanently behind, and the two can sail past each other without ever being equal at the same instant. The fix is to align first: 1. Compute `depth(a)` and `depth(b)` by walking each to the root: `O(h)`. 2. Lift the deeper cursor by `|depth(a) - depth(b)|` steps. 3. Advance both one step at a time; the first time they are the same node, stop. Total time `O(h)`, extra space `O(1)`. If depth is already stored on each node — natural in an append-only history, where a new revision's depth is its parent's plus one — step 1 vanishes and the setup is `O(1)`. The alternative shape trades space for simplicity: insert every ancestor of `a` into a set, then walk `b` upward and return the first node already in the set. Also `O(h)` time, but `O(h)` memory. It reads more clearly and is the version most people write under time pressure; the two-cursor walk is the one to reach for when the chain is long or memory is accounted for. ## When you can only go down With only child links from a root, every algorithm must start at the top. The standard recursion is post-order and returns a node rather than a boolean: - An empty subtree returns nothing. - A node that *is* one of the targets returns itself. - Otherwise, recurse into both children. If **both** return non-null, the two targets were found on opposite sides, so the current node is the lowest common ancestor — return it. If exactly one returns non-null, pass that result upward unchanged. If neither does, return nothing. The upward-propagated value is doing double duty: below the answer it means "a target lives here"; at and above the answer it means "the answer is here". That overloading is what makes the code short and what makes it subtle to explain. Cost: `O(n)` time — you cannot prove a subtree is target-free without visiting it, so worst case is every node, even when both targets sit near the root. Space is `O(h)` for the call stack, which is `O(log n)` on a balanced tree and `O(n)` on a degenerate chain. **Recursion depth is space**, and a candidate who calls this algorithm `O(1)` space has skipped the stack. ## The failure mode worth naming The downward recursion assumes both targets are in the tree. Give it a target that is absent and it returns the other one — a perfectly plausible-looking node that is not an ancestor of anything you asked about. Nothing in the algorithm distinguishes "found the meeting point" from "found the only target there was". If the inputs are not trusted, either verify presence with a separate traversal, or thread a found-count through the recursion and report the pair only when the count is two. The upward walk has the same class of problem in a different dress: a node from a different tree walks up to *its* root and simply never meets the other chain, so a null result must be handled rather than assumed impossible. ## Choosing If parent pointers exist, use them: `O(h)` beats `O(n)` and the constant factors are pointer hops. If they do not exist, adding a parent field is a real design change — it costs memory per node and it must be maintained on every structural mutation, which is a new invariant for every write path to honour. Judge it against how often ancestor queries actually run. Also compare against the ordered shortcut: on a search tree, key comparisons already give you an `O(h)` downward walk with no parent pointers at all, because the ordering supplies the routing information that a general binary tree lacks.
- What does the downward recursion return if one of the two targets is absent from the tree?It returns the target that is present, and nothing marks that result as suspect. The algorithm cannot tell "the two branches met here" from "only one target ever existed". If inputs are untrusted, thread a found-count through the recursion and accept the answer only when both targets were seen, or verify presence in a separate pass first.
- Why must you equalize depths before stepping two upward cursors in lockstep?Because the answer is the same node reached at the same moment. If one cursor starts three levels deeper, it is permanently three steps behind its partner, so the two occupy the shared chain at different times and never coincide until both reach the root — if they coincide at all. Aligning depths puts them in step so their first collision is the merge point.
- How would you speed up repeated upward queries on a tree that only ever gains leaves?Store each node's depth at insertion time — a new leaf's depth is its parent's plus one — which removes the two root-walks used to measure depth and leaves only the lockstep climb. That is a cheap, local invariant to maintain precisely because appends never change any existing node's depth.
saying these in an interview costs you the question
- Steps both cursors upward without equalizing depths first
- Calls the downward recursion O(log n) on an arbitrary binary tree
- Reports the downward recursion as O(1) space, ignoring the stack
- Assumes the recursion validates that both targets exist
- Adds parent pointers without costing their maintenance on writes