Skip to content

Chapter 6: Binary Trees: Infinity in the Palm of your Hand

George Heineman edited this page Jul 25, 2021 · 4 revisions

The following are the code listings for Chapter 6. [book.py]


Listing 6-1. Recursive and iterative functions to sum the values in a linked list

class Node:
  def __init__(self, val, rest=None):
    self.value = val
    self.next = rest

def sum_iterative(n):
  total = 0while n:
    total += n.valuen = n.nextreturn total

def sum_list(n):
  if n is None:                     ❹
    return 0
  return n.value + sum_list(n.next) ❺

❶ Initialize total to 0 to prepare for computation.
❷ For each node, n, in linked list, add its value to total.
❸ Advance to the next node in the linked list.
❹ Base case: the sum of a nonexistent list is 0.
❺ Recursive case: the sum of a linked list, n, is the sum of its value added to the sum of the rest of the list.

Listing 6-2. Expression data structure to represent mathematical expressions

class Value:                                    ❶
  def __init__(self, e):
    self.value = e

  def __str__(self):
    return str(self.value)

  def eval(self):
    return self.value

class Expression:                               ❷
  def __init__(self, func, left, right):
    self.func  = func
    self.left  = left
    self.right = right

  def __str__(self):                            ❸
    return '({} {} {})'.format(self.left, self.func.__doc__, self.right)

  def eval(self):                               ❹
    return self.func(self.left.eval(), self.right.eval())

def add(left, right):                           ❺
  """+"""
  return left + right

❶ A Value stores a numeric value. It can return its value and string representation.
❷ An Expression stores a function, func, and left and right sub-expressions.
❸ Provides built-in __str()__ method to recursively produce strings with parentheses around expressions.
❹ Evaluate an Expression by evaluating left and right children and passing those values to func.
❺ Function to perform addition; mult() for multiplication is similar. The docString __doc__ for the function contains the operator symbol.

Listing 6-3. Structure of a binary search tree

class BinaryNode:
  def __init__(self, val):
    self.value = valself.left  = Noneself.right = None

❶ Each node stores a value.
❷ Each node's left subtree, if it exists, contains values ≤ value.
❸ Each node's right subtree, if it exists, contains values ≥ value.

Listing 6-4. BinaryTree class to improve usability of binary search tree

class BinaryTree:
  def __init__(self):
    self.root = Nonedef insert(self, val):                         ❷ 
    self.root = self._insert(self.root, val)

  def _insert(self, node, val):
    if node is None:
      return BinaryNode(val)                     ❸

    if val <= node.value:                        ❹
      node.left = self._insert(node.left, val)
    else:                                        ❺
      node.right = self._insert(node.right, val)
    return node

self.root is the root node of the BinaryTree (or None if empty).
❷ Use _insert() helper function to insert val into tree rooted at self.root.
❸ Base case: to add val to an empty subtree, return a new BinaryNode.
❹ If val is smaller than or equal to node's value, set node.left to be the subtree that results when inserting val into subtree node.left.
❺ If val is larger than node value, set node.right to be the subtree that results when inserting val into subtree node.right.
❻ This method must return node to uphold its contract that it returns the root of the subtree into which val was inserted.

Listing 6-5. Determining whether a BinaryTree contains a value

class BinaryTree:
  def __contains__(self, target):
    node = self.rootwhile node:
      if target == node.value:       ❷
        return True

      if target < node.value:        ❸
        node = node.left
      else:
        node = node.rightreturn False

❶ Start the search at the root.
❷ If target value is same as node's value, return True for success.
❸ If target is smaller than node's value, set node to its left subtree to continue search in that subtree.
❹ If target had been larger than node's value, continue search in right subtree.
❺ If the search runs out of nodes to inspect, the value does not exist in the tree, so return False.

Listing 6-6. Removing minimum value

def _remove_min(self, node):
  if node.left is None:                    ❶
    return node.right

  node.left = self._remove_min(node.left)  ❷
  return node

❶ Base case: if node has no left subtree, then it is the smallest value in the subtree rooted at node; to remove it, just "lift up" and return its right subtree (which could be None).
❷ Recursive case: remove the minimum value from left subtree, and the returned subtree becomes new left subtree for node.
_remove_min() completes the recursive case by returning the node whose left subtree may have been updated.

Listing 6-7. Removing a value from a BinaryTree

def remove(self, val):
  self.root = self._remove(self.root, val)         ❶

