Notes:

Today's topic:

Reasoning about list structures.

Recall that:

NOTE: a non-empty [proper] list always has a list for its tail.

We can define some list manipulation procedures, like copy:

>>> On leftmost board, and leave it here for 2 proofs.

(define copy
  (lambda (l)
    (map (lambda (x) x) l)))

 

What about reversing a list? A bit trickier because resulting structure needs to be a LIST. Can’t just interchange arguments to cons (think about the case where the list has length 1!)

(define reverse
  (lambda (l)
    (if (null? l)
	'()
	(append (reverse (tail l))
		(list (head l))))))

What is the running time of reverse, in terms of n, the length of l? O(n^2)! Append takes time linear in the length of its first argument.

How would you write a linear time reverse? [Variant of a classic Microsoft interview question]

 

 

 

One way to think about lists and recursive list manipulation is using induction and the substitution model.

How could we go about showing that, for all lists l, (copy l) = l?

But it's a different kind of induction.

list - that's an integer for sure.

The length of () is 0

The length of (x) is 1

etc.

Proof by induction: (each step has a reason)

INDUCTION ON: n, length of l

STATEMENT: (copy l) = l for any list l of length n

BASE n=0:

Why?? Because (null? ()) ==> #t

INDUCTION:

Assume that for any list l of length n, (copy l) = l

We must show that for any list l' of length n+1, (copy l') = l'.

Using the substitution model, we know that:

