Start learning free

LESSON 436 OF 574 · BINARY TREES

Balanced Binary Tree in Swift: O(n) with a Bottom-Up Height Check

Practice O(n) Read the full explanation

Given the root of a binary tree, return true if it is height-balanced, and false otherwise.

A binary tree is height-balanced when, at every node, the heights of its 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. An empty tree has a height of 0, and it is balanced.

The trees below are written in level order (top to bottom, left to right), with nil for a missing child.

 

Example 1:

    3
   / \
  9   20
     /  \
    15   7

Input: root = [3,9,20,nil,nil,15,7]
Output: true
Explanation: At the root, the left subtree (9) has a height of 1 and the right subtree (20, 15, 7) a height of 2: they differ by 1. At 20, both subtrees have a height of 1, and the leaves have two empty subtrees.

Example 2:

          1
         / \
        2   2
       / \
      3   3
     / \
    4   4

Input: root = [1,2,2,3,3,nil,nil,4,4]
Output: false
Explanation: At the root, the left subtree has a height of 3 and the right subtree a height of 1: they differ by 2.

Example 3:

          1
         / \
        2   2
       /     \
      3       3
     /         \
    4           4

Input: root = [1,2,2,3,nil,nil,3,4,nil,nil,4]
Output: false
Explanation: The root is fine: both of its subtrees have a height of 3. But the left 2 has a left subtree of height 2 (3 and 4) and an empty right subtree, of height 0: they differ by 2. The rule must hold at every node, not only at the root.

 

Constraints:

  • The number of nodes in the tree is in the range [0, 5000].
  • Each node value is between -104 and 104.
  • The tree is at most 800 levels deep.

Note:

Your algorithm should run in O(n) time, where n is the number of nodes, visiting each node once.

Measuring a subtree's height again for every node above it repeats work. Find each subtree's height once, from the bottom up, and check the rule at the same time.

