LeetCode 543 - Diameter of Binary Tree
The problem asks us to compute the diameter of a binary tree. The diameter is defined as the length of the longest path between any two nodes, measured in number of edges. Importantly, this path does not need to pass through the root of the tree.
Difficulty: 🟢 Easy
Topics: Tree, Depth-First Search, Binary Tree
Solution
Problem Understanding
The problem asks us to compute the diameter of a binary tree. The diameter is defined as the length of the longest path between any two nodes, measured in number of edges. Importantly, this path does not need to pass through the root of the tree. In other words, the path can lie entirely within the left subtree, right subtree, or span across the root.
The input is the root of a binary tree, which may contain up to 10,000 nodes, and node values can range from -100 to 100. The output is a single integer representing the diameter of the tree. The constraints indicate that we must consider performance carefully because naive solutions that repeatedly traverse subtrees could be too slow for large trees.
Important edge cases include a tree with a single node, which has a diameter of 0, and unbalanced trees where the longest path does not pass through the root.
Approaches
The brute-force approach would involve checking the longest path for every node. For each node, we could compute the height of its left and right subtrees, then sum them to get the longest path passing through that node. Repeating this for all nodes results in a lot of redundant computation, because subtree heights are recalculated multiple times. This leads to a time complexity of O(n^2) in the worst case for skewed trees.
The optimal approach leverages a single post-order depth-first search (DFS) traversal to calculate both the height of subtrees and the maximum diameter simultaneously. The key insight is that the longest path passing through a node is the sum of the heights of its left and right children. By updating a global maximum diameter during recursion, we only need one traversal to get the correct result, reducing the time complexity to O(n).
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^2) | O(n) | Computes height for every node multiple times; slow for large trees |
| Optimal | O(n) | O(h) | Single DFS traversal, updates global maximum; h is the tree height |
Algorithm Walkthrough
-
Define a global variable
max_diameterinitialized to 0. This variable will store the maximum diameter found during traversal. -
Implement a recursive DFS function that computes the height of a subtree rooted at a given node. Height is defined as the number of edges in the longest path from the node down to a leaf.
-
For each node in the DFS:
-
Recursively compute the left subtree height.
-
Recursively compute the right subtree height.
-
Update
max_diameteras the maximum of its current value and the sum of left and right heights. This represents the longest path passing through the current node. -
Return the height of the current node as 1 plus the maximum of the left and right heights.
-
Call DFS starting from the root.
-
Return
max_diameterafter traversal completes.
Why it works: At each node, we consider the longest path that passes through it. Because the DFS traversal ensures that all subtrees are processed before their parent, we always have correct heights available to compute local diameters. This guarantees that the maximum diameter across the entire tree is found.
Python Solution
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
self.max_diameter = 0
def dfs(node: Optional[TreeNode]) -> int:
if not node:
return 0
left_height = dfs(node.left)
right_height = dfs(node.right)
self.max_diameter = max(self.max_diameter, left_height + right_height)
return 1 + max(left_height, right_height)
dfs(root)
return self.max_diameter
Implementation walkthrough: The dfs function returns the height of a node, while simultaneously updating self.max_diameter with the longest path found at that node. Base case returns 0 for null nodes. For each node, we compute left and right heights, update the diameter, and propagate the height upward. This ensures a single traversal suffices.
Go Solution
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func diameterOfBinaryTree(root *TreeNode) int {
maxDiameter := 0
var dfs func(node *TreeNode) int
dfs = func(node *TreeNode) int {
if node == nil {
return 0
}
leftHeight := dfs(node.Left)
rightHeight := dfs(node.Right)
if leftHeight+rightHeight > maxDiameter {
maxDiameter = leftHeight + rightHeight
}
return 1 + max(leftHeight, rightHeight)
}
dfs(root)
return maxDiameter
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
Go-specific notes: Go requires handling nil pointers explicitly, and integers do not overflow in this scenario. A helper max function is used instead of Python's built-in max.
Worked Examples
Example 1: root = [1,2,3,4,5]
| Node | Left Height | Right Height | Max Diameter |
|---|---|---|---|
| 4 | 0 | 0 | 0 |
| 5 | 0 | 0 | 0 |
| 2 | 1 | 1 | 2 |
| 3 | 0 | 0 | 2 |
| 1 | 2 | 1 | 3 |
Final output: 3
Example 2: root = [1,2]
| Node | Left Height | Right Height | Max Diameter |
|---|---|---|---|
| 2 | 0 | 0 | 0 |
| 1 | 1 | 0 | 1 |
Final output: 1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each node is visited exactly once in DFS |
| Space | O(h) | Maximum recursion depth equals tree height h; O(n) worst-case for skewed tree |
The complexity is efficient even for the maximum constraint of 10,000 nodes.
Test Cases
# test cases
assert Solution().diameterOfBinaryTree(None) == 0 # empty tree
assert Solution().diameterOfBinaryTree(TreeNode(1)) == 0 # single node
assert Solution().diameterOfBinaryTree(TreeNode(1, TreeNode(2))) == 1 # two nodes
assert Solution().diameterOfBinaryTree(TreeNode(1, TreeNode(2), TreeNode(3))) == 2 # simple balanced
assert Solution().diameterOfBinaryTree(TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))) == 3 # example 1
| Test | Why |
|---|---|
None |
Handles empty tree, returns 0 |
TreeNode(1) |
Single node diameter is 0 |
TreeNode(1, TreeNode(2)) |
Diameter is 1 for two-node tree |
TreeNode(1, TreeNode(2), TreeNode(3)) |
Small balanced tree |
TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3)) |
Verifies example with diameter passing through root |
Edge Cases
The first edge case is a single-node tree, where the diameter must be 0. The algorithm handles this correctly by returning 0 when both left and right heights are 0.
The second edge case is a skewed tree, where all nodes are only on one side (like a linked list). In this case, the diameter equals the number of nodes minus 1. The algorithm still works because DFS calculates heights correctly along the chain.
The third edge case is a tree where the longest path does not pass through the root. For example, a tree with a deep left subtree and a deep right subtree that does not share the root node. The global max_diameter ensures that all possible node pairs are considered, so the maximum path is captured correctly regardless of whether it passes through the root.