Problem overview
The method isBalanced receives the root of a binary tree and returns true when the tree is height-balanced: at every node, the heights of the left and right subtrees differ by at most 1. The height of a tree is the number of nodes on its longest path from the top down to a leaf, and an empty tree has height 0 and counts as balanced. The lesson provides the TreeNode class, with an Int field value and the fields left and right, which are null when a child is missing.
In the tree [3, 9, 20, null, null, 15, 7] (level order, null for a missing child), the root's left subtree is the single node 9, of height 1, and its right subtree, 20 with children 15 and 7, has height 2. They differ by 1, and every other node is fine too, so the answer is true. In [1, 2, 2, 3, 3, null, null, 4, 4], the root's left subtree has height 3 and its right subtree height 1: they differ by 2, so the answer is false.
The rule must hold at every node, not just the root. In [1, 2, 2, 3, null, null, 3, 4, null, null, 4] the root is fine, because both sides have height 3. But the left 2 has a left subtree of height 2 and an empty right subtree of height 0, so the tree is not balanced.
The lesson asks for O(n) time, visiting each node once.
Approach: compute heights from the bottom and signal a failure with -1
The direct idea calls a height function on both children of every node and compares. That works, but height itself walks a whole subtree, and it is called again for every node above it. On a long chain of nodes that is about n² work.
The fix is to find each subtree's height once, from the bottom up, and to check the rule on the way back. A recursive helper height returns the height of the tree under a node when that tree is balanced, and -1 as soon as it finds a node that breaks the rule. A real height is never negative, so -1 can't be confused with one. It means "unbalanced somewhere in here", and once it appears, it travels up through every caller unchanged.
At each node the helper does the following:
- An empty tree has height 0.
- Ask for the left height. If it is -1, return -1 immediately.
- Ask for the right height. If it is -1, return -1.
- If the two heights differ by more than 1, this node breaks the rule: return -1.
- Otherwise return
1 + max(left, right): this node plus its taller side.
Then isBalanced only has to ask whether the answer for the root is not -1. Run on the three examples, height returns 3 for the first tree and -1 for the second and the third. In the first tree, height of the node 9 is 1 and height of the node 20 is 2.
Step by step
- Define what
isBalancedreturns. It callsheight(root)and answers whether the result is!= -1. The helper is written next. - Write the helper's signature.
heighttakes aTreeNodenamednodeand returns anInt. - Handle the empty tree. If
node == null, return 0. This is also what stops the recursion when it steps past a leaf. - Get the left height with a recursive call on
node.left, stored in avalnamedleftHeight. Trust the recursion: it returns the left subtree's height, or -1. - Pass the failure up. If
leftHeight == -1, return -1 at once, without looking at the right side. - Get the right height the same way, as
rightHeight. - Pass the failure up again. Do not skip this check: a -1 from the right would otherwise be treated as a height, and it differs from an empty side's 0 by only 1.
- Check this node. If
math.abs(leftHeight - rightHeight) > 1, return -1. This line runs for every node on its way back up, so the rule is checked everywhere. - Return the height of the tree under the node: 1 for the node itself plus
math.max(leftHeight, rightHeight).
The solution in Scala
class Solution {
def height(node: TreeNode): Int = {
if (node == null) {
return 0
}
val leftHeight = height(node.left)
if (leftHeight == -1) {
return -1
}
val rightHeight = height(node.right)
if (rightHeight == -1) {
return -1
}
if (math.abs(leftHeight - rightHeight) > 1) {
return -1
}
return 1 + math.max(leftHeight, rightHeight)
}
def isBalanced(root: TreeNode): Boolean = {
return height(root) != -1
}
}
The Scala details worth a closer look:
- Recursive methods need a return type.
heightdeclares: Intexplicitly. Scala can infer most return types, but not for a method that calls itself. nullin Scala. The lesson'sTreeNodeusesnullfor a missing child, sonode == nullis the base case.==compares values, and againstnullit is safe: it never throws.returnin a method body. Each early exit is a plainreturn. This is allowed anywhere in a method, and it is not inside a closure here, so Scala 3.5 doesn't warn. The last expression would work as well without the keyword.math.absandmath.max. They come from thescala.mathpackage object, available without an import.math.absdrops the sign, so it doesn't matter which side is taller.- A helper method in the class.
heightis a normal method ofSolution. It's called without any prefix, and its position above or belowisBalancedmakes no difference. - Recursion depth. Each level of the tree adds one stack frame. The lesson limits trees to 800 levels, which is well within what the JVM allows by default. A chain of 800 nodes is handled without trouble.
Complexity
Let n be the number of nodes and h the height of the tree.
Time: O(n). Each node is visited once, and the work at a node is a few comparisons. The early returns for -1 only make it faster on unbalanced trees.
Space: O(h). The only extra memory is the recursion stack, one frame per level. That is about log n for a balanced tree and up to n for a chain.
Edge cases and common mistakes
- An empty tree.
nullreturnstrue:height(null)is 0, which is not -1. - A single node. Both sides have height 0, so the tree is balanced.
- Chains. A root with one child is balanced, with heights 1 and 0. A chain of three nodes is not: its top node sees heights 2 and 0.
- Checking only the root. The third example has a root with equal subtree heights, yet the tree is not balanced. The check inside
heightruns at every node. - Not handling -1 from the right. Without the
rightHeight == -1check, a failure on the right side could be mistaken for a height and give a wrongtrue. - Measuring height again from every node. That also gives the right answer, but it repeats work on every node above, which is the slowdown this approach avoids.
Testing
[3, 9, 20, null, null, 15, 7]givestrue.[1, 2, 2, 3, 3, null, null, 4, 4]givesfalse.[1, 2, 2, 3, null, null, 3, 4, null, null, 4]givesfalse.- An empty tree (
null) givestrue. [1]givestrue.[1, 2]givestrue.[1, 2, null, 3](a chain of three) givesfalse.
