Tree patterns

Most tree questions are solved by one of two walks. (The Python course's binary-trees lesson builds the tree used here.)

Depth-first: ask the children

Many answers about a tree come from the answers about its two subtrees. Write the function as if it already works for the children, then combine:

def depth(node):
    if node is None:          # the base case: an empty tree
        return 0
    return 1 + max(depth(node.left), depth(node.right))

The same shape gives the number of nodes, the sum of the values, whether two trees are equal, and whether a tree is balanced. The only questions are: what is the answer for an empty tree, and how do the children's answers combine?

Breadth-first: level by level

To visit a tree level by level (the level-order traversal, the rightmost node of each level, the minimum depth), use a queue. Taking exactly len(queue) nodes in each round processes one whole level:

from collections import deque

class Node:
    def __init__(self, value, left=None, right=None):
        self.value, self.left, self.right = value, left, right

def levels(root):
    result = []
    queue = deque([root])
    while queue:
        level = []
        for _ in range(len(queue)):        # exactly the nodes of this level
            node = queue.popleft()
            level.append(node.value)
            for child in (node.left, node.right):
                if child:
                    queue.append(child)
        result.append(level)
    return result

tree = Node(8, Node(3, Node(1), Node(6)), Node(10, None, Node(14)))
print(levels(tree))
Open in compiler

Validating a binary search tree

In a binary search tree (BST), every value in the left subtree is smaller than the node, and every value in the right subtree is bigger. A classic wrong answer only checks each node against its two children. The value must fit between bounds inherited from all its ancestors:

class Node:
    def __init__(self, value, left=None, right=None):
        self.value, self.left, self.right = value, left, right

def is_bst(node, low=float("-inf"), high=float("inf")):
    if node is None:
        return True
    if not low < node.value < high:
        return False
    return is_bst(node.left, low, node.value) and is_bst(node.right, node.value, high)

good = Node(5, Node(3, Node(2), Node(4)), Node(8))
bad = Node(5, Node(3, Node(2), Node(6)), Node(8))     # 6 is left of 5
print(is_bst(good), is_bst(bad))
Open in compiler

In the second tree, 6 is a correct right child of 3 (it is bigger than 3), so comparing each node with its children finds nothing wrong. But 6 is in the left subtree of 5, so it must be smaller than 5. Only the bounds catch it.

Another way: an in-order walk (left, node, right) of a BST visits the values in increasing order, so check that each value is bigger than the previous one.

The lowest common ancestor

The lowest common ancestor (LCA) of two nodes is the deepest node that has both below it. In a BST it is easy: start at the root; if both values are smaller, go left; if both are bigger, go right; otherwise this node splits them, and it is the LCA. In a general binary tree, recurse: a node is the LCA if one target is found in each subtree (or the node is one of the targets and the other is below it).

Quick check

1. What should depth(None) return?
2. Which walk gives the nodes level by level?
3. Why is checking each node against its two children not enough to validate a BST?

Practice problems for this lesson

My notes

Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.