Balanced Binary Search Trees
March 3, 2017
We begin with the same binary search tree code we wrote in a recent exercise:
(define (insert lt? x tree) (cond ((null? tree) (list x (list) (list))) ((lt? x (car tree)) (list (car tree) (insert lt? x (cadr tree)) (caddr tree))) ((lt? (car tree) x) (list (car tree) (cadr tree) (insert lt? x (caddr tree)))) (else tree)))
(define (member? lt? x tree) (cond ((null? tree) #f) ((lt? x (car tree)) (member? lt? x (cadr tree))) ((lt? (car tree) x) (member? lt? x (caddr tree))) (else #t)))
A binary search tree is balanced if the depths of its two children are the same, and if both of its two children are balanced. We are assuming here perfect balance, so that only trees with 2n−1 nodes for integer n can possibly be balanced:
(define (depth tree) (if (null? tree) 0 (+ 1 (max (depth (cadr tree)) (depth (caddr tree))))))
(define (balanced? tree) (if (null? tree) #t (and (= (depth (cadr tree)) (depth (caddr tree))) (balanced? (cadr tree)) (balanced? (caddr tree)))))
Here are two examples:
> (balanced? (insert < 4 (insert < 6 (insert < 0 (insert < 7 (insert < 1 (insert < 5 (insert < 2 (insert < 3 (list)))))))))) #f > (balanced? (insert < 0 (insert < 6 (insert < 4 (insert < 2 (insert < 1 (insert < 5 (insert < 3 (list))))))))) #t
There’s a lot of extra work going on there. Both functions use the same recursion, so they could be flattened into a single function. And both functions recompute previously-computed depths in their recursive calls; a more clever function could order its computations to calculate the depths only once. An even more clever function never even computes the depth: a tree is balanced if it is null, or if both its children are null, or if both its children are non-null and are balanced:
(define (balanced? tree) (or (null? tree) (and (null? (cadr tree)) (null? (caddr tree))) (and (not (null? (cadr tree))) (not (null? (caddr tree))) (balanced? (cadr tree)) (balanced? (caddr tree)))))
Here are the same two examples:
> (balanced? (insert < 4 (insert < 6 (insert < 0 (insert < 7 (insert < 1 (insert < 5 (insert < 2 (insert < 3 (list)))))))))) #f > (balanced? (insert < 0 (insert < 6 (insert < 4 (insert < 2 (insert < 1 (insert < 5 (insert < 3 (list))))))))) #t
You can run the program at http://ideone.com/JFq9dE. I wonder if CBP agents speak Scheme?
Untested, of course. I’m trying to simulate my being in the situation of that engineer :(
[sourcelang=”css”]
def makeNode(left=None, right=None):
(left, right)
def left(node):
node[0]
def right(node):
node[1]
left = makeNode(left=makeNode(left=makeNode(left=makeNode(left=makeNode()))))
print(“{} is {}balanced.”.format(‘left’,”” if isbalanced(left) else “not “))
left is balanced.
# good job Python!
[/sourcelang=”css”]
In Common Lisp:
Python functions need to have return statements. They return None by default.
[…] a recent exercise we wrote a program to determine if a binary tree was balanced. Today’s exercise is […]
A C++ solution that checks for near perfect balance.
[sourcelang=”c++”]
struct Node
{
int key;
Node * left, * right;
Node(int k)
{
key = k;
left = right = NULL;
}
};
int height(Node * node)
{
if(node == NULL)
return 0;
return max( height(node->left), height(node->right));
}
bool IsBalanced(Node * node)
{
int diff = height(node->left) – height(node->right);
return ( -1 <= diff && diff left) && IsBalanced(node->right));
}
[/sourcelang=”c++”]
Here’s a solution in Java. It uses pre-order traversal, and it each node checks that the heights of the left sub-tree and right sub-tree are within 1. The height calculation uses memoization to avoid redundant calculations.
Output: