Trees
Binary trees
Inductive bt (X : Type) : Type :=
| empty
| node (l : bt X) (v : X) (r : bt X).
Arguments empty {X}.
Arguments node {X} l v r.
| empty
| node (l : bt X) (v : X) (r : bt X).
Arguments empty {X}.
Arguments node {X} l v r.
If you've studied plant biology, the word 'node' may make you
think of trees... but otherwise, what's going on here? What do
these structures have to do with trees?
When you encounter a definition you don't understand, you need to
play with it. So let's get playing!
Check empty.
Check @empty nat.
Check (node empty true empty).
Check (node empty 5 empty).
Check (node empty G (node empty C empty)).
Check (node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty)).
Check @empty nat.
Check (node empty true empty).
Check (node empty 5 empty).
Check (node empty G (node empty C empty)).
Check (node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty)).
Okay, those are all well typed. But... what are they? Let's adopt
a graph notation, where each constructor is a vertex in our
graph and we draw edges from constructors to their arguments. Here
are each of those examples in order.
Gallina term:
empty as a graph:
Gallina term:
@empty nat as a graph:
Gallina term:
node empty true empty as a graph:
Gallina term:
node empty 5 empty as a graph:
Gallina term:
node empty G (node empty C empty) as a graph:
Gallina term:
node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty) as a graph:
These graph notations can get a little heavy, so if it's
unambiguous, people will leave off the constructors. Here's the
last one in simpler notation:
Gallina term:
node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty) as a graph:
Note that we write nothing for empty and we write the value v
for a node.
Some terminology: a node has two children, l and r. A
node is a leaf node when both of its children are empty.
The topmost node is called the root. This means that computer
scientists draw trees upside down. Whoops! Too late to change
now.
Let's start by writing some functions on binary trees. We'll
compute their size (the number of nodes), determine whether
they're a leaf or not with is_leaf, count their leaves with
num_leaves.
empty as a graph:
empty
@empty nat as a graph:
empty
node empty true empty as a graph:
node /|\ / | \ / | \ / | \ empty true empty
node empty 5 empty as a graph:
node /|\ / | \ / | \ empty 5 empty
node empty G (node empty C empty) as a graph:
node /|\ / | \ / | \ empty G node /|\ / | \ / | \ empty C empty
node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty) as a graph:
node /|\ / | \ / | \ node 25 node /|\ /|\ empty | empty / | \ 12 / | \ node 5 empty /|\ / | \ empty 5 empty
node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty) as a graph:
25 /\ / \ / \ 12 5 /\ / \ 5 /\
Exercise: 1 star, standard (size)
Fixpoint size {X : Type} (t : bt X) : nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
☐
Definition is_leaf {X : Type} (t : bt X) : bool (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
☐
Fixpoint num_leaves {X : Type} (t : bt X) : nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
☐
Check empty.
Check @empty nat.
Check (node empty true empty). (* leaf node *)
Check (node empty 5 empty). (* leaf node *)
Check (node empty G (node empty C empty)).
Check (node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty)).
Compute (size (node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty))).
Check @empty nat.
Check (node empty true empty). (* leaf node *)
Check (node empty 5 empty). (* leaf node *)
Check (node empty G (node empty C empty)).
Check (node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty)).
Compute (size (node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty))).
We defined some terminology already: a leaf is a node where both
children are empty.
The root of a tree is the topmost node.
Definition root {X : Type} (t : bt X) : option X :=
match t with
| empty ⇒ None
| node _ v _ ⇒ Some v
end.
Compute (root (node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty))).
(* ===> Some 25 *)
match t with
| empty ⇒ None
| node _ v _ ⇒ Some v
end.
Compute (root (node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty))).
(* ===> Some 25 *)
It's common enough to want to make a leaf that we'll define a
function to do it for us.
Definition leaf {X : Type} (v : X) : bt X := node empty v empty.
Compute (root (node (leaf 12) 25 (node (leaf 5) 5 empty))).
(* ===> Some 25 *)
Compute (root (node (leaf 12) 25 (node (leaf 5) 5 empty))).
(* ===> Some 25 *)
Traversals
- A pre-order traversal puts the value first, then traverses the
left child, then the right child.
- An in-order traversal first traverses the left child, then the
value of the node, then the right child.
- A post-order traversal first traverses the left child, then the right child, then the value of the node.
Fixpoint inorder {X : Type} (t : bt X) : list X :=
match t with
| empty ⇒ []
| node l v r ⇒ inorder l ++ [v] ++ inorder r
end.
Compute (inorder (node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty))).
match t with
| empty ⇒ []
| node l v r ⇒ inorder l ++ [v] ++ inorder r
end.
Compute (inorder (node (node empty 12 empty) 25 (node (node empty 5 empty) 5 empty))).
Exercise: 2 stars, standard (traversals)
Define traversal functions preorder and postorder that implement those respective traversals.
(* FILL IN HERE *)
Write a tree that produces different output from all three
traversals. (Note the type!) It should have at least three
nodes.
Definition different_all_three : bt base (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Write a tree that produces the same output from pre- and in-order
traversals (but not post-order). It should have at least three
nodes.
Write a tree that produces the same output from in- and post-order
traversals (but not pre-order). It should have at least three
nodes.
Definition same_post_in : bt base (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
(* Do not modify the following line: *)
Definition manual_grade_for_traversals : option (nat×string) := None.
☐
(* Do not modify the following line: *)
Definition manual_grade_for_traversals : option (nat×string) := None.
☐
Binary search trees (BSTs)
- t is empty, or
- t is node l v r where:
Exercise: 1 star, standard (search_property)
Write two trees of naturals that have the search property and two trees that do not. All of your trees should have at least two nodes.
Definition bst1 : bt nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Definition bst2 : bt nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Definition non_bst1 : bt nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Definition non_bst2 : bt nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
(* Do not modify the following line: *)
Definition manual_grade_for_search_property : option (nat×string) := None.
☐
Definition bst2 : bt nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Definition non_bst1 : bt nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Definition non_bst2 : bt nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
(* Do not modify the following line: *)
Definition manual_grade_for_search_property : option (nat×string) := None.
☐
Inductive ordering : Type :=
| LT
| EQ
| GT.
Fixpoint compare (n m : nat) : ordering :=
match n, m with
| O, O ⇒ EQ
| S _, O ⇒ GT
| O, S _ ⇒ LT
| S n', S m' ⇒ compare n' m'
end.
Compute (compare 0 0). (* ===> EQ *)
Compute (compare 0 10). (* ===> LT *)
Compute (compare 7 4). (* ===> GT *)
Compute (compare 1000 1000). (* ===> EQ *)
| LT
| EQ
| GT.
Fixpoint compare (n m : nat) : ordering :=
match n, m with
| O, O ⇒ EQ
| S _, O ⇒ GT
| O, S _ ⇒ LT
| S n', S m' ⇒ compare n' m'
end.
Compute (compare 0 0). (* ===> EQ *)
Compute (compare 0 10). (* ===> LT *)
Compute (compare 7 4). (* ===> GT *)
Compute (compare 1000 1000). (* ===> EQ *)
Here's a function that uses the search property to rapidly
determine whether or not a value is in a BST already.
Fixpoint lookup (n : nat) (t : bt nat) : bool :=
match t with
| empty ⇒ false
| node l v r ⇒
match compare n v with
| LT ⇒ lookup n l
| EQ ⇒ true
| GT ⇒ lookup n r
end
end.
match t with
| empty ⇒ false
| node l v r ⇒
match compare n v with
| LT ⇒ lookup n l
| EQ ⇒ true
| GT ⇒ lookup n r
end
end.
Exercise: 3 stars, standard (insert)
Write a function that inserts a number n into a binary search tree t. To get full credit, you should use the binary search property!
Fixpoint insert (n : nat) (t : bt nat) : bt nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
☐
Exercise: 2 stars, standard (remove_min)
Write a function that tries to remove the smallest element from a binary search tree. You'll need to return not just the smallest element, but also what's left of the tree.
Fixpoint remove_min (t : bt nat) : option (nat × bt nat) (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
☐
Exercise: 4 stars, standard (remove)
Write a function that removes a number n from a binary search tree. If n isn't in the tree, you should return the tree unchanged.- Handling empty should be simple.
- To remove n from node l v r, look at compare n v.
Fixpoint remove (n : nat) (t : bt nat) : bt nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
☐
Exercise: 4 stars, standard, optional (is_bst)
Write a function is_bst that takes t : bt nat and returns true if t has the search property.- You'll need at least one helper function. How can you check the
"every value" parts of the search property?
- Don't think about efficiency---you're just making things hard for yourself. Get it to work first, and then make it efficient if that'll make you happy.
(* Do not modify the following line: *)
Definition manual_grade_for_is_bst : option (nat×string) := None.
☐
Definition manual_grade_for_is_bst : option (nat×string) := None.
☐
(* 2022-01-12 10:20 *)