Traversing Binary Trees

We take another look at binary trees, now from an object oriented perspective.

Binary Trees as Objects

Recall the application of sorting a sequence of numbers with a binary tree, introduced with an example in Fig. 35. The numbers are sorted if we traverse the tree in inorder.

In the class Node below we define the left and right branches via protected attributes, indicated by the underscore _ at the start of the attribute name. Protected attributes are only used by methods of the class.

class Node(object):
    """
    Defines a node in a binary tree.
    """
    def __init__(self, data, *children):
        """
        Returns a node with data.
        Children, left and right, are optional.
        """
        self._data = data  # protected attribute
        if len(children) == 0:
            self._left = None
            self._right = None
        else:
            self._left = children[0]
            self._right = children[1]

The * in the front of *children means that children are optional. Consider the string representation.

def __str__(self):
    """
    Data at node represented as string.
    """
    return str(self._data)

To make the value at the node accessible, we define the method value().

def value(self):
    """
    Returns the data at the node.
    """
    return self._data

Because we work with protected objects, for a node nd, instead of nd.\_data, we do nd.value().

If the definition of class Node is in the file classnode.py, then we may use it in an interactive Python shell as

>>> from classtree import Node
>>> nd = Node(2017)
>>> nd
<classtree.Node instance at 0x6a670>
>>> nd.value()
2017
>>> str(nd)
'2017'

Notice the difference between the value and the string representation of the data.

Because the left and right of a node are protected attributes, we also define methods to get the left and right branch of a node.

def left(self):
    """
    Returns the node at the left.
    """
    return self._left

def right(self):
    """
    Returns the node at the right.
    """
    return self._right

Inserting an item to a node is defined recursively by the method insert() listed below.

def insert(self, item):
    """
    Inserts the item to the node.
    """
    if item != self._data:
        if item < self._data:
            if self._left is None:
                self._left = Node(item)
            else:
                self._left.insert(item)
        else:
            if self._right is None:
                self._right = Node(item)
            else:
                self._right.insert(item)

A test in an interactive Python shell could runs as follows.

>>> from classnode import Node
>>> nd = Node("single")
>>> print(nd)
single
>>> left = Node("left")
>>> right = Node("right")
>>> root = Node("root", left, right)
>>> print(root.left())
left
>>> print(root.right())
right
>>> root.insert("next")
>>> print(root.left().right())
next

The code above is placed in the definition of main() below:

def main():
    """
    Simple test on the Node class.
    """
    node = Node("single")
    print('a single node :', node)
    left = Node("left")
    right = Node("right")
    root = Node("root", left, right)
    print('the root node :', root)
    print('-> its left :', root.left())
    print('-> its right :', root.right())
    root.insert("next")
    print('after inserting \"next\",')
    print('at the right of the left :', end =' ')
    print(root.left().right())

Because an object of the class Node cannot be None and we want to have trees that are empty, we define a class Tree separately.

class Tree(object):
    """
    Defines a ordered binary tree.
    """
    def __init__(self):
        """
        Returns an empty tree.
        """
        self._root = None

What do we want to do with our trees? Well consider for instance, the sorting of some words.

>>> from classtree import Tree
>>> t = Tree()
>>> t.add("here")
>>> t.add("comes")
>>> t.add("the")
>>> t.add("best")
>>> t.add("part")
>>> t.add("we")
>>> t.add("have")
>>> t
here
|->comes
|  |->best
|  |->have
|->the
|  |->part
|  |->we

The string representation defines the representation of the object. Its definition is recursive as well and depends on a method show().

def __str__(self):
    """
    Returns the string representation.
    """
    if self._root is None:
        return ''
    else:
        result = self.show(self._root, 0)
        return result[0:len(result)-1]

def __repr__(self):
    """
    The representation is the string representation.
    """
    return str(self)

The display of a tree is defined by the recursive method show(), which remembers the current level in the tree by the parameter k.

def show(self, node, k):
    """
    Returns a string to display a tree,
    for the current node and level k.
    """
    result = (k-1)*"|  "
    if k > 0:
        result = result + "|->"
    result = result + str(node) + "\n"
    if node.left() is not None:
        result += self.show(node.left(), k+1)
    if node.right() is not None:
        result += self.show(node.right(), k+1)
    return result

Traversing Trees

We encountered the inorder traversal before when sorting numbers, with the example in Fig. 35.

Definition of Inorder Traversal

The inorder traversal of a binary tree is defined as follows:

  1. inorder traverse the left branch of the node,
  2. visit the data at the node,
  3. inorder traverse the right branch of the node.

The data comes out sorted, for an ordered binary tree.

For an arithmetic expression, for example: 3 + 4, where the + is stored at the node, 3 at the left, and 4 at the right, this order corresponds to the infix notation: 3 + 4.

