Practice: Lowest Common Ancestor of a Binary Tree

Problem: https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/

Recognition reminder: “lowest common ancestor” is a named tree cue. Note this is a general binary tree, not a binary search tree, so you cannot use value comparisons to pick a direction; you have to search both subtrees. The shape is a DFS that returns “a node, or None”, and the answer reveals itself where the two child searches both come back non-empty.

Before you start (the five-beat rhythm)

  1. Your Pattern Card for this week is already written.
  2. Name the pattern aloud and write your approach as a plain-English comment before any code.
  3. Struggle floor: 25 minutes unaided. No hints, no AI, no Discuss tab.
  4. If stuck past the floor, ask the tutor for a hint. Six rungs, one per ask.
  5. Debrief in your commit message before moving to the next problem.

Your target

Fill these in yourself before you look at anyone else’s solution:

Target time complexity:  ____
Target space complexity: ____

A single DFS that returns up the tree is enough; you should not need more than one pass. Think about what each call returns and how a parent combines its two children’s returns. Name the space cost (it is the recursion stack again) and what dominates.

The shape of the input and the output

You implement a function lowest_common_ancestor(root, p, q) that returns a node. The arguments are TreeNode objects: root is the tree, and p and q are two distinct nodes that are both guaranteed to exist in the tree. You return the lowest node that has both p and q somewhere in its subtree (a node is allowed to be a descendant of itself, so if p is an ancestor of q, the answer is p).

The recursion returns “a node or None”: None if neither target is in this subtree, otherwise a target it found (or the ancestor it has already concluded). The combining rule is the lesson: if the left search and the right search both come back non-empty, the current node is the split point, so it is the answer; if only one side comes back non-empty, pass that side’s result up. Work out the base case (what you return when you reach None, and what you return when the current node is p or q) before you write code.

For your own local testing you will need to hand the function actual node objects. The provided test does this for you (see below); when you experiment by hand, remember p and q are nodes, not values.

Where your code goes

Write your solution as a function lowest_common_ancestor(root, p, q) in a file named solution.py in your own work repo (see getting-started.md), not in the course repo. Define your own TreeNode class (the same three-attribute shape the test uses). This folder ships only the problem spec and a provided-example test (tests/test_provided.py). Because the function takes node objects, the test specifies p and q by their values, builds the tree, locates those two nodes by value for you, calls your function with the node objects, and checks the returned node’s val. Read the test so you see how it finds the nodes. The judge is the oracle; the tutor will not confirm your answer by reading it.

Debrief (paste into your commit message)

1. What pattern did this turn out to be?
2. What was the trigger phrase or input shape that should have made me reach for it?
3. What was the time and space complexity, and what would dominate at scale?
4. What edge case would have broken my first attempt?
5. What would I do differently in three days when I see this cold?