Today:

Stacks, Queues, and Mutable Data Structures. More rope to hang yourself!

> Data structures are the basic tools of the trade.

- Choosing the right structures (and abstractions around them) is critical. Makes difference between

efficient and not

easy to understand and not

easy to solve and not

easy to debug and not

easy to change and not

> A few basic data structures in computer science,
> they show up everywhere. Two are stacks and queues, which
> you've undoubtedly seen, but as with most things in 212 they'll
> look a little different here.

A stack is like a stack of papers.

Often called LIFO -- Last In First Out (Reverses order)

A stack is a data structure supporting the following operations:

(make-stack) ---------- Makes a new empty stack

(insert thing stack) -- Returns a new stack with thing on top of `stack'.

** DOES NOT CHANGE THE ORIGINAL! Not a `!' **

(delete stack) ------- Returns a stack like `stack', but without its top element.

(top stack) ----------- Returns the top element of the stack.

(empty? stack) -------- Is there anything on the stack?

 

They obey the contract:

(top (insert thing stack)) = thing

(delete (insert thing stack)) = stack

(empty? (make-stack)) = #t

(empty? (insert thing stack)) = #f

So, we can implement a stack with lists:

insert = cons

delete = tail

top = head

empty? = null?

This implementation is quite efficient:

NOTE: This implementation does NOT CHANGE list structures, it just creates new ones to make a new stack after insert or delete operation.

>> This is all easy and standard and you know it all already. <<

[If you lectured this far, I owe you a glass of wine -- brd]


A queue has the same operation names as a stack, but they do slightly

different things:

* It's like a line of people at a cafeteria

- Waiting to pick trays off the stack?

* You can join at the back end of the line,

* But you leave at the front end.

* FIFO -- first in first out (maintains order)

- Nobody gets moldy

- Everything stays in the same order.

(make-queue) -----------

(insert thing queue) --- puts a thing at the tail end of the queue.

(delete queue) --------- deletes the head of the queue

(headq queue) ----------- returns the head of the queue

(empty? queue) ---------

 

Contract:

Let I be the number of insertions,

D be the number of deletions so far,

We must have I >= D at all times,

Let x_J be the J'th element inserted into the queue.

[We can be mathematicians, and start at 1, or computer scientists and start at 0. Today, we'll be mathematicians.]

(headq queue) = x_(D+1) if I > D

= undefined if I = D

(empty? queue) = (= I D)

For example, suppose we insert a b c d e f in that order.

^ ^

head tail

Now:

I = 6, D = 0

(headq queue) = x_(D+1) = x_1 = a

Now, let's do a delete

a b c d e f

^ ^

head tail

 

I = 6, D = 1

(headq queue) = x_(D+1) = x_2 = b, which is right.

We could implement a queue as a list with the head -- oldest element

-- at the front.

headq = head

delete = tail

empty? = null?

 

but this makes insert be:

(lambda (thing q) (append q (list thing)))

which is O(n) time. We have to walk all the way down the list (recall how append works!) This is expensive.

We could also do it backwards -- the head at the last element -- but that wouldn't help either: insert would be O(1), but delete and head would be O(n).

We would like to have *both* the head *and* the tail available in constant time.

Note: this implementation of queues does NOT CHANGE list structures, it creates a new list structure for each delete or insert operation.

[If you lectured this far, I owe you two glasses of wine -- brd]


The tool we'll use is set!, but we will use it change portions of a list structure instead of changing a value in a location as we have used it so far.

-- Mutable Data Structures

Keep pointers to both ends of the list.

NOW we're going to CHANGE EXISTING LIST STRUCTURES! [Have some more rope…]

REMEMBER:

cons, head, and tail are NOT mutators.

- It's WRONG!

- It will get you in TROUBLE

Just to remind you:

(define x '(1 2 3))

(define y (tail x))

x is STILL (1 2 3)

y is (2 3) the tail of that list.

 

 

List MUTATION or CHANGE is done using set! applied to the appropriate part of a list structure, it has the effect of changing the structure.

(set! (head l) x) changes the head of list l to the object x

(set! (tail l) l') changes the tail of list l to the list l'

x is an object (it may be a list, but that list is then an element of l)

l' is a list, it is the rest of the new list that replaces l

Alternate syntax:

(set-car! L x)

(set-cdr! L L’)

[Note: to RDZ’s glee, there is no set-head!/tail!]

Example:

 

(define x (list 1 2 3))

;; We use list because it always makes new list structure.

;; '(1 2 3) isn't guaranteed to. (eq? ‘(1 2) ‘(1 2)) can be #t

;; Recall the contract of quote!

(define y (cons 4 x))

>>> Draw box-and-pointer diagrams for x and y.

>>> Note that the tail of y is the same structure as x

(set! (head x) 10)

>>> Change head of x to 10

Now, x is (10 2 3).

ALSO y is (4 10 2 3)!

 

** x and y *share structure*

- Changing one of them can change the other one as well.

- This is MASSIVELY confusing.

NOTE: (set! x 10) just changes x, *not* y, because it changes the value stored at x, not part of the structure stored at x (that structure being what is shared with y). [!?!]

Also, when you pass a list to a function, it is not copied (would be too slow). Thus if I call (f x) the value of y can change! [If we copied, Scheme would be call by value, like C]

SO: as with all uses of operations that change things, such as set!, we want to "package up" the use of change, keep there from being unintended shared structures.


Now, here's how to implement a queue with constant time insertions and deletions. [Prelim #2 equivalent of accumulate problem!]

Use a list structure with pointers to the beginning and end of the list.

(define <queue> <list>)

Keep pointers to the beginning and the end of the list:

>>> Draw the box diagram of (1 2 3),

>>> q is a box whose head points to the 1-cell, tail to the 3-cell.

We'll read from the front of the list, and add to the rear.

(define empty-queue?

(lambda (q)

(null? (head q))))

(define make-queue

(lambda()

(cons '() '())))

(define headq

(lambda (q)

(if (empty-queue? q)

(error "Head of empty queue")

(head (head q)))))

Note: (head q) gives the front of the list, (head (head q)) gives the first element of queue

(define insert

(lambda (x q)

(let ((new (cons x '()))

(cond

((empty-queue? q)

(set! (head q) new)

(set! (tail q) new)

q)

(else:

;; make the tail of the last cell point to new

(set! (tail (tail q)) new)

;; make (tail q) be new

(set! (tail q) new)

q)))))

In the empty case, we need to make sure that the head and tail of q point to the *same* cons cell, because there is just one thing in the queue, the new element.

In the non-empty case, add the new element at the rear.

(define delete

(lambda (q)

(cond

((empty-queue? q)

(error "Cant delete from empty queue."))

(else:

;; Make (head q) be (tail (head q))

(set! (head q) (tail (head q)))

q))))

 

[If you lectured this far, I owe you a bottle of wine -- brd]


 

(define fred (make-queue))

fred --> (())

>>> Draw b&p diagram.

(insert 1 fred)

>>> change B&P diagrams on the board.

>>> B&P diagrams: both head and tail of q point to a cons cell (1 . nil)

(headq fred)

---> 1

(insert 2 fred)

>>> Change B&P diagrams on the board.

>>>

(headq fred)

--> 1 still

(delete fred)

>>> Change B&P's so that both the head and tail of fred point to the (2 .

>>> nil)

Note: the remains, the cons cell (1 . [pointer-to-(2.nil)] ) are INACCESSIBLE. Nobody has a pointer to it. It can't be reached by anything the program does. It is GARBAGE

 

It will eventually get GARBAGE COLLECTED, and its space recycled. More on this later in the term...


Summary:

* Stacks - LIFO,

* Queues - FIFO

O(1) implementations of basic operations.

Our O(1) queue implementation uses *mutation* to do this.

 

 

Joke:

A student hands in his PhD thesis. On p 90 he writes: [If you read this far, I owe you a bottle of wine.]. On p 100 he writes [If you read this far, I owe you two bottles of wine.] On p 273 he writes [If you read this far, I owe you a case of wine.] His advisor reads the thesis over the weekend. Handing it back, he smiles smugly and says "You owe me a bottle of wine!"