Today: Stacks, Queues, and Mutable Data Structures. > 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. * You can put new things on top of it * You can get at the top thing on the stack. Often called LIFO -- Last In First Out (Reverses order) * Like a stack of trays in a cafeteria * Bottom one gets moldy 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 = head top = tail empty? = null? This implementation is quite efficient: * The operations all take O(1) time, independent of the size of the stack. 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: (method ((thing <object>) (q <list>)) (append q (list thing))) which is O(n) time. We have to walk all the way down the list. 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: ths 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. * When you insert something, add it to the list * When you delete something, physically remove it from the list NOW we're going to CHANGE EXISTING LIST STRUCTURES! REMEMBER: cons, head, and tail are NOT mutators. * They *don't* change any data structures * Though cons *does* allocate new memory. * If you were thinking of them as changing things, STOP! - It's WRONG! - It will get you in TROUBLE Just to remind you: (define (x <list>) '(1 2 3)) (define (y <list>) (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 element x (set! (tail l) l') changes the tail of list l to the list l' x is an element (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 Example: (define (x <list>) (list 1 2 3)) ;; We use list because it always makes new list structure. ;; '(1 2 3) isn't guaranteed to. (define (y <list>) (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). [!?!] 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. 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? <method>) (method ((q <queue>)) (null? (head q)))) (define (make-queue <method>) (method () (cons '() '()))) (define (headq <method>) (method ((q <queue>)) (if (empty-queue? q) (error "Head of empty queue") (head (head q))))) Note: (head q) gives the front of the list, (caar q) gives the first element of queue (define (insert <method>) (method ((x <object>) (q <queue>)) (bind (((new <list>) (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 <method>) (method ((q <queue>)) (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 <queue>) (make-queue)) fred --> (()) >>> Draw b&p diagram. (insert 1 foo) >>> change B&P diagrams on the board. >>> B&P diagrams: both head and tail of q point to a cons cell (1 . nil) (headq foo) ---> 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.] Her advisor reads the thesis over the weekend. Handing it back, he smiles smugly and says "You owe me a bottle of wine!" ----------------------------------------------------------------------