questions D-E

This commit is contained in:
2025-05-25 11:08:40 +01:00
parent e028167a47
commit 798e0ba1df
18 changed files with 4022 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
title: Diameter of Binary Tree
slug: diameter-of-binary-tree
difficulty: easy
leetcode_id: 543
leetcode_url: https://leetcode.com/problems/diameter-of-binary-tree/
categories:
- trees
- recursion
patterns:
- dfs
- tree-traversal
description: |
Given the `root` of a binary tree, return *the length of the **diameter** of the tree*.
The **diameter** of a binary tree is the **length** of the longest path between any two nodes in a tree. This path may or may not pass through the `root`.
The **length** of a path between two nodes is represented by the number of edges between them.
constraints: |
- The number of nodes in the tree is in the range `[1, 10^4]`
- `-100 <= Node.val <= 100`
examples:
- input: "root = [1,2,3,4,5]"
output: "3"
explanation: "3 is the length of the path [4,2,1,3] or [5,2,1,3]."
- input: "root = [1,2]"
output: "1"
explanation: "The diameter is the single edge connecting node 1 to node 2."
explanation:
intuition: |
Picture a binary tree as a network of roads connecting cities. The "diameter" is the longest road trip you can take between any two cities without backtracking.
Here's the key insight: **every path in a tree passes through exactly one "highest" node** — the node where the path changes direction from going "up" to going "down". At this pivot node, the path consists of going down into the left subtree and going down into the right subtree.
Think of it like this: if you're standing at any node and want to find the longest path that passes through you, you'd reach as deep as possible into your left subtree, then come back up through yourself, then reach as deep as possible into your right subtree. The total path length is `left_depth + right_depth`.
The diameter of the entire tree is the maximum such path across all possible pivot nodes. This means we need to visit every node and calculate the best path that passes through it, keeping track of the global maximum.
approach: |
We solve this using **DFS (Depth-First Search)** with a clever twist: while computing depths, we simultaneously track the diameter.
**Step 1: Define the recursive helper function**
- Create a helper function `depth(node)` that returns the maximum depth of the subtree rooted at `node`
- Depth is defined as the number of edges from `node` to its deepest descendant
- A `None` node has depth `-1` (so a leaf has depth `0`)
&nbsp;
**Step 2: Calculate diameter at each node**
- At each node, the longest path passing through it equals `left_depth + right_depth + 2`
- The `+2` accounts for the edges connecting the node to its left and right children
- Update the global `diameter` variable if this path is longer than any seen before
&nbsp;
**Step 3: Return the depth for parent calculations**
- Return `max(left_depth, right_depth) + 1` — this gives the depth of the current subtree
- The parent node needs this value to compute its own potential diameter
&nbsp;
**Step 4: Initiate DFS from root**
- Call `depth(root)` to traverse the entire tree
- Return the `diameter` variable which holds the maximum path length found
&nbsp;
This approach is elegant because we compute two things in one traversal: the depth (needed for parent calculations) and the diameter (our answer). Each node is visited exactly once.
common_pitfalls:
- title: Confusing Depth with Diameter
description: |
The depth of a node is the distance from that node to its deepest descendant. The diameter is the longest path between any two nodes.
A common mistake is returning depth instead of diameter, or confusing how they relate. Remember: **diameter at a node = left_depth + right_depth + 2** (the path going down left, up through node, down right).
wrong_approach: "Return depth as the answer"
correct_approach: "Track diameter separately while computing depths"
- title: Forgetting the Path Doesn't Have to Pass Through Root
description: |
Many people assume the longest path must go through the root node. Consider a tree like:
```
1
/
2
/ \
3 4
/ \
5 6
```
The diameter is `4` (path: 5→3→2→4→6), which doesn't pass through node 1. You must check the diameter at every node, not just the root.
wrong_approach: "Only calculate left_depth + right_depth at root"
correct_approach: "Update diameter at every node during traversal"
- title: Off-by-One Errors with Depth Definition
description: |
There are two common conventions for depth:
- Edges: `None` = -1, leaf = 0
- Nodes: `None` = 0, leaf = 1
If using the edges convention, the diameter formula is `left + right + 2`. If using the nodes convention, it's `left + right`. Mixing these up causes off-by-one errors.
This solution uses the edges convention for clarity.
wrong_approach: "Inconsistent depth definitions"
correct_approach: "Stick to one convention: None=-1, leaf=0, diameter=left+right+2"
key_takeaways:
- "**Compute while traversing**: When you need a global property (like diameter), compute it during DFS rather than making separate passes"
- "**The pivot node pattern**: Many tree path problems involve finding the best path that passes through each node as a pivot point"
- "**Depth vs. Diameter**: Understand that depth is a local property (one subtree) while diameter is a global property (spanning across subtrees)"
- "**Similar problems**: This pattern applies to Binary Tree Maximum Path Sum, Longest Univalue Path, and other tree path problems"
time_complexity: "O(n). We visit each node exactly once during the DFS traversal."
space_complexity: "O(h) where h is the height of the tree. This is the recursion stack space, which is O(log n) for a balanced tree and O(n) for a skewed tree."
solutions:
- approach_name: DFS with Depth Calculation
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 diameter_of_binary_tree(root: TreeNode) -> int:
# Track the maximum diameter found across all nodes
diameter = 0
def depth(node: TreeNode) -> int:
nonlocal diameter
# Base case: empty node has depth -1
if not node:
return -1
# Recursively find depth of left and right subtrees
left_depth = depth(node.left)
right_depth = depth(node.right)
# Diameter through this node = left + right + 2 edges
# (one edge to left child, one edge to right child)
diameter = max(diameter, left_depth + right_depth + 2)
# Return depth of this subtree for parent's calculation
return max(left_depth, right_depth) + 1
depth(root)
return diameter
explanation: |
**Time Complexity:** O(n) — Each node is visited exactly once.
**Space Complexity:** O(h) — Recursion stack depth equals tree height.
We perform a post-order DFS, computing depths bottom-up. At each node, we calculate the potential diameter passing through it and update our global maximum. The depth returned to the parent is the longer of the two subtree depths plus one.
- approach_name: DFS with Class Variable
is_optimal: true
code: |
class Solution:
def diameter_of_binary_tree(self, root: TreeNode) -> int:
self.diameter = 0
def depth(node: TreeNode) -> int:
if not node:
return -1
left = depth(node.left)
right = depth(node.right)
# Update diameter if path through this node is longer
self.diameter = max(self.diameter, left + right + 2)
return max(left, right) + 1
depth(root)
return self.diameter
explanation: |
**Time Complexity:** O(n) — Single traversal of all nodes.
**Space Complexity:** O(h) — Recursion stack space.
This is the same algorithm but uses a class instance variable instead of `nonlocal`. This style is common in LeetCode submissions where the solution is wrapped in a class.
- approach_name: Iterative with Stack
is_optimal: false
code: |
def diameter_of_binary_tree(root: TreeNode) -> int:
if not root:
return 0
diameter = 0
depth_map = {} # Maps node to its depth
stack = [(root, False)] # (node, visited)
while stack:
node, visited = stack.pop()
if not node:
continue
if visited:
# Post-order: children already processed
left_depth = depth_map.get(node.left, -1)
right_depth = depth_map.get(node.right, -1)
# Calculate diameter through this node
diameter = max(diameter, left_depth + right_depth + 2)
# Store depth for parent's calculation
depth_map[node] = max(left_depth, right_depth) + 1
else:
# First visit: push back with visited=True, then children
stack.append((node, True))
stack.append((node.right, False))
stack.append((node.left, False))
return diameter
explanation: |
**Time Complexity:** O(n) — Each node is visited twice (once to push children, once to compute).
**Space Complexity:** O(n) — Stack and hash map both store up to n entries.
This iterative approach simulates post-order traversal using a stack. It's useful when recursion depth might cause stack overflow, but uses more space due to the hash map storing depths. For most practical cases, the recursive solution is cleaner.