def _remove(self, node, val):
  if node is None: return Noneif val < node.value:
    node.left = self._remove(node.left, val)       ❸
  elif val > node.value:
    node.right = self._remove(node.right, val)     ❹
  else:                                            ❺
    if node.left is None:  return node.right
    if node.right is None: return node.leftoriginal = nodenode = node.right
    while node.left:                               ❽
      node = node.left

    node.right = self._remove_min(original.right)  ❾
    node.left = original.leftreturn node

❶ Use _remove() helper function to remove val from tree rooted at self.root.
❷ Base case: attempting to remove val from nonexistent tree returns None.
❸ Recursive case #1: if value to be removed is smaller than node.value, set node.left to be the subtree that results from removing val from node.left.
❹ Recursive case #2: if value to be removed is larger than node.value, set node.right to be the subtree that results from removing val from node.right.
❺ Recursive case #3: it may be that node is root of subtree and contains value to be removed, so there's work to be done.
❻ Handle easy cases first. If node is a leaf, then None is returned. If it has just one child, then return that child node.
❼ Remember original reference to node, since we don't want to lose track of node's original left and right subtrees, both of which must exist.
❽ Start with node = node.right to find the smallest value in the subtree rooted at node.right: as long as node has a left subtree, then it does not contain the smallest value, so iteratively locate the node with no left subtree--this is the smallest value in the right subtree of original.
node will become the new root to the left and right children of original. Here I set node.right to the subtree that results from removing the minimum value from original.right. You might notice that this recursive method essentially repeats the process of the while loop, but this code is much easier to understand than trying to do everything in just one pass.
❿ Stitch the subtree rooted at node back together.

Listing 6-8. Generator that iterates over values in binary search tree in ascending order

class BinaryTree:

  def __iter__(self):
    for v in self._inorder(self.root):      ❶
      yield v

  def _inorder(self, node):
    if node is None:                        ❷
      return

    for v in self._inorder(node.left):      ❸
      yield v

    yield node.valuefor v in self._inorder(node.right):     #<5>
      yield v

❶ Yield all values that result from the in order traversal of binary search tree rooted at self.root.
❷ Base case: nothing to generate for a nonexistent subtree.
❸ To generate all values in order, first generate all values in order from the subtree rooted at node.left.
❹ Now it is node's turn to yield its value.
❺ Finally, generate all values in order from the subtree rooted at node.right.

Listing 6-9. Structure of AVL binary node

class BinaryNode:
  def __init__(self, val):
    self.value = valself.left  = None
    self.right = None
    self.height = 0def height_difference(self):                              ❸
    left_height = self.left.height if self.left else -1right_height = self.right.height if self.right else -1
    return left_height - right_heightdef compute_height(self):                                 ❻
    left_height = self.left.height if self.left else -1
    right_height = self.right.height if self.right else -1
    self.height = 1 + max(left_height, right_height)

❶ Structure of a BinaryNode is essentially the same as a binary search tree.
❷ Record the height for each BinaryNode.
❸ Helper function that computes the height difference between left and right subtree.
❹ Set left_height to +++—1+++ for nonexistent left subtree, or its proper height.
❺ Return height difference, which must be left_height subtracting right_height.
❻ Helper function that updates the height for a node assuming that the height of its respective left and right subtrees (if they exist) have accurate height values.

Listing 6-10. Modify _insert() to compute height properly

  def _insert(self, node, val):
    if node is None:
      return BinaryNode(val)                    ❶

    if val <= node.value:
      node.left = self._insert(node.left, val)
    else:
      node.right = self._insert(node.right, val)

    node.compute_height()                       ❷
    return node

❶ For the base case, when a newly created leaf node is returned, its height is already 0 by default.
❷ When the recursive case completes, val has been inserted into either node.left or node.right. This means the height for node needs to be recomputed.

Listing 6-11. Helper functions that choose appropriate rotation strategy

  def resolve_left_leaning(node):              ❶
    if node.height_difference() == 2:
      if node.left.height_difference() >= 0:   ❷
        node = rotate_right(node)
      else:
        node = rotate_left_right(node)         ❸
    return node                                #<7>

  def resolve_right_leaning(node):
    if node.height_difference() == -2:         ❹
      if node.right.height_difference() <= 0:  ❺
        node = rotate_left(node)
      else:
        node = rotate_right_left(node)         ❻
    return node                                #<7>

❶ A node leans to the left when height difference is +2.
❷ Detects the rotate_right case by confirming that node's left subtree is partially leaning left.
❸ Otherwise, node's left subtree is partially leaning right, meaning a rotate_left_right is in order.
❹ A node leans to the right when height difference is –2.
❺ Detects the rotate_left case by confirming that node's right subtree is partially leaning right.
❻ Otherwise, node's right subtree is partially leaning left, meaning a rotate_right_left is in order.
❼ Be sure to remember to return node of (potentially rebalanced) subtree.

