questions S-W

This commit is contained in:
2025-05-30 19:18:33 +01:00
parent ddceeec07e
commit 041a877295
46 changed files with 9696 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
title: Subtree of Another Tree
slug: subtree-of-another-tree
difficulty: easy
leetcode_id: 572
leetcode_url: https://leetcode.com/problems/subtree-of-another-tree/
categories:
- trees
- recursion
patterns:
- dfs
- tree-traversal
description: |
Given the roots of two binary trees `root` and `subRoot`, return `true` if there is a subtree of `root` with the same structure and node values of `subRoot` and `false` otherwise.
A subtree of a binary tree `tree` is a tree that consists of a node in `tree` and all of this node's descendants. The tree `tree` could also be considered as a subtree of itself.
constraints: |
- The number of nodes in the `root` tree is in the range `[1, 2000]`
- The number of nodes in the `subRoot` tree is in the range `[1, 1000]`
- `-10^4 <= root.val <= 10^4`
- `-10^4 <= subRoot.val <= 10^4`
examples:
- input: "root = [3,4,5,1,2], subRoot = [4,1,2]"
output: "true"
explanation: "The subtree rooted at node 4 in root has the same structure and values as subRoot."
- input: "root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]"
output: "false"
explanation: "The subtree rooted at node 4 in root has an additional child node 0 that is not present in subRoot."
explanation:
intuition: |
Think of this problem as searching for a "matching snapshot" within a larger tree.
Imagine you have a big family tree hanging on your wall, and a smaller family tree on a card. You want to check if there's any branch in the big tree that looks *exactly* like the card — same people, same relationships, same structure.
The key insight is that this problem breaks down into two separate questions:
1. **Where to look?** We need to visit every node in the main tree as a potential "starting point" for a match
2. **How to compare?** At each node, we need to check if the tree rooted there is *identical* to `subRoot`
This naturally leads to a recursive solution: traverse the main tree (DFS), and at each node, run a separate tree comparison. If any comparison succeeds, we've found our subtree.
approach: |
We solve this using **recursive DFS with a helper function**:
**Step 1: Define a helper function to check tree equality**
- `is_same_tree(p, q)`: Returns `true` if trees rooted at `p` and `q` are identical
- Two trees are identical if:
- Both are `null` (empty trees match)
- Both have the same root value AND their left subtrees match AND their right subtrees match
- If one is `null` and the other isn't, they don't match
&nbsp;
**Step 2: Define the main subtree check**
- `is_subtree(root, subRoot)`: Returns `true` if `subRoot` is a subtree of `root`
- Base case: If `root` is `null`, return `false` (can't find a subtree in an empty tree)
- At each node, check three possibilities:
- The tree rooted at `root` is identical to `subRoot` (use `is_same_tree`)
- `subRoot` is a subtree of `root.left`
- `subRoot` is a subtree of `root.right`
- Return `true` if any of these conditions holds
&nbsp;
**Step 3: Return the result**
- The recursion naturally explores all nodes in the main tree
- As soon as we find a match, the `true` propagates up
common_pitfalls:
- title: Confusing Subtree with Same Tree
description: |
A subtree must include *all* descendants of a node, not just some of them.
For example, in `root = [3,4,5,1,2,null,null,null,null,0]` with `subRoot = [4,1,2]`, the node 4 in `root` has children 1 and 2, but node 2 also has a child 0. This extra node means the subtree rooted at 4 is `[4,1,2,null,null,0]`, which is NOT the same as `[4,1,2]`.
Your `is_same_tree` function must recursively check the entire structure, not just the first few levels.
wrong_approach: "Only comparing root values and immediate children"
correct_approach: "Recursively compare entire tree structures including all descendants"
- title: Forgetting to Check All Nodes
description: |
The subtree could be rooted at any node in the main tree, not just nodes with the same value as `subRoot.val`.
Make sure your traversal visits every node as a potential starting point. A common mistake is to only recurse when values match, but you need to recurse regardless — the match might be deeper in the tree.
wrong_approach: "Only checking nodes where root.val == subRoot.val"
correct_approach: "Check is_same_tree at every node, regardless of value"
- title: Incorrect Base Cases
description: |
Handle null cases carefully:
- `is_same_tree(null, null)` → `true` (two empty trees are identical)
- `is_same_tree(null, node)` or `is_same_tree(node, null)` → `false`
- `is_subtree(null, subRoot)` → `false` (can't find anything in an empty tree)
Getting these wrong leads to null pointer errors or incorrect results.
key_takeaways:
- "**Decomposition pattern**: Breaking a complex tree problem into two simpler recursive functions (traverse + compare) is a powerful technique"
- "**Subtree vs same tree**: Understanding the difference is crucial — a subtree must include all descendants, making it a stricter condition than partial matching"
- "**DFS for tree search**: When you need to find something anywhere in a tree, DFS traversal that checks each node is the standard approach"
- "**Foundation for advanced problems**: This pattern extends to problems like finding duplicate subtrees, tree serialisation, and tree hashing"
time_complexity: "O(m × n). For each of the `m` nodes in `root`, we potentially compare against all `n` nodes in `subRoot`."
space_complexity: "O(m + n). The recursion stack can go as deep as the height of each tree. In the worst case (skewed trees), this is O(m) for the outer traversal plus O(n) for the comparison."
solutions:
- approach_name: Recursive DFS
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 is_subtree(root: TreeNode | None, sub_root: TreeNode | None) -> bool:
# If main tree is empty, can't find any subtree
if root is None:
return False
# Check if tree rooted at current node matches subRoot
if is_same_tree(root, sub_root):
return True
# Otherwise, search in left and right subtrees
return is_subtree(root.left, sub_root) or is_subtree(root.right, sub_root)
def is_same_tree(p: TreeNode | None, q: TreeNode | None) -> bool:
# Both empty? They're the same
if p is None and q is None:
return True
# One empty, one not? Not the same
if p is None or q is None:
return False
# Values must match, and both subtrees must be identical
return (
p.val == q.val
and is_same_tree(p.left, q.left)
and is_same_tree(p.right, q.right)
)
explanation: |
**Time Complexity:** O(m × n) — For each node in root (m nodes), we may call is_same_tree which visits up to n nodes.
**Space Complexity:** O(m + n) — Recursion stack depth for nested calls.
We traverse every node in the main tree, and at each node, we check if the subtree rooted there is identical to subRoot. The is_same_tree helper does a parallel traversal of both trees, returning false as soon as a mismatch is found.
- approach_name: Tree Serialisation
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 is_subtree(root: TreeNode | None, sub_root: TreeNode | None) -> bool:
# Serialise both trees to strings
def serialise(node: TreeNode | None) -> str:
if node is None:
return "#"
# Use delimiters to avoid false matches like "12" containing "2"
return f"^{node.val},{serialise(node.left)},{serialise(node.right)}"
root_str = serialise(root)
sub_str = serialise(sub_root)
# Check if subRoot's serialisation appears in root's serialisation
return sub_str in root_str
explanation: |
**Time Complexity:** O(m + n) — Serialisation is O(m) and O(n), string search is O(m + n) with KMP/built-in.
**Space Complexity:** O(m + n) — Storing the serialised strings.
This approach converts both trees to unique string representations, then checks if the smaller string is a substring of the larger. The `^` prefix prevents false positives like value "2" matching inside "12". While asymptotically faster, the constant factors and space usage often make the recursive approach preferable for typical inputs.