Purely Functional Data structures
> {-# OPTIONS_GHC -Wincomplete-patterns #-}
> {-# LANGUAGE ScopedTypeVariables, FlexibleInstances, DeriveFoldable #-}
> module AVL (Set(..),AVL(..),
> avlEmpty,avlElements,avlMember,avlInsert,avlDelete,
> t1,t2,t3,bad1,bad2,bad3,main,rebalance,height,bf,
> setProperties,prop_empty,prop_elements,prop_insert1,
> prop_insert2,prop_delete1,prop_delete2,prop_delete3,
> avlProperties,prop_BST,prop_ht,prop_balance) where
> import Prelude hiding (zipWith,zipWith3)
> import Test.QuickCheck hiding (elements)
The goal this homework is to implement a purely functional version of AVL trees in Haskell. If you are unfamiliar with this data structure, the wikipedia page is a good start.
As usual, you should work on this file.
AVL trees are an alternative implementation of balanced binary trees. Like Red-Black trees, they can be used to implement a set-like interface.
> class Set s where
> empty :: s a
> member :: Ord a => a -> s a -> Bool
> insert :: Ord a => a -> s a -> s a
> elements :: Ord a => s a -> [a]
> delete :: Ord a => a -> s a -> s a
Your job in this homework will be to implement and test the following type class instance.
> instance Set AVL where
> empty = avlEmpty
> member = avlMember
> insert = avlInsert
> elements = avlElements
> delete = avlDelete
Set invariants
Go through the lecture notes and find some general correctness properties about sets. These properties can be specialized to AVL trees here, but you should only use functions from the Set
type class above in their definition. The names and their types are hints, feel free to change them. You may also add more properties than shown here. (Make sure you update setProperties
if necessary.)
To assist your definitions, you may also use the following equality instance for AVL
-based sets.
> -- | Compare sets for equality
> instance (Ord a) => Eq (AVL a) where
> t1 == t2 = elements t1 == elements t2
NB: The quickcheck counterexample
function names any failing inputs appropriately.
> setProperties :: Property
> setProperties =
> counterexample "empty" prop_empty .&&.
> counterexample "elts" prop_elements .&&.
> counterexample "insert1" prop_insert1 .&&.
> counterexample "insert2" prop_insert2 .&&.
> counterexample "delete1" prop_delete1 .&&.
> counterexample "delete2" prop_delete2 .&&.
> counterexample "delete3" prop_delete3
AVL tree definition & invariants
AVL trees can be implemented with the following datatype definition. This definition is similar to that of standard binary trees, the only difference is that nodes store the height of the tree at that point.
> data AVL e = E -- empty tree
> | N -- non-empty tree, with...
> Int -- 1. cached height of the tree
> (AVL e) -- 2. left subtree
> e -- 3. value
> (AVL e) -- 4. right subtree
> deriving (Show, Foldable)
The height of a tree is the maximum distance from a node any leaf below it. In a wellformed AVL
tree, we should be able to access this component straight off.
> -- | Access the height of the tree
> height :: AVL e -> Int
> height E = 0
> height (N h _ _ _) = h
The balance factor corresponds to the difference in height between the left subtree and the right subtree of the node. An invariant of AVL
trees is that the balance factor must be between -1 and 1.
> -- | Calculate the balance factor of a node
> bf :: AVL e -> Int
> bf E = 0
> bf (N _ l _ r) = height l - height r
As the definitions above imply, AVL
trees must satisfy specific invariants that ensure that the tree is balanced. In this problem, you'll need to define quickcheck properties for those invariants.
Of course, AVL
trees must be binary search trees.
And they must satisfy the AVL invariants about height and balance.
> -- | The height stored at each node is correctly calculated.
> prop_ht :: AVL Int -> Bool
> prop_ht = undefined
> -- | The balance factor at each node is between -1 and +1.
> prop_balance :: AVL Int -> Bool
> prop_balance = undefined
> avlProperties :: Property
> avlProperties =
> counterexample "bst" prop_BST .&&.
> counterexample "height" prop_ht .&&.
> counterexample "balance" prop_balance
In order to use quick check test these properties of AVL trees, we need to define a generator for arbitrary AVL trees. Feel free to use any of the functions of the Set
interface in this implementation, even if you haven't defined those operations for AVL trees yet.
> instance (Ord e, Arbitrary e) => Arbitrary (AVL e) where
> arbitrary = undefined
> shrink = undefined
AVL tree operations
Define the first three operations, the empty tree, the function to list the elements of the AVL tree, and a function to lookup up elements in the tree.
> -- | List the elements in the tree, in order
> avlElements :: AVL e -> [e]
> avlElements t = undefined
> -- | Determine whether an element is contained within the tree
> avlMember :: Ord e => e -> AVL e -> Bool
> avlMember = undefined
Sample trees
Build a few particular trees that you can use as test cases later---some that obey all of the AVL invariants...
... and some others that do not...
Make sure that you do NOT change the type annotations for these trees. Your test cases should all have the same type, just some should violate the correctness properties of the AVL type.
Now write a testing function that makes sure that the good trees are valid AVL trees and the bad trees fail at least one property.
Rebalance
Write a function rebalance
that takes a tree e
whose root node has balance factor -2 or +2 and rearranges it to an equivalent tree that satifies the balance factor invariant.
For this step, you will probably find it helpful to have a good diagram to refer to (such as the one on Wikipedia.) Note, though, that most explanations of AVL trees will talk about "rotating" the nodes near the root, which implies some sort of pointer manipulation. Here, we're simply rebuilding a completely new tree out of the pieces of the old one, so the notion of rotating doesn't really apply. In particular, you may find it easier to implement the "double rotations" that standard presentations of the algorithm talk about in a single step.
Even so, a diagram that shows the effect such rotations are trying to achieve is a useful guide to implementing your rearrangement. I named the variables in my patterns to match the labels in the diagram I was looking at, and this made it very much easier to write the rearranged trees correctly.
Insert
Write an insertion function for adding new elements to AVL trees.
You should use QuickCheck to verify your implementation of avlInsert
-- both the fact that it correctly implements insertion and that the resulting tree is an AVL tree.
> -- | Insert a new element into a tree, returning a new tree
> avlInsert :: (Ord e) => e -> AVL e -> AVL e
> avlInsert = undefined
Delete
Write a function that removes an element from a tree and rebalances the resulting tree as necessary. Again, use the properties defined above to test your implementation, making sure that it implements the deletion operation correctly and preserves the AVL tree properties.
> -- | Delete the provided element from the tree
> avlDelete :: Ord e => e -> AVL e -> AVL e
> avlDelete = undefined