LeetCode 543: Diameter of a Binary Tree in C++ & Post-Order DFS

In tree traversal algorithms, one of the most frequent misconceptions encountered in technical interviews is assuming that the longest path between two nodes in a binary tree must inevitably pass through the root node. In LeetCode 543: Diameter of a Binary Tree, candidates are asked to find the length of the longest path between any two arbitrary nodes in a binary tree, defined specifically by the number of edges connecting them. Because a tree may possess a heavily skewed, deep branch isolated within a single subtree, the true diameter often bypasses the root completely. Below is an in-depth architectural and algorithmic deconstruction of the diameter problem in C++, exploring why naive top-down height recalculation causes an O(N^2) quadratic bottleneck, how bottom-up post-order DFS collapses time complexity to optimal O(N), and how C++ pointer layouts and stack frames impact cache locality.

1. Deconstructing the Diameter Invariant & The Root Trap

Formally, the diameter of a binary tree is the length (measured in edges) of the longest path between any two nodes. For any individual node u, the longest path that has u as its highest common ancestor is equal to the maximum depth of its left subtree plus the maximum depth of its right subtree:

Diameter_Through(u) = MaxDepth(u.left) + MaxDepth(u.right)

The global tree diameter is therefore the maximum value of Diameter_Through(u) across all nodes u in the tree. Consider the structural distinction illustrated below: in Tree A, the diameter passes cleanly through the root; in Tree B, an asymmetrical deep cluster creates a diameter fully contained inside the left subtree.

Tree A: Path Passes Through Root (Edges = 3) Tree B: Diameter Bypasses Root Entirely 1 2 3 4 5 Longest Path: [4 → 2 → 1 → 3] = 3 Edges Root R1 L1 L2 L3 L4 L5 Diameter = 4 Edges (Path within Left Subtree: L4 → L5)

2. Naive Top-Down vs. Optimal Bottom-Up Post-Order DFS

A frequent anti-pattern is computing the tree height at every node using a separate helper function:

// ANTI-PATTERN: Quadratic O(N^2) Top-Down Traversal
int depth(TreeNode* node) {
    if (!node) return 0;
    return 1 + std::max(depth(node->left), depth(node->right));
}
int diameterOfBinaryTree(TreeNode* root) {
    if (!root) return 0;
    int current = depth(root->left) + depth(root->right);
    return std::max({current, diameterOfBinaryTree(root->left), diameterOfBinaryTree(root->right)});
}

In this naive top-down formulation, computing depth() traverses downward repeatedly. For a degenerate linked-list tree of N nodes, this yields a recurrence relation of T(N) = T(N - 1) + O(N), degenerating to O(N^2) quadratic time. For large trees ($N = 10^4$), this triggers severe performance timeouts.

The Bottom-Up Post-Order DFS Invariant (O(N))

We can eliminate all redundant computation by adopting a bottom-up post-order traversal (Left, Right, Root). During the recursive ascent:

  1. We recursively compute the height of the left subtree and the height of the right subtree.
  2. At the current node, we update our global maximum diameter: maxDiameter = std::max(maxDiameter, leftHeight + rightHeight).
  3. We return the current node's height to its parent caller: 1 + std::max(leftHeight, rightHeight).

Because every node is visited exactly once, the entire calculation completes in strict O(N) linear time.

3. Memory Layout & Cache Considerations in C++

In high-performance C++ systems engineering, binary tree representation deserves careful inspection:

  • Structure Padding & Pointer Overhead: A standard 64-bit TreeNode contains an int val (4 bytes), 4 bytes of compiler structure padding, and two 8-byte raw pointers (left and right), totaling 24 bytes per node. Nodes scattered across the heap via individual new allocations result in pointer chasing and frequent CPU L1/L2 cache misses.
  • Call Stack Frames: Each recursive DFS invocation consumes a stack frame storing return addresses and local integers. The maximum recursion depth is bounded by the height of the tree H: for balanced trees H = O(log N) (approx. 14 frames for 10,000 nodes); for degenerate skewed trees H = O(N), requiring attention to stack memory limits.