STEP 1 OF 9
    Ctrl + Enter to check

    SWIFT · BINARY TREES

    Balanced Binary Tree in Swift, explained

    Problem overview

    The method isBalanced receives the root of a binary tree, a TreeNode?, 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. An empty tree has a height of 0 and is balanced. The lesson provides the TreeNode class, with an integer val and the optional children left and right.

    In the first example, [3, 9, 20, nil, nil, 15, 7], the left subtree of the root (just the 9) has height 1 and the right subtree (20 with 15 and 7) has height 2. They differ by 1, and the other nodes are fine too, so the answer is true. In the second, [1, 2, 2, 3, 3, nil, nil, 4, 4], the root's left subtree has height 3 and its right subtree height 1, which differ by 2, so the answer is false. The third, [1, 2, 2, 3, nil, nil, 3, 4, nil, nil, 4], is the subtle one. The root is fine, because both of its subtrees have height 3. But the left 2 has a left subtree of height 2 and an empty right subtree of height 0, which differ by 2. The rule must hold at every node, not only at the root.

    The statement asks for O(n) time, where n is the number of nodes: each node is visited once.

    Approach: compute heights from the bottom up, and check as you go

    The obvious plan is to write a function that measures a tree's height, then at every node compare the heights of its two subtrees. That repeats work: the height of a deep subtree is measured again for every node above it. On a tree shaped like a long chain it adds up to quadratic time.

    The fix is to measure each height once, from the bottom up, and test the rule at the same moment. A recursive helper, height, returns the height of the tree under a node, with one extra meaning: it returns -1 as soon as it finds a node that breaks the rule. A real height is never negative, so -1 can't be mistaken for one. It is a signal that means "unbalanced somewhere in here".

    For a node, the helper does this:

    1. An empty tree has height 0.
    2. Get the height of the left subtree. If it is -1, pass the signal up at once.
    3. Do the same for the right subtree.
    4. If the two heights differ by more than 1, this node breaks the rule: return -1.
    5. Otherwise, return 1 for the node itself plus the larger of the two heights.

    The method then returns whether the height of the root is not -1.

    On the examples, the helper gives these values: for the first tree, the whole tree has height 3, its left child 1 and its right child 2. For the second tree, the left 2 has height 3 and the right 2 has height 1, so the root returns -1. For the third, the left 2 returns -1 because its children have heights 2 and 0, so the root returns -1 too, although its own two sides look equal in size.

    Step by step

    1. Return from isBalanced. The method returns whether height(root) != -1.
    2. Write the helper's signature. height takes one parameter without an argument label, _ node: TreeNode?, and returns an Int. It goes above isBalanced in the class.
    3. Handle the empty tree. guard let node = node else { return 0 } unwraps the optional, and an empty tree has height 0. This also ends the recursion when it steps past a leaf. Below the guard, node stands for the unwrapped node.
    4. Find the left height with a recursive call on node.left, into the constant leftHeight.
    5. Pass the signal up. If leftHeight == -1, return -1 without looking at the right side. When part of the tree is unbalanced, the whole tree is.
    6. Find the right height the same way, into rightHeight. A missing child gives 0, thanks to the base case.
    7. Pass the signal up again. If rightHeight == -1, return -1. Skipping this check is a trap: -1 would pass for a height, and it differs from an empty side's 0 by only 1.
    8. Check this node. If abs(leftHeight - rightHeight) > 1, return -1. Every node runs this on its way back up, so the rule is checked at every node, not only at the root. abs makes the order of the two sides irrelevant.
    9. Return the height: 1 + max(leftHeight, rightHeight), the node plus its taller side.

    The solution in Swift

    class Solution {
        func height(_ node: TreeNode?) -> Int {
            guard let node = node else {
                return 0
            }
            let leftHeight = height(node.left)
            if leftHeight == -1 {
                return -1
            }
            let rightHeight = height(node.right)
            if rightHeight == -1 {
                return -1
            }
            if abs(leftHeight - rightHeight) > 1 {
                return -1
            }
            return 1 + max(leftHeight, rightHeight)
        }
        func isBalanced(_ root: TreeNode?) -> Bool {
            return height(root) != -1
        }
    }
    

    The Swift details worth knowing:

    • Optional children. left and right are TreeNode?, so a missing child is nil. The helper takes a TreeNode? as well, so it can be called on node.left without unwrapping.
    • guard let node = node. It declares a new constant named node that shadows the parameter and holds the unwrapped value. When the optional is nil, the else block runs and must leave the method. After the guard, node is a plain TreeNode, and its fields are used without !.
    • Methods calling each other. height calls itself, and isBalanced calls height, with no self. in front. Inside a class's method, Swift finds the other methods of the class by name. The order in which the methods are written doesn't matter either.
    • abs and max are global functions: abs(-3) is 3 and max(2, 5) is 5.
    • Recursion depth. The statement promises at most 800 levels, which recursion handles easily. A chain of 800 left children is rejected: its height is -1, and isBalanced returns false.

    Complexity

    Time: O(n). Each node is visited once, and the work at a node is a few comparisons plus the two recursive calls. Each height is found once. The early returns of -1 only make it faster.

    Space: O(h), where h is the height of the tree. The recursion keeps one call per level on the stack. For a balanced tree that is about log n, and in the worst case it is n.

    Edge cases and common mistakes

    • Empty tree. nil has height 0, so isBalanced(nil) returns true.
    • Single node and a root with one child. [1] is balanced, and so is [1, 2]: the heights are 0 and 1. Adding a grandchild on the same side, [1, 2, nil, 3], makes the heights 2 and 0, and the answer is false.
    • Checking only the root. The third example has equal-looking sides at the root and is still unbalanced. The rule applies to every node.
    • Forgetting to test for -1. Without the two -1 checks, a signal from below would be treated as a height. A subtree that returns -1 next to an empty side's 0 differs by only 1, so the node above would accept it: without the checks, [1, 2, nil, 3, nil, 4] gives true, although the 2 has subtrees of height 2 and 0.
    • Measuring height separately at each node. It works, but the same subtree is measured again and again, which can take quadratic time on a long chain.
    • Counting edges instead of nodes. The height here counts nodes, so a single node has height 1 and an empty tree 0. The base case is what ties this together.

    Testing

    • [3, 9, 20, nil, nil, 15, 7] gives true.
    • [1, 2, 2, 3, 3, nil, nil, 4, 4] gives false.
    • [1, 2, 2, 3, nil, nil, 3, 4, nil, nil, 4] gives false.
    • An empty tree (nil) gives true.
    • [1] gives true, and [1, 2] gives true.
    • [1, 2, nil, 3] gives false.
    Keep going with Swift500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free