questions F-L

This commit is contained in:
2025-05-25 11:47:04 +01:00
parent 360b5fa255
commit ad320dc703
54 changed files with 11235 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
title: Lowest Common Ancestor of a Binary Search Tree
slug: lowest-common-ancestor-of-a-binary-search-tree
difficulty: medium
leetcode_id: 235
leetcode_url: https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/
categories:
- trees
patterns:
- tree-traversal
- binary-search
description: |
Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST.
According to the definition of LCA on Wikipedia: "The lowest common ancestor is defined between two nodes `p` and `q` as the lowest node in `T` that has both `p` and `q` as descendants (where we allow **a node to be a descendant of itself**)."
constraints: |
- The number of nodes in the tree is in the range `[2, 10^5]`
- `-10^9 <= Node.val <= 10^9`
- All `Node.val` are **unique**
- `p != q`
- `p` and `q` will exist in the BST
examples:
- input: "root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8"
output: "6"
explanation: "The LCA of nodes 2 and 8 is 6."
- input: "root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4"
output: "2"
explanation: "The LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition."
- input: "root = [2,1], p = 2, q = 1"
output: "2"
explanation: "The LCA of nodes 2 and 1 is 2."
explanation:
intuition: |
The key insight is to **leverage the BST property**: for any node, all values in its left subtree are smaller, and all values in its right subtree are larger.
Think of it like searching for a meeting point. Imagine you're standing at the root, and two people are trying to find each other — one at node `p` and one at node `q`. As you traverse down the tree, at some point you'll reach a node where the two people would need to go in **different directions** to reach their respective nodes. That splitting point is the LCA.
More concretely:
- If both `p` and `q` are **smaller** than the current node, the LCA must be in the left subtree
- If both `p` and `q` are **larger** than the current node, the LCA must be in the right subtree
- If `p` and `q` are on **opposite sides** (or one equals the current node), then the current node is the LCA
This is fundamentally different from finding the LCA in a general binary tree, where you'd need to search both subtrees. The BST ordering gives us a guaranteed direction at each step.
approach: |
We solve this using the **BST Property** to guide our traversal:
**Step 1: Start at the root**
- Begin traversal at the root node
- We'll move down the tree based on how `p` and `q` compare to the current node
&nbsp;
**Step 2: Compare values and decide direction**
- If both `p.val` and `q.val` are **less than** `current.val`, move to the left child
- If both `p.val` and `q.val` are **greater than** `current.val`, move to the right child
- Otherwise, we've found the split point — return the current node
&nbsp;
**Step 3: The split point is the LCA**
- When `p` and `q` lie on different sides of the current node (or one of them equals the current node), the current node is the lowest common ancestor
- Return this node as the answer
&nbsp;
This works because the BST property guarantees that once `p` and `q` "split" to different subtrees, they can never reunite at a lower node.
common_pitfalls:
- title: Ignoring the BST Property
description: |
A common mistake is treating this like a general binary tree LCA problem and recursing into both subtrees to find `p` and `q`.
In a general binary tree, you'd need to search both children and check which subtree contains which node. But in a BST, you can determine which direction to go with a simple value comparison — O(1) per node instead of potentially visiting both subtrees.
This makes the BST solution O(h) instead of O(n).
wrong_approach: "Search both subtrees like in a general binary tree"
correct_approach: "Use value comparisons to choose one direction at each step"
- title: Forgetting a Node Can Be Its Own Ancestor
description: |
The problem states that a node can be a descendant of itself. If `p = 2` and `q = 4`, and node 2 is an ancestor of node 4 in the BST, then the LCA is 2, not some parent of 2.
When checking the split condition, remember to handle the case where the current node equals `p` or `q`. In this case, the current node is the LCA because one node is an ancestor of the other.
wrong_approach: "Only return when p and q are on opposite sides"
correct_approach: "Return when p and q split OR when current equals p or q"
- title: Incorrect Comparison Logic
description: |
Be careful with the comparison operators. The condition for moving left is when **both** values are less than current. Similarly for moving right.
A common bug is using OR instead of AND:
- Wrong: `if p.val < current.val or q.val < current.val` (might miss the split)
- Correct: `if p.val < current.val and q.val < current.val`
wrong_approach: "Using OR logic for direction decisions"
correct_approach: "Using AND logic — both must be less/greater to continue"
key_takeaways:
- "**Exploit BST ordering**: The BST property lets you make O(1) direction decisions, avoiding the need to search both subtrees"
- "**Split point = LCA**: The moment two values would need to go different directions, you've found their common ancestor"
- "**Iterative vs recursive**: Both approaches work, but iterative uses O(1) space vs O(h) for the recursive call stack"
- "**Foundation for harder problems**: This pattern extends to problems like finding paths between nodes or validating BST structure"
time_complexity: "O(h) where h is the height of the tree. In a balanced BST, h = log(n). In the worst case (skewed tree), h = n."
space_complexity: "O(1) for the iterative solution. We only use a single pointer to traverse the tree, regardless of input size."
solutions:
- approach_name: Iterative Traversal
is_optimal: true
code: |
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def lowest_common_ancestor(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
# Start at the root and traverse down
current = root
while current:
# Both nodes are in the left subtree
if p.val < current.val and q.val < current.val:
current = current.left
# Both nodes are in the right subtree
elif p.val > current.val and q.val > current.val:
current = current.right
# Split point found — p and q are on different sides
# (or one of them equals current)
else:
return current
return None # Should never reach here if p and q exist in tree
explanation: |
**Time Complexity:** O(h) — We traverse at most the height of the tree, making one comparison per level.
**Space Complexity:** O(1) — Only a single pointer variable is used; no recursion stack.
We exploit the BST property to navigate directly to the LCA. At each node, we compare both `p` and `q` values to decide whether to go left, right, or stop. The moment they would diverge (or one matches the current node), we've found the LCA.
- approach_name: Recursive Traversal
is_optimal: false
code: |
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def lowest_common_ancestor(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
# Both nodes are in the left subtree
if p.val < root.val and q.val < root.val:
return lowest_common_ancestor(root.left, p, q)
# Both nodes are in the right subtree
if p.val > root.val and q.val > root.val:
return lowest_common_ancestor(root.right, p, q)
# Split point — this is the LCA
return root
explanation: |
**Time Complexity:** O(h) — Same traversal pattern as iterative, visiting at most h nodes.
**Space Complexity:** O(h) — Recursive call stack can grow up to the height of the tree.
This recursive version follows the same logic but uses the call stack instead of a loop. While elegant, it uses more space than the iterative approach. The recursive calls naturally unwind once we find the split point.