Inserting Into a Binary Search Tree
Chapter Sixty-Six
Syllabus topic Module 2, "Trees: Binary Search Tree"
Pages 202 to 204 of 411
In one line
Insertion searches for the value, and where the search falls off the tree is exactly where the new node is attached as a leaf.
The algorithm
insert(node, value):
if node is null: return a new node holding value
if value < node.data: node.left = insert(node.left, value)
if value > node.data: node.right = insert(node.right, value)
(equal: do nothing, or handle duplicates by the chosen convention)
return node
Two things about this are worth stating.
A new value always becomes a leaf. Nothing in the existing tree moves. The search walks down until it reaches a null, and that null becomes the new node.
The returned node is assigned back. node.left = insert(node.left, value) is what makes the new leaf actually attach. Writing insert(node.left, value) alone compiles, runs, and inserts nothing, because the new node is created and immediately discarded. That is the single commonest error in this chapter.
Run, with the tree after each insertion
class BNode:
__slots__ = ("data", "left", "right")
def __init__(self, data):
self.data = data
self.left = None
self.right = None
def insert(root, value):
if root is None:
return BNode(value)
if value < root.data:
root.left = insert(root.left, value) # the assignment matters
elif value > root.data:
root.right = insert(root.right, value)
return root
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 height(node):
return -1 if node is None else 1 + max(height(node.left), height(node.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
root = None
for value in (50, 30, 70, 20, 40, 60, 80):
root = insert(root, value)
print("insert %-3d -> %-36s height %d" % (value, shape(root), height(root)))
print()
print("inorder:", inorder(root), "which is sorted:", inorder(root) == sorted(inorder(root)))
print()
print("inserting a duplicate does nothing:")
before = shape(root)
root = insert(root, 40)
print(" shape unchanged:", shape(root) == before)
print(" size unchanged :", len(inorder(root)) == 7)insert 50 -> 50 height 0
insert 30 -> 50(30, .) height 1
insert 70 -> 50(30, 70) height 1
insert 20 -> 50(30(20, .), 70) height 2
insert 40 -> 50(30(20, 40), 70) height 2
insert 60 -> 50(30(20, 40), 70(60, .)) height 2
insert 80 -> 50(30(20, 40), 70(60, 80)) height 2
inorder: [20, 30, 40, 50, 60, 70, 80] which is sorted: True
inserting a duplicate does nothing:
shape unchanged: True
size unchanged : TrueSeven values, a perfectly balanced tree of height 2, and the inorder is sorted. Every intermediate shape was printed by the program after the insertion that produced it.
The error that inserts nothing
The missing assignment deserves a demonstration, because the broken version does not crash.
Inserting Into a Binary Search Tree
class BNode:
__slots__ = ("data", "left", "right")
def __init__(self, data):
self.data = data
self.left = None
self.right = None
def insert_correct(root, value):
if root is None:
return BNode(value)
if value < root.data:
root.left = insert_correct(root.left, value)
elif value > root.data:
root.right = insert_correct(root.right, value)
return root
def insert_broken(root, value):
"""The new node is created and then thrown away."""
if root is None:
return BNode(value)
if value < root.data:
insert_broken(root.left, value) # no assignment
elif value > root.data:
insert_broken(root.right, value)
return root
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
for name, insert in (("correct", insert_correct), ("broken ", insert_broken)):
root = None
for value in (50, 30, 70, 20, 40):
root = insert(root, value)
print("%s: tree holds %s" % (name, inorder(root)))
print()
print("the broken version raised no error and kept only the root's first child.")correct: tree holds [20, 30, 40, 50, 70]
broken : tree holds [50]
the broken version raised no error and kept only the root's first child.The broken version holds one of the five values and reported nothing wrong. Only the first insertion worked, because at the top level the caller does assign the return value (root = insert(root, value)). Every insertion after that descended into the tree, created its node, and threw it away.
What the insertion order does to the shape
The same values, inserted in different orders, give completely different trees. This is the setup for chapter 68.
class BNode:
__slots__ = ("data", "left", "right")
def __init__(self, data):
self.data = data
self.left = None
self.right = None
def insert(root, value):
if root is None:
return BNode(value)
if value < root.data:
root.left = insert(root.left, value)
elif value > root.data:
root.right = insert(root.right, value)
return root
def build(values):
root = None
for value in values:
root = insert(root, value)
return root
def height(node):
return -1 if node is None else 1 + max(height(node.left), height(node.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))
orders = [
("balanced order ", [50, 30, 70, 20, 40, 60, 80]),
("sorted order ", [20, 30, 40, 50, 60, 70, 80]),
("reverse sorted ", [80, 70, 60, 50, 40, 30, 20]),
("almost sorted ", [20, 30, 40, 50, 80, 70, 60]),
]
for name, values in orders:
tree = build(values)
print("%s height %d %s" % (name, height(tree), shape(tree)))balanced order height 2 50(30(20, 40), 70(60, 80))
sorted order height 6 20(., 30(., 40(., 50(., 60(., 70(., 80))))))
reverse sorted height 6 80(70(60(50(40(30(20, .), .), .), .), .), .)
almost sorted height 6 20(., 30(., 40(., 50(., 80(70(60, .), .)))))Inserting Into a Binary Search Tree
The same seven values. Height 2 in one order and height 6 in another, which is a line.
Sorted input is not an unusual case: data often arrives sorted, from a database, a file, or a previous sort. The commonest real input produces the worst possible tree, and that is chapter 68.
The cost
| Cost | |
|---|---|
| Insertion into a balanced tree | O(log n) |
| Insertion into a degenerate tree | O(n) |
| Nodes moved | zero, always |
The last row is worth noticing against the array of chapter 12, which moved everything after the insertion point. A tree pays with pointer-following rather than with movement.
Quick revision
- Insertion searches for the value; where the search falls off the tree is where the new leaf goes.
- A new value always becomes a leaf and nothing existing moves.
node.left = insert(node.left, value): the assignment is what attaches the node. Omitting it inserts
nothing and raises no error.
- A duplicate is ignored under this book's convention.
- Insertion order decides the shape: the same seven values gave height 2 in one order and height 6 in
sorted order, which is a line.
- Sorted input is common, which makes the worst case common.
- O(log n) balanced, O(n) degenerate, and zero nodes moved either way.
Test yourself
1. Where does a newly inserted value end up? As a leaf, at the position where the search for it falls off the tree.
2. Why must the recursive call be assigned back to the child pointer? Because the function returns the subtree's new root, which for an empty subtree is the newly created node. Without the assignment the new node is discarded and nothing is inserted.
3. What happens if the assignment is omitted, and why is it dangerous? Only the first insertion takes effect, because only the top level caller assigns the return value. No error is raised: in the run, five insertions left a tree holding one value, silently.
4. The same seven values gave heights 2 and 6 in different orders. Which order gave 6 and why? Sorted order, ascending or descending. Each new value is larger (or smaller) than everything present, so it goes to the same side every time and the tree becomes a line.
5. Why is that the dangerous case in practice? Because sorted input is common: data often arrives already in order from a file, a database or an earlier sort. The commonest input produces the worst tree.
6. How many existing nodes move during an insertion? None, ever. The cost is in walking to the position, not in moving anything, which is the difference from an array.
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.