Listing 6-12. Rotating nodes when an unbalanced node is detected

def _insert(self, node, val):
    if node is None:
      return BinaryNode(val)

    if val <= node.value:
      node.left = self._insert(node.left, val)
      node = resolve_left_leaning(node)           ❶
    else:
      node.right = self._insert(node.right, val)
      node = resolve_right_leaning(node)          ❷

    node.compute_height()
    return node

❶ If left subtree is now left-leaning, resolve it.
❷ If right subtree is now right-leaning, resolve it.

Listing 6-13. Updating _remove() to maintain AVL property

  def _remove_min(self, node):
    if node.left is None: return node.right

    node.left = self._remove_min(node.left)
    node = resolve_right_leaning(node)             ❶
    node.compute_height()
    return node

  def _remove(self, node, val):
    if node is None: return None

    if val < node.value:
      node.left = self._remove(node.left, val)
      node = resolve_right_leaning(node)           ❷
    elif val > node.value:
      node.right = self._remove(node.right, val)
      node = resolve_left_leaning(node)            ❸
    else:
      if node.left is None:  return node.right
      if node.right is None: return node.left

      original = node
      node = node.right
      while node.left:
        node = node.left

      node.right = self._remove_min(original.right)
      node.left = original.left
      node = resolve_left_leaning(node)            ❹

    node.compute_height()
    return node

❶ Removing the minimum value from a subtree rooted at node.left could make node right-leaning; rotate to rebalance as needed.
❷ Removing a value from the left subtree of node could make node right-leaning; rotate to rebalance as needed.
❸ Removing a value from the right subtree of node could make node left-leaning; rotate to rebalance as needed.
❹ After the minimum has been removed from the subtree returned to be node.right, node could be left-leaning; rotate to rebalance as needed.

Listing 6-14. Updated BinaryNode when using binary tree to store symbol table

class BinaryNode:
  def __init__(self, k, v):
    self.key = kself.value = vself.left = None
    self.right = None
    self.height = 0

❶ The key is used to navigate the binary search tree.
❷ The value contains arbitrary data that is irrelevant to the operation of the binary search tree.

Listing 6-15. Updated BinaryNode when using binary tree to store priority queue

class BinaryNode:
  def __init__(self, v, p):
    self.value = vself.priority = pself.left  = None
    self.right = None
    self.height = 0

❶ The value contains arbitrary data that is irrelevant to the operation of the binary search tree.
❷ The priority is used to navigate the binary search tree.

Listing 6-16. PQ class provides enqueue() and dequeue() functions

class PQ:
  def __init__(self):
    self.tree = BinaryTree()                                  ❶
    self.N = 0

  def __len__(self):
    return self.N

  def is_empty(self):
    return self.N == 0

  def is_full(self):
    return False

  def enqueue(self, v, p):
    self.tree.insert(v, p)                                    ❷
    self.N += 1

  def _remove_max(self, node):                                ❸
    if node.right is None:
      return (node.value, node.left)                          ❹

    (value, node.right) = self._remove_max(node.right)        ❺
    node = resolve_left_leaning(node)                         ❻
    node.compute_height()                                     ❼
    return (value, node)

  def dequeue(self):                                          ❽
    (value, self.tree.root) = self._remove_max(self.tree.root)
    self.N -= 1
    return value

❶ Use a balanced binary search tree for storage.
❷ To enqueue a (v, p) pair, insert that pair into the binary search tree and increment N count.
❸ The _remove_max() helper method both removes the node with maximum priority from the subtree rooted at node and returns its value and the node of the resulting subtree as a tuple.
❹ Base case: with no right subtree, this node has maximum priority; return both the value in the node being deleted and the left subtree that will eventually take its place.
❺ Recursive case: retrieve removed value and root of updated subtree.
❻ If node is out of balance (it could now lean left), fix with rotations.
❼ Compute node height before returning it along with value that was removed.
❽ The dequeue() method removes node with maximum priority from the binary search tree and returns its value.
❾ After decrementing count, N, return the value that had been associated with highest priority.

Listing 6-17. Enhance the _insert() method to return description of what happened

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

class SpeakingBinaryTree:
  def __init__(self):
    self.root = None

  def insert(self, val):
    (self.root,explanation) = self._insert(self.root, val,
         'To insert `{}`, '.format(val))
    return explanation

  def _insert(self, node, val, sofar):
    """
    Return (node,explanation) resulting from inserting val into subtree
    rooted at node.
    """
Clone this wiki locally