CS195Y Lecture 13

2/29/16

Announcements:

Binary search trees

What are some examples?

   [5]    [5]     {}      [5]               [5]
           |             /   \             /   \
          [4]          [4]   [6]        [4]     [6]
                                                  \
                                                   [7]

Do these actually capture what a binary tree is?

Let’s model this in Alloy!

What do we do with “leaves”? We’ll define an Empty node that’s pointed to if a node has no children.

abstract sig Node {}
sig Empty extends Node {}
sig Data extends Node {
    num: Int,
    left: Node, 
    right: Node -- note: if this was Data, our tree would ever end!
}

What if we did make right of type Data? Right now, Alloy would just give us non-tree instances. If we constrained it to be a tree, Alloy would probably output a single empty node. If we ran for nontrivial instances, it would probably be unsatisfiable.

pred btree {
    -- root reaches everything
    some r: Node | 
        -- what's wrong with the line below? It doesn't include r
        -- Node in r.^(left + right)
        Node in r.^(left + right) + r
        
    all n: Node | {
        -- no cycles
        n not in n.^(left + right)
        
        -- at most one parent for each node
        -- not one, because that would disallow the root
        lone n.~(left + right) 
        
        -- distinct children if any
        -- one approach: this enforces that there be multiple Emptys, we're
        -- not going to use it to let us change our approach to leaf nodes
        -- n.left != n.right
        
        -- instead
        no n.left & n.right
    }
}

run btree for exactly 8 node

Uh oh, this run statement is not satisfiable!

Q: Why is it failing?
A: If we define Empty, then the total number of nodes must be odd.

run btree for exactly 3 Data, 4 Empty

This works, but… why might this not be there best representation?

Making the tree ordered

We need to actually implement this to make Alloy order the tree.

pred orderedTree {
    btree
    
    -- n' is all nodes reachable from the left, BUT without left
    -- all n: Node, n': n.left.^(left + right) - Empty |
    
    -- * is REFLEXIVE transitive closure, that includes the atom itself
    -- left descendants
    all n: Node, n': n.left.*^(left + right) - Empty | 
        n.num > n'.num // note: enforces distinct numbers 
    
    -- right descendants
    all n: Node, n': n.right.*^(left + right) - Empty | 
        n.num < n'.num // note: enforces distinct numbers
}

Note: we haven’t proven anything about binary search trees, we’ve just modeled them. This can be useful when encountering a new data structure: mock it up in Alloy and play with the instances it generates. Being able to experiment is powerful!

Now we have binary search trees, what can we do with them?

Search

What do we want to be true about search?

Searching for 5: If 5 is in the tree, we better find it. If 5 is not in the tree, we better not find it!

We’re going to model the descent steps.
Q: Why might seq be a better choice than util/ordering?
A: That is enforces descents to be of a particular length. This is unrealistic!

What characterizes a search? The value we’re looking for, and the path to the node that might contain that value.

line sig Descent {
    val: Int,
    path: seq Node
}{
    -- where do we start? The root node - the node with no parent
    no path.first.~(left + right)
    
    // actually do something
    not path.isEmpty
    
    // search until done
    path.last
    
    // search until done
    path.last in Empty or path.last.num = val
    path.last in Empty implies val not in path.elems.num
}

Q: Does Alloy give “index out of bounds” type errors?
A: It won’t error, but it might give you the empty set.

If we run what we found above, Alloy gives an instance where we search for (and find) the root’s number. Good, but not very useful yet!

Add:

    -- stop once we find it - do we need this?
    all idx: path.inds - path.lastIdx | 
        path[idx].num != val
        
    -- drive forward
    all idx: path.inds - path.lastIdx | {
        let idx' = add[idx, 1] | {
            path[idx].num > val implies path[idx'] = path[idx].left
            path[idx].num < val implies path[idx'] = path[idx].right
        }
    }

Note: there are many modeling choices for how to model this. Tim chose to take a more step-wise approach, because it’s more closely related to how search on a binary tree would likely be implemented in a standard language.

assert findIfThere {
    all d: Descent |
        orderedTree implies {
            d.val in Node.num =>
            d.path.last.num = d.val else
            d.path.last.num != d.val 
        }
}
check findIfThere for exactly 1 Descent, exactly 7 Node

Q: What haven’t we proven?
A: That this approach is correct in general.

Q: Ok, what part of that have we proven?
A: That if there were a counterexample of size < 7, we would have found it.