Python Arena
Step 1 of 6

Binary trees and recursion

You built binary trees in the recursion module: a Node class with a value and two children, left and right, where None means "no child here", and recursive functions that counted the nodes, added up the values and measured the depth of a tree. This module takes trees on to what interviews ask about them. This step asks what those functions cost and where they break, with a short look at the linked list, a chain of nodes with one link each. The next ones add the order of a search tree, other ways to walk a tree, heaps and tries.

Here are the three functions again, with the names interview questions usually give them: size is the recursion module's count_nodes, and height is its depth.

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


def size(node):
    if node is None:
        return 0
    return 1 + size(node.left) + size(node.right)


def tree_sum(node):
    if node is None:
        return 0
    return node.value + tree_sum(node.left) + tree_sum(node.right)


def height(node):
    if node is None:
        return 0
    return 1 + max(height(node.left), height(node.right))


#        4
#       / \
#      9   2
#         / \
#        7   5
root = Node(4, Node(9), Node(2, Node(7), Node(5)))
print(size(root), tree_sum(root), height(root))
Output
5 27 3

Read None as the empty tree. Then a tree is either empty or a node with two smaller trees, its subtrees, and that hands you the shape of almost every tree function:

  1. The base case: the empty tree, None. Its answer is usually 0, True or an empty list.
  2. Ask the same question of the left subtree and of the right subtree.
  3. Combine the two answers with the node itself.

Trust the recursion. You don't need to follow every call to know that size is right: if it gives the right answers for the two subtrees, then 1 plus those answers is right for the whole tree. And the calls do end, because each one goes a level down, and the levels run out at None.

Here the height is the number of nodes on the longest path from the root down to a leaf: 3, for the path 4, 2, 7. (Some books count the links between the nodes instead, which gives one less. Interview questions often call this height the "maximum depth".)

A leaf is not enough of a base case

The recursion stops at the leaves, so it's tempting to make the leaf the base case:

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


def height(node):
    if node.left is None and node.right is None:
        return 1
    return 1 + max(height(node.left), height(node.right))


print(height(Node(4, Node(9), Node(2, Node(7), Node(5)))))
print(height(Node(4, Node(9, Node(1)))))
Output
3
Python stops with
Traceback (most recent call last):
  File "main.py", line 15, in <module>
    print(height(Node(4, Node(9, Node(1)))))
          ~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "main.py", line 11, in height
    return 1 + max(height(node.left), height(node.right))
                   ~~~~~~^^^^^^^^^^^
  File "main.py", line 11, in height
    return 1 + max(height(node.left), height(node.right))
                                      ~~~~~~^^^^^^^^^^^^
  File "main.py", line 9, in height
    if node.left is None and node.right is None:
       ^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'left'

The first tree works, because every node in it has two children or none. In the second, 4 and 9 each have a left child only. Neither is a leaf, so height calls itself for both children of each, and the call for the missing right child of 9 gets None, which has no left. Interview tests like a node with one child for exactly this reason. Checking each child before its call (if node.left is not None:) works, but it needs a check in front of every call and still fails on an empty tree. Handle None once, at the top, and the function works for leaves, for nodes with one child and for the empty tree alike.

What it costs

Each function makes one call for every node and one for every None below a node, and does O(1) work in each: O(n) time for n nodes. The memory goes to the calls that are waiting for an answer, which are the nodes on the path from the root to the current node: O(h) extra memory, where h is the height. A balanced tree, in which the two subtrees of every node have about the same height, has a height of about log₂ n: 20 levels hold a million nodes. A tree in which every node has one child is a chain with h = n, and there the waiting calls run into a limit:

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


def height(node):
    if node is None:
        return 0
    return 1 + max(height(node.left), height(node.right))


chain = None
for value in range(2000):
    chain = Node(value, chain)
print(height(chain))
Python stops with
Traceback (most recent call last):
  File "main.py", line 17, in <module>
    print(height(chain))
          ~~~~~~^^^^^^^
  File "main.py", line 11, in height
    return 1 + max(height(node.left), height(node.right))
                   ~~~~~~^^^^^^^^^^^
  File "main.py", line 11, in height
    return 1 + max(height(node.left), height(node.right))
                   ~~~~~~^^^^^^^^^^^
  File "main.py", line 11, in height
    return 1 + max(height(node.left), height(node.right))
                   ~~~~~~^^^^^^^^^^^
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded

