Facebook Pixel

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 to bᵢ₊₁ (or b₁ if it's the last leaf bₖ)
  • Each leaf bᵢ has its left child pointing to bᵢ₋₁ (or bₖ if it's the first leaf b₁)

This creates a circular chain among the leaves where:

  • The leaves form a cycle through their children pointers
  • b₁'s left child points to bₖ (the last leaf)
  • bₖ's right child points to b₁ (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:

  1. We need to traverse from the root to find the longest path to any node (the height)
  2. DFS naturally explores paths from root to leaves, keeping track of depth as it goes
  3. The special circular linking among leaves requires careful traversal logic, which DFS can handle by checking conditions before recursing
  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 child B, we should check: does B.right point back to A? If yes, then B 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 visited
  • d is the current depth from the tree's root
  • ans is a nonlocal variable tracking the maximum depth found

Key Implementation Details:

  1. 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.

  2. 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.

  3. 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.

  4. 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 Evaluator

Example 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:

  1. 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
  2. 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
  3. 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
  4. Back at node 2

    • Check right child: null, nothing to traverse
    • Return to node 1
  5. 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
  6. 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
  7. 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 to bi
  • And bi+1's left child points back to bi

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)
        # ...
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More