munotes®

The Binary Search Tree: The Invariant

Get access to whole semester resourcesSemester Pass

Chapter Sixty-Four

Syllabus topic Module 2, "Trees: Binary Search Tree"

Pages 196 to 198 of 411

In one line

A binary search tree is a binary tree in which, at every node, every value in the left subtree is smaller and every value in the right subtree is larger.

The invariant, said correctly

For every node x: every value in x's left subtree is less than x, and every value in x's right subtree is greater than x.

The wording that students usually give is: "the left child is smaller and the right child is larger." That is not the same thing and it is not enough. It talks about the two children only, and says nothing about grandchildren.

Here is a tree that satisfies the wrong rule and is not a search tree:

20

/ .

10 30

/ .

5 25

Check it with the wrong rule: at 20, left child 10 is smaller and right child 30 is larger, fine. At 10, left child 5 is smaller and right child 25 is larger, fine. Every node passes.

But 25 is in the left subtree of 20, and 25 is greater than 20. Searching for 25 from the root would go right at 20 and never find it. It is not a search tree.

A checker that tells the two rules apart

class BNode:
    __slots__ = ("data", "left", "right")

    def __init__(self, data, left=None, right=None):
        self.data = data
        self.left = left
        self.right = right


def shape(node):
    if node is None:
        return "."
    if node.left is None and node.right is None:
        return str(node.data)
    return "%s(%s, %s)" % (node.data, shape(node.left), shape(node.right))


def children_only(node):
    """The WRONG rule: compares a node with its two children only."""
    if node is None:
        return True
    if node.left is not None and node.left.data >= node.data:
        return False
    if node.right is not None and node.right.data <= node.data:
        return False
    return children_only(node.left) and children_only(node.right)


def whole_subtree(node, low=None, high=None):
    """The RIGHT rule: every value in the subtree must lie in a range."""
    if node is None:
        return True
    if low is not None and node.data <= low:
        return False
    if high is not None and node.data >= high:
        return False
    return (whole_subtree(node.left, low, node.data)
            and whole_subtree(node.right, node.data, high))


#        20                        20
#      /    .                    /    .
#   10       30     against   10       30
#  /  .                      /  .
# 5    25                   5    15
impostor = BNode(20, BNode(10, BNode(5), BNode(25)), BNode(30))
genuine = BNode(20, BNode(10, BNode(5), BNode(15)), BNode(30))

for name, tree in (("impostor", impostor), ("genuine ", genuine)):
    print("%s %-28s children-only rule: %-5s  whole-subtree rule: %s"
          % (name, shape(tree), children_only(tree), whole_subtree(tree)))

print()
print("the impostor passes the wrong rule and fails the right one.")
print("25 sits in the LEFT subtree of 20 and is greater than 20.")
impostor 20(10(5, 25), 30)            children-only rule: True   whole-subtree rule: False
genuine  20(10(5, 15), 30)            children-only rule: True   whole-subtree rule: True

the impostor passes the wrong rule and fails the right one.
25 sits in the LEFT subtree of 20 and is greater than 20.
munotes.in196

The Binary Search Tree: The Invariant

The right way to write the check is the one above: carry a range down the tree. Every node must lie strictly between the bounds it inherits, and it narrows the bound for each child.

The second way to check it

There is a neater test, and it is a direct consequence of chapter 59:

A binary tree is a binary search tree exactly when its inorder traversal is strictly increasing.

class BNode:
    __slots__ = ("data", "left", "right")

    def __init__(self, data, left=None, right=None):
        self.data = data
        self.left = left
        self.right = right


def inorder(node, out=None):
    out = [] if out is None else out
    if node is not None:
        inorder(node.left, out)
        out.append(node.data)
        inorder(node.right, out)
    return out


def whole_subtree(node, low=None, high=None):
    if node is None:
        return True
    if low is not None and node.data <= low:
        return False
    if high is not None and node.data >= high:
        return False
    return (whole_subtree(node.left, low, node.data)
            and whole_subtree(node.right, node.data, high))


def inorder_increasing(node):
    walk = inorder(node)
    return all(walk[i] < walk[i + 1] for i in range(len(walk) - 1))


impostor = BNode(20, BNode(10, BNode(5), BNode(25)), BNode(30))
genuine = BNode(20, BNode(10, BNode(5), BNode(15)), BNode(30))

for name, tree in (("impostor", impostor), ("genuine ", genuine)):
    print("%s inorder %-22s increasing: %-6s range check: %s"
          % (name, str(inorder(tree)), inorder_increasing(tree),
             whole_subtree(tree)))

print()
print("the two tests agree on both trees:",
      all(inorder_increasing(t) == whole_subtree(t)
          for t in (impostor, genuine)))
impostor inorder [5, 10, 25, 20, 30]    increasing: False  range check: False
genuine  inorder [5, 10, 15, 20, 30]    increasing: True   range check: True

the two tests agree on both trees: True

The impostor's inorder is 5, 10, 25, 20, 30, which is not increasing: 25 comes before 20. That is the same defect seen from a different angle.

Duplicates

The invariant as stated uses strictly less and strictly greater, so duplicates are not allowed. Three conventions exist and an answer should name the one it uses:

Forbid them. An insertion of an existing value does nothing. This is what this book does. All duplicates to the right, using "less than" on the left and "greater than or equal" on the right. Keep a count in each node, which stores one node per distinct value with a multiplicity.

The third is usually the best in practice and is worth mentioning.

What the invariant buys

Everything in the next three chapters. At any node, a comparison tells you which one of the two subtrees can possibly contain your value, so the other is discarded entirely. That is the halving of chapter 50, and it is the whole purpose of the arrangement.

munotes.in197

The Binary Search Tree: The Invariant

Quick revision

  • Binary search tree: at every node, every value in the left subtree is less and every value in the right

subtree is greater.

  • "The left child is smaller and the right child is larger" is NOT the invariant: it allows a tree where

a grandchild sits on the wrong side, which search cannot find.

  • Check it by carrying a range down the tree, narrowing it at each step.
  • Equivalently: a binary tree is a search tree exactly when its inorder traversal is strictly increasing.
  • Duplicates are excluded by the strict inequalities; the three conventions are to forbid them, to send

them right, or to store a count per node.

  • The invariant is what lets one comparison discard an entire subtree.

Test yourself

1. State the binary search tree invariant exactly. For every node, every value in its left subtree is less than it and every value in its right subtree is greater than it.

2. Why is "the left child is smaller and the right child is larger" wrong? Because it only constrains the immediate children. A grandchild may then sit on the wrong side of an ancestor, as 25 in the left subtree of 20, and a search for it will go the other way and fail.

3. Describe the correct way to check the invariant programmatically. Carry a lower and an upper bound down the tree. Each node must lie strictly between them, and it becomes the upper bound for its left subtree and the lower bound for its right.

4. Give the equivalent test using a traversal. A binary tree is a binary search tree exactly when its inorder traversal is strictly increasing.

5. What is the inorder of the impostor tree in this chapter, and what does it show? 5, 10, 25, 20, 30. It is not increasing, since 25 comes before 20, which shows the tree is not a search tree.

6. Name the three conventions for duplicate values. Forbid them; place them all in the right subtree using a non-strict comparison there; or store a count in each node so one node holds a value and its multiplicity.

munotes.in198

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.

Issue
Done!