questions B (backspace - burst-balloons)

This commit is contained in:
2025-05-24 22:06:49 +01:00
parent f757e28b24
commit 2123791ec3
67 changed files with 13945 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
title: Binary Tree Paths
slug: binary-tree-paths
difficulty: easy
leetcode_id: 257
leetcode_url: https://leetcode.com/problems/binary-tree-paths/
categories:
- trees
- strings
- recursion
patterns:
- dfs
- backtracking
- tree-traversal
description: |
Given the `root` of a binary tree, return *all root-to-leaf paths in **any order***.
A **leaf** is a node with no children.
constraints: |
- The number of nodes in the tree is in the range `[1, 100]`
- `-100 <= Node.val <= 100`
examples:
- input: "root = [1,2,3,null,5]"
output: '["1->2->5","1->3"]'
explanation: "The tree has two leaf nodes: 5 and 3. The path from root to 5 is 1->2->5, and the path from root to 3 is 1->3."
- input: "root = [1]"
output: '["1"]'
explanation: "The tree has only one node which is both root and leaf, so there is only one path."
explanation:
intuition: |
Imagine standing at the root of a tree and needing to explore every possible route down to each leaf node, recording your path as you go.
This is a classic **tree traversal problem** where we need to visit every node while keeping track of the path we've taken. The key insight is that **depth-first search (DFS)** naturally models this exploration: we go as deep as possible down one branch, record the path when we hit a leaf, then backtrack and explore other branches.
Think of it like navigating a maze where you need to find all exits (leaves). You walk down one corridor, marking your route. When you hit a dead end (leaf), you record the complete path, then retrace your steps to try other corridors.
The core insight is recognizing when we've reached a leaf node: a node where **both** the left and right children are `None`. At that point, we've completed a valid root-to-leaf path.
approach: |
We solve this using **Depth-First Search with Path Tracking**:
**Step 1: Handle the base case**
- If the root is `None`, return an empty list (no paths exist)
&nbsp;
**Step 2: Initialise the DFS traversal**
- Create a result list to store all complete paths
- Start DFS from the root with the initial path containing just the root's value
&nbsp;
**Step 3: Define the recursive DFS function**
- For each node, check if it's a leaf (no left or right children)
- If it's a leaf, the current path is complete - add it to results
- If not a leaf, recursively explore left and right children
- When exploring children, extend the current path with `"->"` and the child's value
&nbsp;
**Step 4: Return all collected paths**
- After DFS completes, return the result list containing all root-to-leaf paths
&nbsp;
This approach naturally handles all tree shapes because DFS explores every branch, and the path string is built incrementally as we descend.
common_pitfalls:
- title: Forgetting the Leaf Check
description: |
A common mistake is adding paths at every node rather than only at leaves. This would result in incomplete paths being added to your result.
The correct leaf check requires **both** children to be `None`:
```python
if not node.left and not node.right: # This is a leaf
```
Not just checking one side:
```python
if not node.left: # Wrong - node could still have a right child
```
wrong_approach: "Adding paths at every node"
correct_approach: "Only add path when both children are None (leaf node)"
- title: Incorrect Path String Building
description: |
Building the path string incorrectly can lead to malformed output. A common error is adding the arrow `"->"` before or after incorrectly.
The pattern should be: start with the root value, then prepend `"->"` before each subsequent node value:
- Path starts as `"1"`
- When visiting child with value 2: `"1->2"`
- When visiting grandchild with value 5: `"1->2->5"`
Don't add a trailing arrow or forget to convert node values to strings.
wrong_approach: "Adding arrow before first node or after last node"
correct_approach: "Build path as 'current_path + \"->\" + str(node.val)'"
- title: Mutating Shared Path State
description: |
When using a list to build the path (instead of a string), forgetting to properly backtrack can corrupt your paths.
If you use a mutable list like `path = [1, 2]` and pass it to recursive calls, you need to either:
- Remove the element after returning (explicit backtracking)
- Pass a copy of the list to each recursive call
Using strings avoids this issue since strings are immutable in Python.
wrong_approach: "Mutating a shared list without backtracking"
correct_approach: "Use immutable strings or explicitly backtrack"
key_takeaways:
- "**DFS for path enumeration**: When you need all paths in a tree, DFS naturally explores each branch completely before backtracking"
- "**Leaf detection pattern**: A leaf node has no children - check `not node.left and not node.right`"
- "**Immutable path building**: Using strings for path construction avoids backtracking bugs since strings are immutable"
- "**Foundation for path problems**: This pattern extends to problems like path sum, longest path, and path with maximum value"
time_complexity: "O(n). We visit each node exactly once during the DFS traversal, where `n` is the number of nodes in the tree."
space_complexity: "O(n). In the worst case (a skewed tree), the recursion stack can be `n` levels deep. Additionally, we store all paths which in total can have O(n) nodes across all paths."
solutions:
- approach_name: DFS with String Path
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 binary_tree_paths(root: TreeNode | None) -> list[str]:
# Handle empty tree
if not root:
return []
result = []
def dfs(node: TreeNode, path: str) -> None:
# Check if we've reached a leaf node
if not node.left and not node.right:
# Leaf reached - add complete path to results
result.append(path)
return
# Explore left subtree if it exists
if node.left:
dfs(node.left, path + "->" + str(node.left.val))
# Explore right subtree if it exists
if node.right:
dfs(node.right, path + "->" + str(node.right.val))
# Start DFS from root with initial path
dfs(root, str(root.val))
return result
explanation: |
**Time Complexity:** O(n) — We visit each node once.
**Space Complexity:** O(n) — Recursion stack depth plus storage for paths.
This solution uses DFS to explore all root-to-leaf paths. The path is built as a string, which is immutable, so we don't need explicit backtracking. When we reach a leaf (no children), we add the complete path to our result list.
- approach_name: Iterative DFS with Stack
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 binary_tree_paths(root: TreeNode | None) -> list[str]:
if not root:
return []
result = []
# Stack stores tuples of (node, path_so_far)
stack = [(root, str(root.val))]
while stack:
node, path = stack.pop()
# Check if leaf node
if not node.left and not node.right:
result.append(path)
continue
# Add children to stack with extended paths
if node.right:
stack.append((node.right, path + "->" + str(node.right.val)))
if node.left:
stack.append((node.left, path + "->" + str(node.left.val)))
return result
explanation: |
**Time Complexity:** O(n) — We process each node once.
**Space Complexity:** O(n) — Stack can hold up to n nodes in worst case.
This iterative approach uses an explicit stack instead of recursion. Each stack entry contains both the node and the path built so far. This avoids potential stack overflow for very deep trees and makes the traversal order explicit. We add right child first so left is processed first (LIFO order).