The loop builds the chain from the bottom up, each new node taking the chain so far as its left child: 2,000 levels, and no recursion. But height keeps a call waiting for every level, and Python stops at about 1000 waiting calls with a RecursionError. So recursion is safe on balanced trees, which are never deep, and on small trees. Interviewers like to ask about this, and the next steps show how to walk a deep tree with a loop and a stack or a queue of your own.

Linked lists

A chain is a list in disguise. Give each node a single link, next, and you have a linked list, an interview favorite. The list is known by its first node, the head, and the last node's next is None. A linked list adds a node at its front in O(1), where a list's insert(0, x) is O(n), but reaching the k-th node takes k steps: there's no head[k]. Its classic questions are loops that follow the links, each O(n) time and O(1) memory:

class ListNode:
    def __init__(self, value, next_node=None):
        self.value = value
        self.next = next_node


def reverse(head):
    prev = None
    node = head
    while node is not None:
        nxt = node.next
        node.next = prev
        prev = node
        node = nxt
    return prev


def middle(head):
    slow = head
    fast = head
    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next
    return slow


def has_cycle(head):
    slow = head
    fast = head
    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            return True
    return False


head = None
for value in [5, 4, 3, 2, 1]:
    head = ListNode(value, head)
head = reverse(head)
print(head.value, head.next.value, middle(head).value, middle(None))
print(has_cycle(head))
loop = ListNode(1, ListNode(2, ListNode(3)))
loop.next.next.next = loop.next
print(has_cycle(loop))
Output
5 4 3 None
False
True

The loop builds 1, 2, 3, 4, 5 from the back, each new node in front of the old head, and reverse turns it into 5, 4, 3, 2, 1. Its nxt saves the rest of the list before node.next changes, and prev holds the part already reversed. middle and has_cycle use slow and fast pointers: slow moves one node at a time and fast two, so when fast reaches the end, slow is at the middle. (middle returns the node, so an empty list gives None.) loop goes 1, 2, 3, 2, 3 and on forever, so there fast never reaches an end: it goes round the loop and gains a node on slow at each step until they meet. slow is fast asks whether both are the same node, not whether their values are equal.

Your task

Challenge 1 of 3

A tree is height-balanced when, at every node, the heights of the two subtrees differ by at most 1. Calling height for the two subtrees of every node works, but it walks the nodes below each node again for every node above them: O(n log n) on a balanced tree. checked_height does it all in one walk, O(n). It returns the height of the tree, or -1 when some node in it isn't balanced, and a -1 from either subtree passes straight up. Fill in the blanks: when a node isn't balanced, and the height of a node that is. The program should print:

True 3
False False
False False
True 3

Challenge 2

Two trees are the same when they have the same shape and the same values in the same places. Write same_tree(a, b) that returns True when the trees a and b are the same, and False otherwise. Either tree can be empty (None).

For example, this program:

first = Node(1, Node(2), Node(3))
second = Node(1, Node(2), Node(3))
print(same_tree(first, second))
print(same_tree(first, Node(1, Node(3), Node(2))))

prints:

True
False

Challenge 3

A root-to-leaf path starts at the root and goes down until it ends at a leaf. Write has_path_sum(node, target) that returns True when the values on some root-to-leaf path add up to target, and False otherwise. An empty tree has no paths at all.

For example, in this tree the root-to-leaf paths add up to 6 + 3 + 1 = 10, 6 + 3 + 4 = 13 and 6 + 9 + 2 = 17. 6 + 3 = 9 doesn't count, since the path stops at 3, which isn't a leaf. So this program:

#        6
#       / \
#      3   9
#     / \   \
#    1   4   2
root = Node(6, Node(3, Node(1), Node(4)), Node(9, None, Node(2)))
print(has_path_sum(root, 13))
print(has_path_sum(root, 9), has_path_sum(root, 15))

prints:

True
False False