Structural computations
Computing properties of a tree from its subtrees: node count, height, diameter, the balance check, leaf count and the maximum path sum.
Learning goals
- Compute a property by combining the answers from both subtrees
- Calculate the diameter and the maximum path sum with a global best
- Check balance without recomputing heights repeatedly
- Use a sentinel value to stop early
Prerequisites
Most tree questions have the same shape: ask each child a question, combine their answers, and sometimes record a candidate for a global answer on the way. The skill is deciding what to return and what to record.
Count nodes
The size of a tree is one for the root plus the sizes of its two subtrees, with 0 for an empty tree. This is postorder: the children answer first, and the parent combines. The animation shows n= under every node, the size of its subtree; on the “Count nodes” preset the root shows 6, the total.
1static int countNodes(TreeNode n) {2if (n == null) return 0;3return 1 + countNodes(n.left) + countNodes(n.right);4}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(h)
Computing node count, height, leaves, diameter, balance and path sum bottom-up
Interactive · edit the inputbinary tree · 5 nodes
Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value.
1static int countNodes(TreeNode n) {2if (n == null) return 0;3return 1 + countNodes(n.left) + countNodes(n.right);4}5static int height(TreeNode n) { // edges on the longest downward path; an empty tree is -16if (n == null) return -1;7return 1 + Math.max(height(n.left), height(n.right));8}9static int leaves(TreeNode n) {10if (n == null) return 0;11if (n.left == null && n.right == null) return 1;12return leaves(n.left) + leaves(n.right);13}14static int best;15static int diameterHeight(TreeNode n) { // fills best with the most edges on any path16if (n == null) return -1;17int l = diameterHeight(n.left), r = diameterHeight(n.right);18best = Math.max(best, l + r + 2); // the path that bends at n uses both subtrees19return 1 + Math.max(l, r);20}21static int balancedHeight(TreeNode n) { // -2 means "unbalanced somewhere below"22if (n == null) return -1;23int l = balancedHeight(n.left); if (l == -2) return -2;24int r = balancedHeight(n.right); if (r == -2) return -2;25if (Math.abs(l - r) > 1) return -2;26return 1 + Math.max(l, r);27}28static int bestPath;29static int gain(TreeNode n) { // best downward path sum starting at n30if (n == null) return 0;31int l = Math.max(0, gain(n.left)), r = Math.max(0, gain(n.right));32bestPath = Math.max(bestPath, n.val + l + r); // the path that bends at n33return n.val + Math.max(l, r);34}
Live state
quantity
Diameter
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L15 | quantity=Diameter | Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value. |
| 2 | L18 | quantity=Diameter best=0 | A path bending at 4 has -1 + -1 + 2 = 0 edges (heights -1 and -1). New best diameter. |
| 3 | L18 | quantity=Diameter best=0 | A path bending at 5 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 4 | L18 | quantity=Diameter best=2 | A path bending at 2 has 0 + 0 + 2 = 2 edges (heights 0 and 0). New best diameter. |
| 5 | L18 | quantity=Diameter best=2 | A path bending at 3 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 6 | L18 | quantity=Diameter best=3 | A path bending at 1 has 1 + 0 + 2 = 3 edges (heights 1 and 0). New best diameter. |
| 7 | L19 | result=3 | Diameter: 3 edges (the longest path between any two nodes). |
Height
Height is one more than the taller child's height, with -1 for an empty tree. The animation shows h= under every node: leaves have 0, and the root of the “Height” preset (“1 2 3 4 5 # 6”) has 2 because the longest root-to-leaf path has 2 edges.
If a problem defines the height of a tree in nodes (a leaf has height 1), return 0 for the empty tree and add 1 as before; only the base case moves.
1static int height(TreeNode n) {2if (n == null) return -1;3return 1 + Math.max(height(n.left), height(n.right));4}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(h)
Computing node count, height, leaves, diameter, balance and path sum bottom-up
Interactive · edit the inputbinary tree · 5 nodes
Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value.
1static int countNodes(TreeNode n) {2if (n == null) return 0;3return 1 + countNodes(n.left) + countNodes(n.right);4}5static int height(TreeNode n) { // edges on the longest downward path; an empty tree is -16if (n == null) return -1;7return 1 + Math.max(height(n.left), height(n.right));8}9static int leaves(TreeNode n) {10if (n == null) return 0;11if (n.left == null && n.right == null) return 1;12return leaves(n.left) + leaves(n.right);13}14static int best;15static int diameterHeight(TreeNode n) { // fills best with the most edges on any path16if (n == null) return -1;17int l = diameterHeight(n.left), r = diameterHeight(n.right);18best = Math.max(best, l + r + 2); // the path that bends at n uses both subtrees19return 1 + Math.max(l, r);20}21static int balancedHeight(TreeNode n) { // -2 means "unbalanced somewhere below"22if (n == null) return -1;23int l = balancedHeight(n.left); if (l == -2) return -2;24int r = balancedHeight(n.right); if (r == -2) return -2;25if (Math.abs(l - r) > 1) return -2;26return 1 + Math.max(l, r);27}28static int bestPath;29static int gain(TreeNode n) { // best downward path sum starting at n30if (n == null) return 0;31int l = Math.max(0, gain(n.left)), r = Math.max(0, gain(n.right));32bestPath = Math.max(bestPath, n.val + l + r); // the path that bends at n33return n.val + Math.max(l, r);34}
Live state
quantity
Diameter
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L15 | quantity=Diameter | Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value. |
| 2 | L18 | quantity=Diameter best=0 | A path bending at 4 has -1 + -1 + 2 = 0 edges (heights -1 and -1). New best diameter. |
| 3 | L18 | quantity=Diameter best=0 | A path bending at 5 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 4 | L18 | quantity=Diameter best=2 | A path bending at 2 has 0 + 0 + 2 = 2 edges (heights 0 and 0). New best diameter. |
| 5 | L18 | quantity=Diameter best=2 | A path bending at 3 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 6 | L18 | quantity=Diameter best=3 | A path bending at 1 has 1 + 0 + 2 = 3 edges (heights 1 and 0). New best diameter. |
| 7 | L19 | result=3 | Diameter: 3 edges (the longest path between any two nodes). |
Diameter
The **diameter** is the number of edges on the longest path between any two nodes. That path may not go through the root. For each node, the longest path that *bends* at it has length height(left) + height(right) + 2 (edges, with -1 for a missing child). The answer is the maximum of that over all nodes, kept in a global variable while the function returns heights.
In the “Diameter (bends below the root)” preset the tree “1 2 3 4 5 # # 6 # # 7” has height 3, but the longest path 6 → 4 → 2 → 5 → 7 bends at node 2 and has 4 edges; a solution that only looked at the root would report less. Each call does O(1) work beyond its children, so the cost is O(n).
1static int best;2static int diameter(TreeNode root) {3best = 0;4heightOf(root);5return best;6}7static int heightOf(TreeNode n) {8if (n == null) return -1;9int l = heightOf(n.left), r = heightOf(n.right);10best = Math.max(best, l + r + 2); // the path that bends at n uses both subtrees11return 1 + Math.max(l, r);12}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(h)
Computing node count, height, leaves, diameter, balance and path sum bottom-up
Interactive · edit the inputbinary tree · 5 nodes
Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value.
1static int countNodes(TreeNode n) {2if (n == null) return 0;3return 1 + countNodes(n.left) + countNodes(n.right);4}5static int height(TreeNode n) { // edges on the longest downward path; an empty tree is -16if (n == null) return -1;7return 1 + Math.max(height(n.left), height(n.right));8}9static int leaves(TreeNode n) {10if (n == null) return 0;11if (n.left == null && n.right == null) return 1;12return leaves(n.left) + leaves(n.right);13}14static int best;15static int diameterHeight(TreeNode n) { // fills best with the most edges on any path16if (n == null) return -1;17int l = diameterHeight(n.left), r = diameterHeight(n.right);18best = Math.max(best, l + r + 2); // the path that bends at n uses both subtrees19return 1 + Math.max(l, r);20}21static int balancedHeight(TreeNode n) { // -2 means "unbalanced somewhere below"22if (n == null) return -1;23int l = balancedHeight(n.left); if (l == -2) return -2;24int r = balancedHeight(n.right); if (r == -2) return -2;25if (Math.abs(l - r) > 1) return -2;26return 1 + Math.max(l, r);27}28static int bestPath;29static int gain(TreeNode n) { // best downward path sum starting at n30if (n == null) return 0;31int l = Math.max(0, gain(n.left)), r = Math.max(0, gain(n.right));32bestPath = Math.max(bestPath, n.val + l + r); // the path that bends at n33return n.val + Math.max(l, r);34}
Live state
quantity
Diameter
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L15 | quantity=Diameter | Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value. |
| 2 | L18 | quantity=Diameter best=0 | A path bending at 4 has -1 + -1 + 2 = 0 edges (heights -1 and -1). New best diameter. |
| 3 | L18 | quantity=Diameter best=0 | A path bending at 5 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 4 | L18 | quantity=Diameter best=2 | A path bending at 2 has 0 + 0 + 2 = 2 edges (heights 0 and 0). New best diameter. |
| 5 | L18 | quantity=Diameter best=2 | A path bending at 3 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 6 | L18 | quantity=Diameter best=3 | A path bending at 1 has 1 + 0 + 2 = 3 edges (heights 1 and 0). New best diameter. |
| 7 | L19 | result=3 | Diameter: 3 edges (the longest path between any two nodes). |
Balanced tree check
A tree is **height-balanced** when at every node the heights of its two subtrees differ by at most 1. The straightforward solution calls height at every node and costs O(n²) on a chain. The linear solution returns the height from each call, and a sentinel (-2 here) when an imbalance has been found, which propagates up and stops the work early.
“3 9 20 # # 15 7” is balanced. “1 2 # 3 # 4” (a left chain of three below the root) is not: at the root the left subtree has height 2 and the right is empty (height -1), a difference of 3. The animation marks the first unbalanced node it meets, in post-order.
1static boolean isBalanced(TreeNode root) {2return balancedHeight(root) != -2;3}4static int balancedHeight(TreeNode n) { // -2 means "unbalanced somewhere below"5if (n == null) return -1;6int l = balancedHeight(n.left);7if (l == -2) return -2;8int r = balancedHeight(n.right);9if (r == -2) return -2;10if (Math.abs(l - r) > 1) return -2;11return 1 + Math.max(l, r);12}
Best
O(1)
Average
O(n)
Worst
O(n)
Space
O(h)
Computing node count, height, leaves, diameter, balance and path sum bottom-up
Interactive · edit the inputbinary tree · 5 nodes
Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value.
1static int countNodes(TreeNode n) {2if (n == null) return 0;3return 1 + countNodes(n.left) + countNodes(n.right);4}5static int height(TreeNode n) { // edges on the longest downward path; an empty tree is -16if (n == null) return -1;7return 1 + Math.max(height(n.left), height(n.right));8}9static int leaves(TreeNode n) {10if (n == null) return 0;11if (n.left == null && n.right == null) return 1;12return leaves(n.left) + leaves(n.right);13}14static int best;15static int diameterHeight(TreeNode n) { // fills best with the most edges on any path16if (n == null) return -1;17int l = diameterHeight(n.left), r = diameterHeight(n.right);18best = Math.max(best, l + r + 2); // the path that bends at n uses both subtrees19return 1 + Math.max(l, r);20}21static int balancedHeight(TreeNode n) { // -2 means "unbalanced somewhere below"22if (n == null) return -1;23int l = balancedHeight(n.left); if (l == -2) return -2;24int r = balancedHeight(n.right); if (r == -2) return -2;25if (Math.abs(l - r) > 1) return -2;26return 1 + Math.max(l, r);27}28static int bestPath;29static int gain(TreeNode n) { // best downward path sum starting at n30if (n == null) return 0;31int l = Math.max(0, gain(n.left)), r = Math.max(0, gain(n.right));32bestPath = Math.max(bestPath, n.val + l + r); // the path that bends at n33return n.val + Math.max(l, r);34}
Live state
quantity
Diameter
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L15 | quantity=Diameter | Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value. |
| 2 | L18 | quantity=Diameter best=0 | A path bending at 4 has -1 + -1 + 2 = 0 edges (heights -1 and -1). New best diameter. |
| 3 | L18 | quantity=Diameter best=0 | A path bending at 5 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 4 | L18 | quantity=Diameter best=2 | A path bending at 2 has 0 + 0 + 2 = 2 edges (heights 0 and 0). New best diameter. |
| 5 | L18 | quantity=Diameter best=2 | A path bending at 3 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 6 | L18 | quantity=Diameter best=3 | A path bending at 1 has 1 + 0 + 2 = 3 edges (heights 1 and 0). New best diameter. |
| 7 | L19 | result=3 | Diameter: 3 edges (the longest path between any two nodes). |
Leaf count
A leaf has no children, so it contributes 1; every other node contributes the leaf counts of its subtrees. Be careful with the base case: a node with exactly one child is not a leaf, and returning 1 for it (or for null) is the common bug. The “Leaf count” preset of “1 2 3 4 5 # 6” has 3 leaves (4, 5 and 6).
1static int leaves(TreeNode n) {2if (n == null) return 0;3if (n.left == null && n.right == null) return 1; // a real leaf4return leaves(n.left) + leaves(n.right);5}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(h)
Computing node count, height, leaves, diameter, balance and path sum bottom-up
Interactive · edit the inputbinary tree · 5 nodes
Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value.
1static int countNodes(TreeNode n) {2if (n == null) return 0;3return 1 + countNodes(n.left) + countNodes(n.right);4}5static int height(TreeNode n) { // edges on the longest downward path; an empty tree is -16if (n == null) return -1;7return 1 + Math.max(height(n.left), height(n.right));8}9static int leaves(TreeNode n) {10if (n == null) return 0;11if (n.left == null && n.right == null) return 1;12return leaves(n.left) + leaves(n.right);13}14static int best;15static int diameterHeight(TreeNode n) { // fills best with the most edges on any path16if (n == null) return -1;17int l = diameterHeight(n.left), r = diameterHeight(n.right);18best = Math.max(best, l + r + 2); // the path that bends at n uses both subtrees19return 1 + Math.max(l, r);20}21static int balancedHeight(TreeNode n) { // -2 means "unbalanced somewhere below"22if (n == null) return -1;23int l = balancedHeight(n.left); if (l == -2) return -2;24int r = balancedHeight(n.right); if (r == -2) return -2;25if (Math.abs(l - r) > 1) return -2;26return 1 + Math.max(l, r);27}28static int bestPath;29static int gain(TreeNode n) { // best downward path sum starting at n30if (n == null) return 0;31int l = Math.max(0, gain(n.left)), r = Math.max(0, gain(n.right));32bestPath = Math.max(bestPath, n.val + l + r); // the path that bends at n33return n.val + Math.max(l, r);34}
Live state
quantity
Diameter
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L15 | quantity=Diameter | Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value. |
| 2 | L18 | quantity=Diameter best=0 | A path bending at 4 has -1 + -1 + 2 = 0 edges (heights -1 and -1). New best diameter. |
| 3 | L18 | quantity=Diameter best=0 | A path bending at 5 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 4 | L18 | quantity=Diameter best=2 | A path bending at 2 has 0 + 0 + 2 = 2 edges (heights 0 and 0). New best diameter. |
| 5 | L18 | quantity=Diameter best=2 | A path bending at 3 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 6 | L18 | quantity=Diameter best=3 | A path bending at 1 has 1 + 0 + 2 = 3 edges (heights 1 and 0). New best diameter. |
| 7 | L19 | result=3 | Diameter: 3 edges (the longest path between any two nodes). |
Sum and max path sum
The **sum** of all values is the same combine-the-children pattern. The **maximum path sum** is the diameter pattern with values instead of edge counts: a path can start and end at any nodes. For each node compute the **gain** it can pass up, value + max(0, best gain from left, best gain from right); a negative gain is dropped, since not extending the path is better. The candidate answer at the node is value + max(0, left gain) + max(0, right gain), a path bending there.
In the “Maximum path sum (negatives)” preset the tree is “-10 9 20 # # 15 7”. Node 20 passes up 20 + 15 = 35; at the root the candidate would be -10 + 9 + 35 = 34, but the best path is 15 → 20 → 7 with sum 42, which bends at node 20.
1static int bestPath;2static int maxPathSum(TreeNode root) {3bestPath = Integer.MIN_VALUE;4gain(root);5return bestPath;6}7static int gain(TreeNode n) { // best downward path sum starting at n8if (n == null) return 0;9int l = Math.max(0, gain(n.left)), r = Math.max(0, gain(n.right)); // drop negative gains10bestPath = Math.max(bestPath, n.val + l + r); // the path that bends at n11return n.val + Math.max(l, r);12}1314static int sum(TreeNode n) {15return n == null ? 0 : n.val + sum(n.left) + sum(n.right);16}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(h)
Computing node count, height, leaves, diameter, balance and path sum bottom-up
Interactive · edit the inputbinary tree · 5 nodes
Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value.
1static int countNodes(TreeNode n) {2if (n == null) return 0;3return 1 + countNodes(n.left) + countNodes(n.right);4}5static int height(TreeNode n) { // edges on the longest downward path; an empty tree is -16if (n == null) return -1;7return 1 + Math.max(height(n.left), height(n.right));8}9static int leaves(TreeNode n) {10if (n == null) return 0;11if (n.left == null && n.right == null) return 1;12return leaves(n.left) + leaves(n.right);13}14static int best;15static int diameterHeight(TreeNode n) { // fills best with the most edges on any path16if (n == null) return -1;17int l = diameterHeight(n.left), r = diameterHeight(n.right);18best = Math.max(best, l + r + 2); // the path that bends at n uses both subtrees19return 1 + Math.max(l, r);20}21static int balancedHeight(TreeNode n) { // -2 means "unbalanced somewhere below"22if (n == null) return -1;23int l = balancedHeight(n.left); if (l == -2) return -2;24int r = balancedHeight(n.right); if (r == -2) return -2;25if (Math.abs(l - r) > 1) return -2;26return 1 + Math.max(l, r);27}28static int bestPath;29static int gain(TreeNode n) { // best downward path sum starting at n30if (n == null) return 0;31int l = Math.max(0, gain(n.left)), r = Math.max(0, gain(n.right));32bestPath = Math.max(bestPath, n.val + l + r); // the path that bends at n33return n.val + Math.max(l, r);34}
Live state
quantity
Diameter
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L15 | quantity=Diameter | Diameter: every value is computed from the children's values, so the work happens after both subtrees return (post-order). A number under a node is its computed value. |
| 2 | L18 | quantity=Diameter best=0 | A path bending at 4 has -1 + -1 + 2 = 0 edges (heights -1 and -1). New best diameter. |
| 3 | L18 | quantity=Diameter best=0 | A path bending at 5 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 4 | L18 | quantity=Diameter best=2 | A path bending at 2 has 0 + 0 + 2 = 2 edges (heights 0 and 0). New best diameter. |
| 5 | L18 | quantity=Diameter best=2 | A path bending at 3 has -1 + -1 + 2 = 0 edges (heights -1 and -1). |
| 6 | L18 | quantity=Diameter best=3 | A path bending at 1 has 1 + 0 + 2 = 3 edges (heights 1 and 0). New best diameter. |
| 7 | L19 | result=3 | Diameter: 3 edges (the longest path between any two nodes). |
Practice ladder
Now try it yourself
Easy
- Maximum Depth of Binary Tree
- Diameter of Binary Tree
- Balanced Binary Tree
Medium
- Count Univalue Subtrees
- Binary Tree Maximum Path Sum
Advanced
- Binary Tree Cameras
- Longest Univalue Path
Key takeaways
- Most structural questions are postorder: combine the answers of the two subtrees.
- The diameter and the maximum path sum record a candidate that bends at each node, and return a one-sided value upward.
- Return a sentinel to stop early in the balance check, keeping it O(n).
- A node with one child is not a leaf.