4. Production-Grade Solutions

Below is the modernized, leak-free C++20 implementation avoiding global mutable state, accompanied by clean TypeScript and Python 3 solutions.

Production C++20 Implementation

#include <algorithm>
#include <iostream>
#include <memory>

// Binary tree node definition
struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode* l, TreeNode* r) : val(x), left(l), right(r) {}
};

class Solution {
private:
    // Post-order DFS helper returning subtree height
    int calculateDepth(TreeNode* node, int& maxDiameter) {
        if (!node) {
            return 0;
        }

        // Bottom-up recursion: visit children first
        int leftHeight = calculateDepth(node->left, maxDiameter);
        int rightHeight = calculateDepth(node->right, maxDiameter);

        // Update diameter candidate at this local subtree root
        maxDiameter = std::max(maxDiameter, leftHeight + rightHeight);

        // Return height of current node to its parent
        return 1 + std::max(leftHeight, rightHeight);
    }

public:
    int diameterOfBinaryTree(TreeNode* root) {
        int maxDiameter = 0;
        calculateDepth(root, maxDiameter);
        return maxDiameter;
    }
};

// Clean RAII Tree builder for verification
int main() {
    // Tree: 1 -> (left: 2 -> [4, 5], right: 3)
    auto n4 = std::make_unique<TreeNode>(4);
    auto n5 = std::make_unique<TreeNode>(5);
    auto n2 = std::make_unique<TreeNode>(2, n4.get(), n5.get());
    auto n3 = std::make_unique<TreeNode>(3);
    auto root = std::make_unique<TreeNode>(1, n2.get(), n3.get());

    Solution sol;
    int diameter = sol.diameterOfBinaryTree(root.get());
    std::cout << "Calculated Tree Diameter: " << diameter << " edges." << std::endl;
    return 0;
}

Modern TypeScript Implementation

export class TreeNode {
  val: number;
  left: TreeNode | null;
  right: TreeNode | null;
  constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

export function diameterOfBinaryTree(root: TreeNode | null): number {
  let maxDiameter = 0;

  function getDepth(node: TreeNode | null): number {
    if (!node) return 0;

    const left = getDepth(node.left);
    const right = getDepth(node.right);

    // Update global diameter candidate
    maxDiameter = Math.max(maxDiameter, left + right);

    return 1 + Math.max(left, right);
  }

  getDepth(root);
  return maxDiameter;
}

Python 3 Implementation

from typing import Optional

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:
        max_diameter = 0

        def dfs(node: Optional[TreeNode]) -> int:
            nonlocal max_diameter
            if not node:
                return 0

            left_height = dfs(node.left)
            right_height = dfs(node.right)

            max_diameter = max(max_diameter, left_height + right_height)
            return 1 + max(left_height, right_height)

        dfs(root)
        return max_diameter

5. Asymptotic Complexity Breakdown

  • Time Complexity: O(N): Each of the N nodes in the binary tree is traversed exactly once during post-order traversal. At each node, computing the maximum of two integers and adding one is an O(1) primitive operation.
  • Auxiliary Space Complexity: O(H): Auxiliary memory is governed exclusively by the implicit recursive function call stack. In a balanced binary tree, height H = ⌊log2 N⌋, giving O(log N) space. In the worst-case degenerate linked list (where every node has only one child), H = N, demanding O(N) stack memory.

6. Edge Cases & Verification Scenarios

Defensive verification must encompass tree topologies that stress recursive base conditions:

  • Single Node Tree ([1]): When the root has no children, leftHeight = 0 and rightHeight = 0. The diameter is 0 edges, which is the correct mathematical result.
  • Two-Node Tree ([1, 2]): Exactly one edge connects root and child. Diameter evaluates to 1.
  • Linear Degenerate Skew (1 → 2 → 3 → 4): Longest path is N - 1 edges. Bottom-up traversal cleanly bubbles depths up without stack overflow.
  • Subtree Dominance: When the left subtree has a diameter of 8 and the right subtree has depth 1, the algorithm correctly records 8 from the left subtree rather than forcing the path through the top root.