The code for and inorder traversal is provided by the method which applies to a node in the tree.

def inorder_nodes(self, node):
    """
    Returns a list by traversing nodes in inorder.
    """
    result = []
    if node.left() is not None:
        result = self.inorder_nodes(node.left())
    result.append(node.value())
    if node.right() is not None:
        result += self.inorder_nodes(node.right())
    return result

As a tree can be None, the method to traverse a Tree object first tests whether a tree is empty or not.

def inorder(self):
    """
    Returns the data as a list in inorder.
    """
    if self._root is None:
        return []
    else:
        return self.inorder_nodes(self._root)

Definition of Preorder Traversal

The preorder traversal of a binary tree is defined as follows:

  1. visit the data at the node,
  2. preorder traverse the left branch of the node,
  3. preorder traverse the right branch of the node.

For an arithmetic expression, for example: 3 + 4, this order corresponds to the prefix notation: + 3 4.

The method below applies preorder traversal to a node.

def preorder_nodes(self, node):
    """
    Returns a list by traversing nodes in preorder.
    """
    result = [node.value()]
    if node.left() is not None:
        result += self.preorder_nodes(node.left())
    if node.right() is not None:
        result += self.preorder_nodes(node.right())
    return result

On a tree, which can be None, we first check whether the tree is empty or not.

def preorder(self):
    """
    Returns the data as a list in preorder.
    """
    if self._root is None:
        return []
    else:
        return self.preorder_nodes(self._root)

Definition of Postorder Traversal

The postorder traversal of a binary tree is defined as follows:

  1. postorder traverse the left branch of the node,
  2. postorder traverse the right branch of the node,
  3. visit the data at the node.

For an arithmetic expression, for example: 3 + 4, this order corresponds to the postfix notation: 3 4 +.

The postorder traversal is encoded in the method below.

def postorder_nodes(self, node):
    """
    Returns a list by traversing nodes in postorder.
    """
    result = []
    if node.left() is not None:
        result += self.postorder_nodes(node.left())
    if node.right() is not None:
        result += self.postorder_nodes(node.right())
    result.append(node.value())
    return result

For a tree, which may be empty, we first check whether the Tree object is None or not.

def postorder(self):
    """
    Returns a list in postorder.
    """
    if self._root is None:
        return []
    else:
        return self.postorder_nodes(self._root)

Running the main function leads then to the following output.

$ python3 classtree.py
here
|->comes
|  |->best
|  |->have
|->the
|  |->part
|  |->we
['here', 'comes', 'best', 'have', 'the', 'part', 'we']
['best', 'comes', 'have', 'here', 'part', 'the', 'we']
['best', 'have', 'comes', 'part', 'we', 'the', 'here']

Expression Trees

Consider the following problem. How to convert '5 * (x + (3 - 8*y))/z' into '5 * [x + [3 - 8*y]]/z'?

Let us practice in an interactive Python shell:

>>> s = '5 * (x + (3 - 8*y))/z'
>>> L = s.split('(')
'5 * [x + [3 - 8*y]]/z'
>>> r = '['.join(L)
'5 * [x + [3 - 8*y]]/z'

The expression tree can then be stored as a string representation of a list:

'[5,*,[ x, + ,[3 - [8,*,y]],/,z]]'

When storing tree as list of lists, consider *, x, etc. as strings: '*', 'x' to prevent their evaluation.

Substitution of variables into a string is then a split followed by a join, encapsulated in the following function.

def subs(form, x, y):
    """
    Replaces all occurrences of x
    in the string form by y.
    """
    data = form.split(x)
    return y.join(data)

Testing the substitution is done by the following function:

def main():
    """
    Prompts user for string and two symbols.
    """
    ins = input("Give a string : ")
    x = input("   what to replace : ")
    y = input("replacement string : ")
    out = subs(ins, x, y)
    print("the new string \"%s\"" % out)

Evaluation of a simple binary expression follows its recursive definition:

< operand > ::= < variable >|< number > |< expression >

< operator > ::= < + >|< - >|< * >|< / >

< expression > ::= < operand >| < operand >< operator >< operand >

Either an expression evaluates directly to a number or the value of a variable (base cases), or an expression consists of two expressions, separated by an operator (recursive call).

Exercises

  1. Write a recursive algorithm to generate a complete binary tree of k levels. The user determines the value for k. How many data elements does this complete tree have? Use a preorder traversal to assign an increasing sequence of integer numbers as data in the nodes.
  2. Draw a complete binary tree with k levels on canvas. Let k be given by the user in an entry field.
  3. For the tree drawn in exercise 2, write a GUI that allows the user to enter, view, and modify the elements at each node by pressing the mouse at the location of the node as shown on canvas.
  4. Describe an algorithm to convert the infix notation of expressions into postfix, where the operand comes last, e.g.: 3 + 9 becomes 3 9 +.