(null? l') --> #f because the length of l' is n+1 > 0

The length (tail l') is length l' - 1, which is n

So, (copy (tail l')) = (tail l') *by the induction hypothesis*

[CLEARLY STATED]

Thus

(copy l') = (cons (head l') (copy (tail l')))

= (cons (head l') (tail l')) <<< by above reasoning

= l' <<< contract for cons

 

 


 

 

However, the induction on (length l) was a bit of a kludge.

Recall that induction was supposed to be usable on any inductively defined set:

Nat was defined by {0} and succ(x)=x+1

The set of lists is defined by '() and (cons x l) for any expression x and list l.

The corresponding inductive proofs

- Assume that the hypothesis holds for l'

- Show that it holds for (cons s l') for every s

This is called STRUCTURAL INDUCTION:

- Induct on the *structure* of lists.

 


 

In proofs, we'll write (cons s l) as s.l

Let's revisit our proof of copy by induction on lists:

INDUCTION ON: l

STATEMENT: (copy l) = l

BASIS: l = ().

(copy ()) = () by the substitution model, because (null? ()) = #t

INDUCTION: Assume (copy l') = l'

Must show (copy s.l') = s.l' for all s.

(null? s.l') = #f by definition, it contains at least s.

(copy s.l') = (cons (head s.l') (copy (tail s.l'))) <<< subst model

= (cons s (copy l')) <<< list contract

= (cons s l') <<< induction hypothesis!

= s.l' <<< definition

 

 

 

Another example, doable either way. We'll use structural induction.

(define member? 
  (lambda (thing lst)
    (cond ((null? lst) #f)
	  ((equal (head lst) thing) #t)
	  (else (member thing (tail lst))))))

equal is a fairly general check to see if its arguments are the same, means "print the same way" (more on this in a moment)

[probably skip this proof in lecture]

Intuitively, member? should return #t iff there is some location in lst where we find thing.

SHOW:

For l = (a_n ... a_1)

(member? x l)=#t <==> Exists i : 1 <= i <= n : a_i =x

*** Explain that this means,

*** There is some i between 1 and n

*** Such that a_i = x

BASE:

(member? x ()) = #f by the substitution model because null? is true,

correct answer [by definition there are no a_i's]

INDUCTION:

Assume that For l' = (a_n ... a_1)

(member? x l')=#t <==> Exists i : 1 <= i <= n : a_i =x

Show this holds for (member? x b.l') [any b!]

Two Cases:

IF b=x, then (member? x b.l') = #t by the substitution model, because

b is the same as x, thus the second clause gets taken #t is returned

There is indeed an index i such that a_i = x, namely i=n+1,

because b.l' = (a_(n+1) a_n ... a_1)

IF b =/= x, then:

(member? x b.l')

= (member? x (tail b.l')) <<< substitution model

= (member? x l') <<< rules for tail

 

By the induction hypothesis this expression, (member? x l'), is

true for lists of length <= n iff Exists i:1 <= i <= n : a_i =x

 


 

One issue that has begun to creep in is,

Recall that equal? tests for "print equality", which means look the same

eq? test for "structural equality", which means are the same thing

Here's an example: Recall that list with N args creates N boxes!

(define a (list 'barney 'and 'dino))

(define b a)

(define c (list 'barney 'and 'dino))

>>> Draw a two-worlds picture.

>>> On the left, the names a, b, c.

>>> On the right, two box-and-pointer pictures of (barney and dino)

>>> a and b point to one, c to the other.

 

How do we tell if the box-and-pointer representations are the same? Just recursively take apart the list structure making sure all the parts are the same.

Assuming we have a generic function equal? that works for "atomic" things like numbers, symbols and booleans, could define

(defmethod (= (x <pair>) (y <pair>))
  (and (= (head x) (head y))
       (= (tail x) (tail y))))

FYI, some of the other methods for = might look like

(defmethod (= (x <number>) (y <number>))
  (not (or (< x y) (> x y))))

(defmethod (= (x <boolean>) (y <boolean>))
  (or (and x y) (and (not x) (not y))))

 


 

Now, let's put some lists together:

(append '(1 2 3) '(4 5 6)) ==> (1 2 3 4 5 6)

This is again a Scheme primitive.

(define append
  (lambda (l1 l2)
    (if (null? l1)
	l2
	(cons (head l1) (append (tail l1) l2)))))

This is kind of surprising:

* l2 doesn't get looked at!

(append '(1 2 3) '(4 5 6))

(cons {1} (append {(2 3)} {(4 5 6)}))

(cons {1} (cons {2} (append {(3)} {(4 5 6)})))

(cons {1} (cons {2} (cons {3} (cons {()} {(4 5 6)}))))

(cons {1} (cons {2} (cons {3} {(4 5 6)})))

(cons {1} (cons {2} (cons {(3 4 5 6)})))

(cons {1} {(2 3 4 5 6)})

{(1 2 3 4 5 6)}

 

> Draw box diagram, with a new 1 2 3 and the tail of the 3 pointing to the old (4 5 6)

Two hard problems:

 

Another example of list equality

(define d (list 'and 't-rex))

(define f (append a d))

(define g (append a d))

(eq? f g) = #f as append *copies* the first list.

(equal? f g) = #t

Note though that

(eq? (tail (tail (tail f))) (tail (tail (tail g)))) = #t

-- since they *do* share structure.

This is the kind of thing that, when combined with side-effects, yields completely undebuggable programs!

2 pencils from the same box are equal?

They aren't eq? unless when you break one, the other breaks too

But, we don't yet know how to break a pencil [assignment, := or set!]

Pretty amazing it's not there yet.... Coming soon!

 


NEXT: Induction on trees

Binary tree, each node has left and right branch, with elements stored at leaves.

(defstruct <btree> left right)

(define (leaf? x)
  (not (btree? x)))

Now, let's do induction over BINARY TREES.

A leaf is a tree of DEPTH 0

Any other tree has depth one larger than the max of its left and right subtrees.

Note that trees don't have to be balanced.

More formally:

is a tree of depth 1+max(d1,d2).

 

Depth of trees is like length of lists:

 

[Aside: recursion is the *key* here, doing this iteratively is a nightmare]

(define (tree-depth tree)
    (if (leaf? tree)
        0
        (+ 1 (max (tree-depth (get-btree-left tree))
                  (tree-depth (get-btree-right tree)))))))

Show:

(count-leaves s) is the number of leaf nodes in s.

by induction on depth.

BASE:

depth = 0

s is by definition a leaf

(count-leaves s) = 1 by the substitution model, because leaf? is true

and there's clearly one leaf in the tree

INDUCTION:

Assume (count-leaves s) is correct if s is a tree of depth 0 <= k < n

Show (count-leaves s') is correct for s' of depth n.

Well, since it's depth > 0, it's not a leaf. leaf? is false so

(count-leaves s') = (+ (count-leaves (left s')) (count-leaves (right s')))

Now, both (left s') and (right s') are trees of depth < n, by definition of depth.

So the by the induction hypothesis the two calls (count-leaves (left s')) and (count-leaves (right s')) yield the number of leaves in the left and right subtrees, respectively.

And the total number of leaves in the tree s' is the sum of two, which is what is computed.

 

NOTE:

Here we used that "all depth < n" rather than just "depth = n - 1"

 

 

[Perhaps for recitation, depending on time in lecture]

Now, how about N-way trees rather than just binary ones?

 

>>> draw a n-way tree. <<<

* How would we define the depth of a structure?

Intent: an atom x is depth-0

a list of atoms '(this is a list of atoms) is depth-1

(a (depth-2) list) is depth-2

>>> Draw a box diagram, horizontally. <<<

Depth = number of head's you need to get to the bottom.

(tail's don't count.)

(define depth
  (lambda (l)
    (if (pair? l)
	0
	(max (+ 1 (depth (head l)))
	     (depth (tail l))))))

Note that the depth only increases with heads, not with tail's. Whereas for a binary tree, would be +1 of tail too.

 

This makes induction a bit tricky:

* calls to HEAD reduce the depth properly.

* calls do TAIL can keep the depth the same.

This is about the same as the usual asymmetries in list structures:

* heads go to simpler things (elements)

* tails just give more lists.

 

 

 

Lessons of the day:

Structural Induction

Equality of Structures: