Recitation 10: Deletion in binary search trees; Boolean AST

Deleting from binary search trees

The data structure below implements a "dictionary" abstract data type using a binary search tree. A dictionary maps a string key to some value. In the binary search tree implementation, the search key is a string, and the datum is parametric. Each key-value pair is stored in a tree node, and the tree is ordered by the natural ordering of strings.

Insertion, lookup, and deletion use the same algorithm as a binary search tree with integer keys. This holds for any type with a comparison function.

We'll focus on the deletion operation. A convenient way to delete a node from a binary search tree is to remove the greatest element of the left subtree of the node, and "overwrite" the deleted node with this greatest element. If no greatest element exists, then there is no left subtree, and hence one can simply replace the deleted node with the right subtree.

This operation is straightforward and minimizes changes to the tree. The greatest element will always be a leaf node, and so deleting it requires only changing its parent. The only change to the structure of the tree will be the removal of the greatest element from the left subtree.

  structure AssocTree =
    struct
	type key = string
	(* Invariant: for Nodes, data to the left have keys that
        * are LESS than the datum and the keys of
	 * the data to the right. *)
	datatype 'a dict = Empty | Node of {key: key,datum: 'a,
					    left: 'a dict,right: 'a dict}
	fun make():'a dict = Empty
  
	fun insert (d:'a dict) (k:key) (x:'a) : 'a dict =
	    case d of
		Empty => Node{key=k, datum=x, left=Empty, right=Empty}
	      | Node {key=k', datum=x', left=l, right=r} =>
		    (case String.compare(k,k') of
			 EQUAL =>
			     Node{key=k, datum=x, left=l, right=r}
		       | LESS =>
			     Node{key=k',datum=x',left=insert l k x,
				  right=r}
		       | GREATER =>
			     Node{key=k',datum=x',left=l,
				  right=insert r k x})
  
	exception NotFound
  
	fun lookup (d:'a dict) (k:key) : 'a =
	    case d of
		Empty => raise NotFound
	      | Node{key=k',datum=x, left=l, right=r} =>
		    (case String.compare(k,k') of
			 EQUAL => x
		       | LESS => lookup l k
		       | GREATER => lookup r k)
  
	fun  delete (d:'a dict) (k: key) : 'a dict =
	    let
		(* This code can be simplified if we guarantee that 
		 * removeGreatest is always called on a non-empty subtree. 
		*)
		fun removeGreatest(d:'a dict) : ('a dict * ((key * 'a) option)) =
		    case d of
			Empty => (Empty, NONE)
		      | Node{key=k',datum=x, left=l, right=Empty} =>
			    (l, SOME(k', x))
		     | Node{key=k',datum=x, left=l, right=r} =>
			    let 
				val (subtree, greatest) = removeGreatest(r)
			    in
				(Node{key=k', datum=x, left=l, right=subtree}, greatest)
			    end
	    in
	    case d of
		Empty => raise NotFound
	      | Node{key=k',datum=x, left=l, right=r} =>
		    (case String.compare(k,k') of
			 EQUAL => 
			     let
				 val (lefttree, greatest) = removeGreatest(l)
			     in
				 case greatest of
				     NONE => r
				     | SOME(nk,nv) => Node{key=nk, 
                                                 datum=nv, left=lefttree, right=r}
			     end
		       | LESS => Node{key=k', datum=x, left=delete l k, right=r}
		       | GREATER => Node{key=k', datum=x, left=l, right=delete r k})
	    end
	    
    end

Evaluating boolean ASTs

Here is a function for evaluating an abstract syntax tree for boolean expressions. Notice how closely the grammar corresponds with the bool_ast datatype.

Grammar

  e ::= true | false | e0 andalso e1 | e0 orelse e1

  datatype bool_ast = TRUE | FALSE | ANDALSO of bool_ast * bool_ast 
    | ORELSE of bool_ast * bool_ast
  
  fun evaluate(exp: bool_ast) : bool = 
    case exp of
	TRUE => true
  | FALSE => false
  | ANDALSO(x,y) => if evaluate(x) then evaluate(y) 
		    else false
  | ORELSE(x,y) =>  if evaluate(x) then true
		    else evaluate(y)

This evaluation procedure implements short circuit evaluation, as expressions are evaluated only if it is needed.


Suppose we wish to extend the grammar with variables. We'll denote this by id (identifier). In the homework assignment, variables are implemented with substitution. Here, we'll implement variables by making dictionary lookups into a set of bindings:

New grammar

  e ::= true | false | e0 andalso e1 | e0 orelse e1 | id

  datatype bool_ast = TRUE | FALSE | ANDALSO of bool_ast * bool_ast 
    | ORELSE of bool_ast * bool_ast | ID of string
  
 fun evaluate(exp: bool_ast, bindings: bool AssocTree.dict) : bool = 
    case exp of
	TRUE => true
  | FALSE => false
  | ANDALSO(x,y) => if evaluate(x, bindings) then evaluate(y, bindings) 
		    else false
  | ORELSE(x,y) =>  if evaluate(x, bindings) then true
		    else evaluate(y, bindings)
  | ID(varname) => AssocTree.lookup bindings varname