2773. Height of Special Binary Tree 🔒
Problem Description
You are given the root of a special binary tree with n
nodes numbered from 1
to n
. This tree has k
leaves arranged in ascending order: b₁ < b₂ < ... < bₖ
.
The leaves of this tree have a unique circular linking property:
- Each leaf
bᵢ
has its right child pointing tobᵢ₊₁
(orb₁
if it's the last leafbₖ
) - Each leaf
bᵢ
has its left child pointing tobᵢ₋₁
(orbₖ
if it's the first leafb₁
)
This creates a circular chain among the leaves where:
- The leaves form a cycle through their children pointers
b₁
's left child points tobₖ
(the last leaf)bₖ
's right child points tob₁
(the first leaf)- All other leaves point to their adjacent neighbors in the sequence
Your task is to find the height of this tree, which is the length of the longest path from the root to any other node.
The challenge lies in correctly identifying which nodes are actual leaves versus internal nodes, since the circular linking means that nodes we would traditionally consider leaves (having no children in a normal tree) actually have pointers to other leaves. The solution needs to detect and avoid following these circular leaf connections when calculating the tree height.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves a binary tree, which is a special type of graph with nodes (tree nodes) and edges (parent-child relationships).
Is it a tree?
- Yes: The problem explicitly states we're working with a special binary tree with a root node and a hierarchical structure.
DFS
- Conclusion: Since we're dealing with a tree structure, the flowchart leads us directly to using DFS (Depth-First Search).
The flowchart confirms that DFS is the appropriate approach for this problem. This makes sense because:
- We need to traverse from the root to find the longest path to any node (the height)
- DFS naturally explores paths from root to leaves, keeping track of depth as it goes
- The special circular linking among leaves requires careful traversal logic, which DFS can handle by checking conditions before recursing
- We need to explore all possible paths to determine the maximum depth, and DFS systematically explores each branch
Conclusion: The flowchart suggests using a DFS approach to traverse the special binary tree and calculate its height by tracking the maximum depth reached during the traversal.
Intuition
The key insight is recognizing that this isn't a standard tree traversal problem due to the circular linking among leaves. In a normal tree, leaves have no children, making it easy to identify when to stop traversing. However, in this special tree, the leaves point to each other in a circular fashion, creating potential infinite loops if we're not careful.
Let's think about what makes this tree "special":
- Leaves are connected in a circle:
b₁ → b₂ → ... → bₖ → b₁
- Each leaf has both left and right children pointing to other leaves
- If we blindly follow these pointers, we'll get stuck in the circular structure
The breakthrough comes from realizing that we need a way to detect when we're about to enter this circular leaf structure. How can we identify that we're at a leaf trying to traverse to another leaf?
Consider this: if we're at a leaf node bᵢ
and we go to its child (say bᵢ₊₁
), then from bᵢ₊₁
's perspective, one of its children should point back to bᵢ
. This creates a bidirectional relationship between leaf nodes.
So the detection mechanism becomes:
- If we're at node
A
and want to traverse to its left childB
, we should check: doesB.right
point back toA
? If yes, thenB
is another leaf in the circular chain, and we shouldn't traverse there. - Similarly for the right child: if
A.right.left == A
, then we're about to enter the leaf cycle.
This observation allows us to use standard DFS while avoiding the circular trap. We traverse normally through the tree's internal structure but stop when we detect we're about to follow a circular leaf connection. By tracking the depth at each node and updating our maximum, we can find the tree's true height without getting caught in the leaf cycle.
Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.
Solution Approach
The implementation uses a DFS traversal with a clever condition to avoid the circular leaf connections. Let's walk through the solution step by step:
Core Algorithm Structure:
We implement a recursive DFS function dfs(root, d)
where:
root
is the current node being visitedd
is the current depth from the tree's rootans
is a nonlocal variable tracking the maximum depth found
Key Implementation Details:
-
Base Case and Answer Update: At each node, we update
ans = max(ans, d)
to keep track of the maximum depth reached so far. This ensures we capture the height regardless of which path leads to the deepest node. -
Detecting Circular Connections: The critical logic lies in the conditions before recursive calls:
if root.left and root.left.right != root: dfs(root.left, d + 1)
This checks two things:
root.left
exists (the node has a left child)root.left.right != root
(the left child's right pointer doesn't point back to current node)
If the second condition fails, it means we're at a leaf trying to traverse to another leaf in the circular chain, so we stop.
-
Symmetric Check for Right Child:
if root.right and root.right.left != root: dfs(root.right, d + 1)
Similarly, we only traverse to the right child if it doesn't have a left pointer pointing back to us.
-
Depth Tracking: Each recursive call increments the depth by 1 (
d + 1
), allowing us to track how far we are from the root.
Why This Works:
The solution elegantly handles the special tree structure by:
- Allowing normal traversal through the tree's internal nodes
- Detecting and avoiding the circular leaf connections
- Still visiting all reachable nodes exactly once
- Correctly computing the maximum depth without infinite loops
The time complexity is O(n)
since we visit each node at most once, and the space complexity is O(h)
where h
is the height of the tree due to the recursion stack.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate how the solution handles the circular leaf connections.
Consider this special binary tree:
1 / \ 2 3 / 4
Where nodes 3 and 4 are leaves with circular connections:
- Node 3's left child points to node 4
- Node 3's right child points to node 4 (since there are only 2 leaves)
- Node 4's left child points to node 3
- Node 4's right child points to node 3
Step-by-step DFS traversal:
-
Start at root (node 1), depth = 0
- Update
ans = max(0, 0) = 0
- Check left child (node 2):
- Node 2 exists ✓
- Node 2's right child is null, not equal to node 1 ✓
- Traverse to node 2
- Update
-
At node 2, depth = 1
- Update
ans = max(0, 1) = 1
- Check left child (node 4):
- Node 4 exists ✓
- Node 4's right child is node 3, not equal to node 2 ✓
- Traverse to node 4
- Update
-
At node 4 (leaf), depth = 2
- Update
ans = max(1, 2) = 2
- Check left child (node 3):
- Node 3 exists ✓
- Node 3's right child is node 4 (points back!) ✗
- Stop - detected circular connection
- Check right child (node 3):
- Node 3 exists ✓
- Node 3's left child is node 4 (points back!) ✗
- Stop - detected circular connection
- Return to node 2
- Update
-
Back at node 2
- Check right child: null, nothing to traverse
- Return to node 1
-
Back at root (node 1)
- Check right child (node 3):
- Node 3 exists ✓
- Node 3's left child is node 4, not equal to node 1 ✓
- Traverse to node 3
- Check right child (node 3):
-
At node 3 (leaf), depth = 1
- Update
ans = max(2, 1) = 2
- Check left child (node 4):
- Node 4 exists ✓
- Node 4's right child is node 3 (points back!) ✗
- Stop - detected circular connection
- Check right child (node 4):
- Node 4 exists ✓
- Node 4's left child is node 3 (points back!) ✗
- Stop - detected circular connection
- Return to node 1
- Update
-
Final result: height = 2
The key moments were at steps 3 and 6, where the algorithm detected it was about to follow a circular leaf connection and stopped. Without these checks, the traversal would have entered an infinite loop between nodes 3 and 4. The algorithm correctly identified that the longest path from root is to node 4 (via node 2), giving us a height of 2.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import Optional
9
10class Solution:
11 def heightOfTree(self, root: Optional[TreeNode]) -> int:
12 """
13 Calculate the height of a binary tree.
14 Height is defined as the maximum depth from root to any leaf.
15
16 Args:
17 root: The root node of the binary tree
18
19 Returns:
20 The height of the tree (0 for single node, -1 for empty tree based on implementation)
21 """
22 # Initialize maximum depth found so far
23 self.max_depth = 0
24
25 def traverse_tree(node: Optional[TreeNode], current_depth: int) -> None:
26 """
27 Recursively traverse the tree using DFS to find maximum depth.
28 Includes cycle detection to prevent infinite recursion.
29
30 Args:
31 node: Current node being visited
32 current_depth: Depth of the current node from root
33 """
34 # Update the maximum depth if current depth is greater
35 self.max_depth = max(self.max_depth, current_depth)
36
37 # Traverse left subtree if it exists and doesn't create a cycle
38 # (checking if left child's right pointer doesn't point back to current node)
39 if node.left and node.left.right != node:
40 traverse_tree(node.left, current_depth + 1)
41
42 # Traverse right subtree if it exists and doesn't create a cycle
43 # (checking if right child's left pointer doesn't point back to current node)
44 if node.right and node.right.left != node:
45 traverse_tree(node.right, current_depth + 1)
46
47 # Start DFS traversal from root with initial depth 0
48 traverse_tree(root, 0)
49
50 return self.max_depth
51
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 // Instance variable to store the maximum height found
18 private int maxHeight;
19
20 /**
21 * Calculates the height of a binary tree.
22 * The height is defined as the number of edges on the longest path from root to leaf.
23 *
24 * @param root The root node of the binary tree
25 * @return The height of the tree
26 */
27 public int heightOfTree(TreeNode root) {
28 maxHeight = 0;
29 depthFirstSearch(root, 0);
30 return maxHeight;
31 }
32
33 /**
34 * Performs depth-first search to find the maximum depth in the tree.
35 * Includes cycle detection to prevent infinite recursion.
36 *
37 * @param currentNode The current node being processed
38 * @param currentDepth The depth of the current node from the root
39 */
40 private void depthFirstSearch(TreeNode currentNode, int currentDepth) {
41 // Update the maximum height if current depth is greater
42 maxHeight = Math.max(maxHeight, currentDepth);
43
44 // Increment depth for child nodes (post-increment was used, maintaining that behavior)
45 currentDepth++;
46
47 // Traverse left subtree if it exists and doesn't create a cycle
48 // (checking if left child's right pointer doesn't point back to current node)
49 if (currentNode.left != null && currentNode.left.right != currentNode) {
50 depthFirstSearch(currentNode.left, currentDepth);
51 }
52
53 // Traverse right subtree if it exists and doesn't create a cycle
54 // (checking if right child's left pointer doesn't point back to current node)
55 if (currentNode.right != null && currentNode.right.left != currentNode) {
56 depthFirstSearch(currentNode.right, currentDepth);
57 }
58 }
59}
60
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 int heightOfTree(TreeNode* root) {
15 // Variable to store the maximum depth found
16 int maxDepth = 0;
17
18 // Define a recursive lambda function for depth-first search
19 // Parameters: current node and its depth in the tree
20 function<void(TreeNode*, int)> dfs = [&](TreeNode* node, int currentDepth) {
21 // Update the maximum depth if current depth is greater
22 maxDepth = max(maxDepth, currentDepth);
23
24 // Increment depth for the next level
25 currentDepth++;
26
27 // Traverse left subtree if it exists and doesn't create a cycle
28 // (checking if left child's right pointer doesn't point back to current node)
29 if (node->left && node->left->right != node) {
30 dfs(node->left, currentDepth);
31 }
32
33 // Traverse right subtree if it exists and doesn't create a cycle
34 // (checking if right child's left pointer doesn't point back to current node)
35 if (node->right && node->right->left != node) {
36 dfs(node->right, currentDepth);
37 }
38 };
39
40 // Start DFS from root with initial depth of 0
41 dfs(root, 0);
42
43 // Return the maximum depth found
44 return maxDepth;
45 }
46};
47
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Calculates the height of a binary tree with cycle detection.
17 * The function checks for potential cycles by verifying that a child's pointer
18 * doesn't point back to its parent, preventing infinite recursion.
19 *
20 * @param root - The root node of the binary tree
21 * @returns The maximum depth/height of the tree
22 */
23function heightOfTree(root: TreeNode | null): number {
24 // Initialize the maximum depth found
25 let maxDepth: number = 0;
26
27 /**
28 * Depth-first search helper function to traverse the tree
29 * @param node - Current node being processed
30 * @param currentDepth - Current depth in the tree
31 */
32 const dfs = (node: TreeNode | null, currentDepth: number): void => {
33 // Handle null node case
34 if (node === null) {
35 return;
36 }
37
38 // Update maximum depth and increment for next level
39 maxDepth = Math.max(maxDepth, currentDepth);
40 const nextDepth: number = currentDepth + 1;
41
42 // Traverse left subtree with cycle detection
43 // Check if left child exists and doesn't create a cycle back to parent
44 if (node.left !== null && node.left.right !== node) {
45 dfs(node.left, nextDepth);
46 }
47
48 // Traverse right subtree with cycle detection
49 // Check if right child exists and doesn't create a cycle back to parent
50 if (node.right !== null && node.right.left !== node) {
51 dfs(node.right, nextDepth);
52 }
53 };
54
55 // Start DFS traversal from root at depth 0
56 dfs(root, 0);
57
58 return maxDepth;
59}
60
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node is visited exactly once during the traversal. The condition checks root.left.right != root
and root.right.left != root
are performed in constant time O(1)
for each node. Since we visit all n
nodes in the tree exactly once, the overall time complexity is O(n)
.
Space Complexity: O(n)
The space complexity comes from the recursive call stack used during the DFS traversal. In the worst case, when the tree is completely unbalanced (essentially a linked list), the recursion depth can reach n
levels deep, requiring O(n)
space on the call stack. In the best case of a perfectly balanced tree, the recursion depth would be O(log n)
, but we consider the worst-case scenario for space complexity analysis, which gives us O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Cycle Detection Logic
The Pitfall: A common mistake is using the wrong conditions to detect circular connections among leaves. Developers might incorrectly check:
root.left != root.right
(comparing siblings instead of detecting cycles)root.left.left != root
(checking the wrong pointer direction)- Only checking one direction but not both
Why It Happens:
The circular linking pattern can be confusing. When at a leaf node bi
:
- Its left child points to
bi-1
- Its right child points to
bi+1
- But
bi-1
's right child points back tobi
- And
bi+1
's left child points back tobi
Incorrect Implementation Example:
# Wrong: This doesn't properly detect the circular connection if root.left and root.left != root.right: # Comparing siblings dfs(root.left, d + 1) # Wrong: Checking the same direction twice if root.left and root.left.left != root: # Should check root.left.right dfs(root.left, d + 1)
Solution: Always check the complementary pointer direction:
- When going LEFT from current node, check if left child's RIGHT pointer points back
- When going RIGHT from current node, check if right child's LEFT pointer points back
# Correct implementation if root.left and root.left.right != root: dfs(root.left, d + 1) if root.right and root.right.left != root: dfs(root.right, d + 1)
2. Forgetting to Handle Edge Cases
The Pitfall: Not properly handling special cases like:
- Single node tree (just the root)
- Tree where root itself is a leaf
- Empty tree (if root is None)
Solution: Add proper null checks and handle the base case:
def heightOfTree(self, root: Optional[TreeNode]) -> int:
if not root:
return -1 # or 0, depending on problem definition
self.max_depth = 0
# ... rest of the implementation
3. Using Global Variables Incorrectly
The Pitfall:
Using a regular variable instead of self.max_depth
or forgetting the nonlocal
keyword when using a nested function approach:
# Wrong: This won't work as expected
def heightOfTree(self, root):
max_depth = 0 # Local variable
def dfs(node, d):
max_depth = max(max_depth, d) # UnboundLocalError!
# ...
Solution:
Either use instance variables (self.max_depth
) or properly declare nonlocal:
# Option 1: Instance variable
self.max_depth = 0
# Option 2: Nonlocal declaration
def heightOfTree(self, root):
max_depth = 0
def dfs(node, d):
nonlocal max_depth
max_depth = max(max_depth, d)
# ...
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!