234 lines
9.9 KiB
YAML
234 lines
9.9 KiB
YAML
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`)
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
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.
|