diff options
author | elioat <elioat@tilde.institute> | 2023-08-23 07:52:19 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2023-08-23 07:52:19 -0400 |
commit | 562a9a52d599d9a05f871404050968a5fd282640 (patch) | |
tree | 7d3305c1252c043bfe246ccc7deff0056aa6b5ab /js/games/nluqo.github.io/~bh/61a-pages/Solutions/week10 | |
parent | 5d012c6c011a9dedf7d0a098e456206244eb5a0f (diff) | |
download | tour-562a9a52d599d9a05f871404050968a5fd282640.tar.gz |
*
Diffstat (limited to 'js/games/nluqo.github.io/~bh/61a-pages/Solutions/week10')
-rw-r--r-- | js/games/nluqo.github.io/~bh/61a-pages/Solutions/week10 | 977 |
1 files changed, 977 insertions, 0 deletions
diff --git a/js/games/nluqo.github.io/~bh/61a-pages/Solutions/week10 b/js/games/nluqo.github.io/~bh/61a-pages/Solutions/week10 new file mode 100644 index 0000000..53e4817 --- /dev/null +++ b/js/games/nluqo.github.io/~bh/61a-pages/Solutions/week10 @@ -0,0 +1,977 @@ +CS 61A -- Week 10 Solutions + + +LAB ASSIGNMENT: + +3.12 append vs. append! + +exp1 is (b); exp2 is (b c d). Append (without the !) makes copies of the +two pairs that are part of the list x. (You can tell because it uses +cons, which is the constructor function that generates a brand new pair.) +Append! does not invoke cons; it mutates the existing pairs to combine +the two argument lists. + + +2. Set! vs. set-cdr! + +There are two ways to think about this, and you should understand both +of them: + +The syntactic explanation -- SET! is a special form; its first argument +must be a symbol, not a compound expression. So anything of the form + (set! (...) ...) +must be an error. + +The semantic explanation -- SET! and SET-CDR! are about two completely +different purposes. SET! is about the bindings of variables in an +environment. SET-CDR! is about pointers within pairs. SET! has nothing +to do with pairs; SET-CDR! has nothing to do with variables. There is +no situation in which they are interchangeable. + +(Note: The book says, correctly, that the two are *equivalent* in the +sense that you can use one to implement the other. But what that means +is that, for example, if we didn't have pairs in our language we could +use the oop techniques we've learned, including local state variables +bound in an environment, to simulate pairs. Conversely, we'll see in +Chapter 4 that we can write a Scheme interpreter, including environments +as an abstract data type, building them out of pairs. But given that +we are using the regular Scheme's built-in pairs and built-in environments, +those have nothing to do with each other.) + + + +3a. Fill in the blanks. + +> (define list1 (list (list 'a) 'b)) +list1 +> (define list2 (list (list 'x) 'y)) +list2 +> (set-cdr! ____________ ______________) +okay +> (set-cdr! ____________ ______________) +okay +> list1 +((a x b) b) +> list2 +((x b) y) + +The key point here is that if we're only allowed these two SET-CDR!s then +we'd better modify list2 first, because the new value for list1 uses the +sublist (x b) that we'll create for list2. + +So it's + +(set-cdr! (car list2) (cdr list1)) + +(set-cdr! (car list1) (car list2)) + + + +3b. Now do (set-car! (cdr list1) (cadr list2)). + +Everything that used to be "b" is now "y" instead: + +> list1 +((a x y) y) +> list2 +((x y) y) + +The reason is that there was only one appearance of the symbol B in +the diagram, namely as the cadr of list1; every appearance of B in the +printed representation of list1 or list2 came from pointers to the +pair (cdr list1). The SET-CAR! only makes one change to one pair, +but three different things point (directly or indirectly) to it. + + + +3.13 make-cycle + +The diagram is + + +----------------+ + | | + V | +---> XX--->XX--->XX---+ + | | | + V V V + a b c + +(last-pair z) will never return, because there is always a non-empty +cdr to look at next. + + + +3.14 Mystery procedure. + +This procedure is REVERSE!, that is to say, it reverses the list +by mutation. After + + (define v (list 'a 'b 'c 'd)) + (define w (mystery v)) + +the value of w is the list (d c b a); the value of v is the list (a) +because v is still bound to the pair whose car is a. (The procedure +does not change the cars of any pairs.) + + + +5a. We want Scheme-2 to accept both the ordinary form + (define x 3) +and the procedure-abbreviation form + (define (square x) (* x x)) +The latter should be treated as if the user had typed + (define square (lambda (x) (* x x))) +The hint says we can use data abstraction to achieve this. + +Here is the existing code that handles DEFINE: + +(define (eval-2 exp env) + (cond ... + ((define-exp? exp) (put (cadr exp) + (eval-2 (caddr exp) env) + env) + 'okay) + ...)) + +We're going to use selectors for the pieces of the DEFINE expression: + +(define (eval-2 exp env) + (cond ... + ((define-exp? exp) (put (DEFINE-VARIABLE exp) + (eval-2 (DEFINE-VALUE exp) env) + env) + 'okay) + ...)) + +To get the original behavior we would define the selectors this way: + +(define define-variable cadr) +(define define-value caddr) + +But instead we'll check to see if the cadr of the expression is a +symbol (so we use the ordinary notation) or a list (abbreviating +a procedure definition): + +(define (define-variable exp) + (if (pair? (cadr exp)) + (caadr exp) ;(define (XXXX params) body) + (cadr exp))) + +(define (define-value exp) + (if (pair? (cadr exp)) + (cons 'lambda + (cons (cdadr exp) ;params + (cddr exp))) ;body + (caddr exp))) + +Writing selectors like this is the sort of situation in which the compositions +like CAADR are helpful. That particular one is (car (cadr exp)), which is the +first element of the second element of the expression. (You should recognize +CADR, CADDR, and CADDDR as selectors for the second, third, and fourth +elements of a list.) The second element of the expression is a list such as +(SQUARE X), so the car of that list is the variable name. + +Since DEFINE-VALUE is supposed to return an expression, we have to construct +a LAMBDA expression, making explicit what this notation abbreviates. + + +5c. In a procedure call, parameters are processed from left to right, +and PUT adds each parameter to the front of the environment. So they +end up in reverse order. Similarly, top-level DEFINEs add things to +the global environment in reverse order. So the sequence of expressions +should be + +Scheme-2: (define b 2) +Scheme-2: (define a 1) +Scheme-2: ((lambda (c b) 'foo) 4 3) + +It doesn't matter what's in the body of the procedure, since we're +interested in the environments rather than in the values returned. + + + +HOMEWORK: + +3.16 incorrect count-pairs + +This procedure would work fine for any list structure that can be expressed +as (quote <anything>). It fails when there are two pointers to the same pair. + +(define a '(1 2 3)) (count-pairs a) --> 3 + +(define b (list 1 2 3)) +(set-car! (cdr b) (cddr b)) (count-pairs b) --> 4 + +(define x (list 1)) +(define y (cons x x)) +(define c (cons y y)) (count-pairs c) --> 7 + +(define d (make-cycle (list 1 2 3))) (count-pairs d) --> infinite loop + +Note from example c that it's not necessary to use mutators to create +a list structure for which this count-pairs fails, but it is necessary +to have a name for a substructure so that you can make two pointers to it. +The name needn't be global, though; I could have said this: + +(define c + (let ((x (list 1))) + (let ((y (cons x x))) + (cons y y) ))) + + +3.17 correct count-pairs + +(define (count-pairs lst) + (let ((pairlist '()) + (count 0)) + (define (mark-pair pair) + (set! pairlist (cons pair pairlist)) + (set! count (+ count 1))) + (define (subcount pair) + (cond ((not (pair? pair)) 'done) + ((memq pair pairlist) 'done) + (else (mark-pair pair) + (subcount (car pair)) + (subcount (cdr pair))))) + (subcount lst) + count)) + +The list structure in pairlist can get very complicated, especially if +the original structure is complicated, but it doesn't matter. The cdrs +of pairlist form a straightforward, non-circular list; the cars may point +to anything, but we don't follow down the deep structure of the cars. We +use memq, which sees if PAIR (a pair) is eq? (NOT equal?) to the car of some +sublist of pairlist. Eq? doesn't care about the contents of a pair; it just +looks to see if the two arguments are the very same pair--the same location +in the computer's memory. + +[Non-experts can stop here and go on to the next problem. The following +optional material is just for experts, for a deeper understanding.] + +It's not necessary to use local state and mutation. That just makes the +problem easier. The reason is that a general list structure isn't a sequence; +it's essentially a binary tree of pairs (with non-pairs as the leaves). So +you have to have some way to have the pairs you encounter in the left branch +still remembered as you traverse the right branch. The easiest way to do +this is to remember all the pairs in a variable that's declared outside the +SUBCOUNT procedure, so it's not local to a particular subtree. + +But another way to do it is to have a more complicated helper procedure +that takes PAIRLIST as an argument, but also sequentializes the traversal by +keeping a list of yet-unvisited nodes, sort of like the breadth-first tree +traversal procedure (although this goes depth-first because TODO is a stack, +not a queue): + +(define (count-pairs lst) + (define (helper pair pairlist count todo) + (if (or (not (pair? pair)) (memq pair pairlist)) ; New pair? + (if (null? todo) ; No. More pairs? + count ; No. Finished. + (helper (car todo) pairlist count (cdr todo))) ; Yes, pop one. + (helper (car pair) (cons pair pairlist) (+ count 1) ; Yes, count it, + (cons (cdr pair) todo)))) ; do car, push cdr + (helper lst '() 0 '())) + +As you're reading this code, keep in mind that all the calls to HELPER +are tail calls, so this is an iterative process, unlike the solution +using mutation, in which the call (SUBCOUNT (CAR PAIR)) isn't a tail call +and so that solution generates a recursive process. + +And after you understand that version, try this one: + +(define (count-pairs lst) + (define (helper pair pairlist count todo) + (if (or (not (pair? pair)) (memq pair pairlist)) ; New pair? + (todo pairlist count) ; No. Continue. + (helper (car pair) (cons pair pairlist) (+ count 1) ; Yes, count it, + (lambda (pairlist count) ; do car, push cdr + (helper (cdr pair) pairlist count todo))))) + (helper lst '() 0 (lambda (pairlist count) count))) + +Here, instead of being a list of waiting pairs, TODO is a procedure that +knows what tasks remain. The name for such a procedure is a "continuation" +because it says how to continue after doing some piece of the problem. +This is an example of "continuation-passing style" (CPS). Since TODO is +tail-called, you can think of it as the target of a goto, if you've used +languages with that feature. + + +3.21 print-queue + +The extra pair used as the head of the queue has as its car an ordinary +list of all the items in the queue, and as its cdr a singleton list of +the last element of the queue. Each of Ben's examples print as a list of +two members; the first member is a list containing all the items in the +queue, and the second member is just the last item in the queue. If you +look at what Ben printed, take its car and you'll get the queue items; +take its cdr and you'll get a list of one member, namely the last queue +item. The only exception is Ben's last example. In that case, the car of +what Ben prints correctly indicates that the queue is empty, but the cdr +still contains the former last item. This is explained by footnote 22 +on page 265, which says that we don't bother updating the rear-ptr when we +delete the last (or any) member of the queue because a null front-ptr is +good enough to tell us the queue is empty. + +It's quite easy to print the sequence of items in the queue: + +(define print-queue front-ptr) + + +3.25 multi-key table + +Several students generalized the message-passing table implementation +from page 271, which is fine, but it's also fine (and a little easier) +to generalize the simpler version of page 270: + +(define (lookup keylist table) + (cond ((not table) #f) + ((null? keylist) (cdr table)) + (else (lookup (cdr keylist) + (assoc (car keylist) (cdr table)))))) + +(define (insert! keylist value table) + (if (null? keylist) + (set-cdr! table value) + (let ((record (assoc (car keylist) (cdr table)))) + (if (not record) + (begin + (set-cdr! table + (cons (list (car keylist)) (cdr table))) + (insert! (cdr keylist) value (cadr table))) + (insert! (cdr keylist) value record))))) + +That solution assumes all the entries are compatible. If you say + (insert! '(a) 'a-value my-table) + (insert! '(a b) 'ab-value my-table) +the second call will fail because it will try to + (assoc 'b (cdr 'a-value)) +and the CDR will cause an error. If you'd like to be able to have +values for both (a) and (a b), the solution is more complicated; +each table entry must contain both a value and a subtable. In the +version above, each association list entry is a pair whose CAR is +a key and whose CDR is *either* a value or a subtable. In the version +below, each association list entry is a pair whose CAR is a key and +whose CDR is *another pair* whose CAR is a value (initially #f) and whose +CDR is a subtable (initially empty). Changes are in CAPITALS below: + +(define (lookup keylist table) + (cond ; *** the clause ((not table) #f) is no longer needed + ((null? keylist) (CAR table)) ; *** + (else (LET ((RECORD (assoc (car keylist) (cdr table)))) + (IF (NOT RECORD) + #F + (lookup (cdr keylist) (CDR RECORD))))))) ; *** + +(define (insert! keylist value table) + (if (null? keylist) + (SET-CAR! table value) ; *** + (let ((record (assoc (car keylist) (cdr table)))) + (if (not record) + (begin + (set-cdr! table + (cons (LIST (CAR keylist) #F) (cdr table))) ; *** + (insert! (cdr keylist) value (CDADR table))) + (insert! (cdr keylist) value (CDR RECORD)))))) ; *** + + +Note: In a sense, this problem can be solved without doing any work at all. +In a problem like + + (lookup '(red blue green) color-table) + +you can think of (red blue green) as a list of three keys, each of which is +a word, or as a single key containing three words! So the original +one-dimensional implementation will accept this as a key. However, for a +large enough table, this would be inefficient because you have to look +through a very long list of length Theta(n^3) instead of three lists each +Theta(n) long. + + + +3.27 Memoization + +Here's what happened when I tried it, with annotations in [brackets]. +In the annotations, (fib n) really means that (memo-fib n) is called! +I just said "fib" to save space. + +> (memo-fib 3) +"CALLED" memo-fib 3 [user calls (fib 3)] + "CALLED" lookup 3 (*table*) + "RETURNED" lookup #f + "CALLED" memo-fib 2 [(fib 3) calls (fib 2)] + "CALLED" lookup 2 (*table*) + "RETURNED" lookup #f + "CALLED" memo-fib 1 [(fib 2) calls (fib 1)] + "CALLED" lookup 1 (*table*) + "RETURNED" lookup #f + "CALLED" insert! 1 1 (*table*) + "RETURNED" insert! ok + "RETURNED" memo-fib 1 [(fib 1) returns 1] + "CALLED" memo-fib 0 [(fib 2) calls (fib 0)] + "CALLED" lookup 0 (*table* (1 . 1)) + "RETURNED" lookup #f + "CALLED" insert! 0 0 (*table* (1 . 1)) + "RETURNED" insert! ok + "RETURNED" memo-fib 0 [(fib 0) returns 0] + "CALLED" insert! 2 1 (*table* (0 . 0) (1 . 1)) + "RETURNED" insert! ok + "RETURNED" memo-fib 1 [(fib 2) returns 1] + "CALLED" memo-fib 1 [(fib 3) calls (fib 1) ****] + "CALLED" lookup 1 (*table* (2 . 1) (0 . 0) (1 . 1)) + "RETURNED" lookup 1 + "RETURNED" memo-fib 1 [(fib 1) returns 1] + "CALLED" insert! 3 2 (*table* (2 . 1) (0 . 0) (1 . 1)) + "RETURNED" insert! ok +"RETURNED" memo-fib 2 [(fib 3) returns 2] +2 + +The line marked **** above is the only call to memo-fib in this example in +which the memoization actually finds a previous value. We are computing +(fib 1) for the second time, so memo-fib finds it in the table. + +In general, calling memo-fib for some larger argument will result in two +recursive calls for each smaller argument value. For example: + + fib 6 ---> fib 5, fib 4 + fib 5 ---> fib 4, fib 3 + fib 4 ---> fib 3, fib 2 + +and so on. (memo-fib 4) is evaluated once directly from (memo-fib 6) and once +from (memo-fib 5). But only one of those actually requires any computation; +the other finds the value in the table. + +This is why memo-fib takes Theta(n) time: it does about 2n recursive calls, +half of which are satisfied by values found in the table. + +If we didn't use memoization, or if we defined memo-fib to be (memoize fib), +we would have had to compute (f 1) twice. In this case there would only be +one duplicated computation, but the number grows exponentially; for (fib 4) +we have to compute (fib 2) twice and (fib 1) three times. + +By the way, notice that if we try (memo-fib 3) a second time from the Scheme +prompt, we get a result immediately: + +> (memo-fib 3) +"CALLED" memo-fib 3 + "CALLED" lookup 3 (*table* (3 . 2) (2 . 1) (0 . 0) (1 . 1)) + "RETURNED" lookup 2 +"RETURNED" memo-fib 2 +2 + + +Scheme-2 set!: This is actually tricky -- I got it wrong the first time +I tried it. The trouble is that the procedure PUT in scheme2.scm, which +was written for use by DEFINE, doesn't modify an existing binding, and +therefore isn't useful for implementing SET!. But it's not a good idea +to change PUT, because that breaks DEFINE. We want a DEFINE in an inner +environment (that is, a DEFINE in a procedure body) to make a new +variable, even if a variable with the same name exists in the global +environment. This is why PUT always adds a new binding, not checking +for an old one. But SET! should *only* modify an existing binding, +not create a new one. + +We change eval-2 like this: + +(define (eval-2 exp env) + (cond ... + ((define-exp? exp) (put (define-variable exp) + (eval-2 (define-value exp) env) + env) + 'okay) + ((SET!-EXP? EXP) (SET-PUT (CADR EXP) + (EVAL-2 (CADDR EXP) ENV) + ENV) + 'OKAY) + ...)) + +Then we define SET-PUT: + +(define (set-put var val env) + (let ((pair (assoc var (cdr env)))) + (if pair + (set-cdr! pair val) + (error "No such variable: " var)))) + + +Scheme-2 bug: This is a complicated diagram, and I'm going to +abbreviate it by not showing the pairs that are inside lambda +expressions. The notation (\x) below means (lambda (x) ...). + + +GLOBAL ENV ----> XX--->XX----------------->XX--------------------->X/ + +----/ ---^ | | | +-^ | + | +--/ V V V ! V + | | *TABLE* XX XX ! XX + | | | \ | \ ! | \ + | | V V V V ! V | + | | g XX--->XX--->X/ h XX--->XX--->X/ ! f | + | | | | | | | | ! | + | | V V | V V | ! | + | | PROC (\z) | PROC (\y) | ! | + | | | | ! | + | +-----------------------------+ | ! | + | +-+ ! | + | | ! | + | | ! | + | V ! | + | env for (f 3)----------> XX--->XX | + | | +-^| | + | V | V | + | *TABLE*| XX | + | | / \ | + | env for (h 4)--------> XX--->XX------------+ V V | + | | | x 3 | + | V V +-----------------+ + | *TABLE* XX V + | / \ XX--->XX--->X/ + | V V | | | + | y 4 PROC (\x) | + +----------------------------------------------------------+ + +The problem is with the vertical arrow made of exclamation points near +the right of the picture. It tells us that the environment created by +the call (f 3) extends the global environment *as it exists at the +time of this procedure call*! So the new environment has a new +binding for X, and the existing binding for F. This is the environment +that procedure H remembers, so when we call (h 4), within the body of H +the bindings of G and H are invisible. + +The whole point of this exercise is to convince you that it's not +good enough to represent an environment as a list of bindings. We +have to represent it as a list of frames, each of which is a list +of bindings. This is how the textbook does it, in week 12. + + +Vector exercises: + +1. VECTOR-APPEND is basically like VECTOR-CONS in the notes, +except that we need two loops, one for each source vector: + +(define (vector-append vec1 vec2) + (define (loop newvec vec n i) + (if (>= n 0) + (begin (vector-set! newvec i (vector-ref vec n)) + (loop newvec vec (- n 1) (- i 1))))) + (let ((result (make-vector (+ (vector-length vec1) (vector-length vec2))))) + (loop result vec1 (- (vector-length vec1) 1) (- (vector-length vec1) 1)) + (loop result vec2 (- (vector-length vec2) 1) (- (vector-length result) 1)) + result)) + + +2. VECTOR-FILTER is tough because we have to do the filtering twice, +first to get the length of the desired result vector, then again to +fill in the slots: + +(define (vector-filter pred vec) + (define (get-length n) + (cond ((< n 0) 0) + ((pred (vector-ref vec n)) + (+ 1 (get-length (- n 1)))) + (else (get-length (- n 1))))) + (define (loop newvec n i) + (cond ((< n 0) newvec) + ((pred (vector-ref vec n)) + (vector-set! newvec i (vector-ref vec n)) + (loop newvec (- n 1) (- i 1))) + (else (loop newvec (- n 1) i)))) + (let ((newlen (get-length (- (vector-length vec) 1)))) + (loop (make-vector newlen) (- (vector-length vec) 1) (- newlen 1)))) + + +3. Bubble sort is notorious because nobody ever uses it in practice, +because it's slow, but it always appears in programming course +exercises, because the operation of swapping two neighboring elements +is relatively easy to write. + +(a) Here's the program: + +(define (bubble-sort! vec) + (let ((len (vector-length vec))) + (define (loop n) + (define (bubble k) + (if (= k n) + 'one-pass-done + (let ((left (vector-ref vec (- k 1))) + (right (vector-ref vec k))) + (if (> left right) + (begin (vector-set! vec (- k 1) right) + (vector-set! vec k left))) + (bubble (+ k 1))))) + (if (< n 2) + vec + (begin (bubble 1) + (loop (- n 1))))) + (loop len))) + +(b) As the hint says, we start by proving that after calling (bubble 1) inside +the call to (loop n), element number n-1 is greater than any element to its +left. + +(Bubble 1) reorders elements 0 and 1 so that vec[0] is less than or equal to +vec[1] (I'm using C/Java notation for elements of vectors), then reorders +elements 1 (the *new* element 1, which is the larger of the original first +two elements) and element 2 so that vec[1] is less than or equal to vec[2]. +It continues, but let's stop here for the moment. After those two steps, +the new vec[2] is at least as large as vec[1]. But the intermediate value +of vec[1] was larger than the new vec[0], so vec[2] must be the largest. + +This might be clearer with a chart. There are six possible original +orderings of the first three elements; here they are, with the ordering +after the 0/1 swap and the ordering after the 1/2 swap. (To make the +table narrower, I've renamed VEC as V. Also, I'm calling the three +values 0, 1, and 2; it doesn't matter what the actual values are, as +long as they are in the same order as a particular line in the table.) + +original after 0/1 swap after 1/2 swap +-------------- -------------- -------------- +v[0] v[1] v[2] v[0] v[1] v[2] v[0] v[1] v[2] +---- ---- ---- ---- ---- ---- ---- ---- ---- + + 0 1 2 0 1 2 0 1 2 + 0 2 1 0 2 1 0 1 2 + 1 0 2 0 1 2 0 1 2 + 1 2 0 1 2 0 1 0 2 + 2 0 1 0 2 1 0 1 2 + 2 1 0 1 2 0 1 0 2 + +After the first swap, we have v[0] <= v[1]. After the second swap, +we have v[1] <= v[2]. But note that there is no guarantee about the +order of the final v[0] and v[1]! All that's guaranteed is that +the largest of the three values is now in v[2]. + +Similarly, after the 2/3 swap, we know that vec[3] is the largest +of the first four values, because either the original vec[3] was +already largest, in which case there is no swap, or the value of +vec[2] just before the 2/3 swap is the largest of the original +vec[0] through vec[2], so it's the largest of vec[0] through vec[3] +and will rightly end up as the new vec[3]. + +Subprocedure BUBBLE calls itself recursively until k=n, which means +that vec[n-1] is the largest of the first n elements. QED. + +Now, if that's true about a single pass, then the first pass +"bubbles" the largest number to the end of the vector (this is why +it's called bubble sort), and then we call LOOP recursively to +sort the remaining elements. The second pass gets vec[len-2] to +be the largest of the first len-1 elements, etc. After LEN passes, +the entire vector is sorted. + +This was a handwavy proof. To make it rigorous, it'd be done by +mathematical induction -- two inductions, one for the swaps in a +single pass, and one for the multiple passes. + +(c) It's Theta(N^2), for the usual reason: N passes, each of which +takes time Theta(N). + + +Extra for experts +----------------- + +3.19 constant-space cycle? predicate + +Just to make sure you understand the issue, let me first do 3.18, which +asks us to write cycle? without imposing a constant-space requirement. +It's a lot like the correct version of count-pairs; it has to keep track +of which pairs we've seen already. + +(define (cycle? lst) + (define (iter lst pairlist) + (cond ((not (pair? lst)) #f) + ((memq lst pairlist) #t) + (else (iter (cdr lst) (cons lst pairlist))))) + (iter lst '())) + +This is simpler than count-pairs because we only have to chase down pointers +in one direction (the cdr) instead of two, so it can be done iteratively. +I check (not (pair? lst)) rather than (null? lst) so that the program won't +blow up on a list structure like (a . b) by trying to take the cdr of b. + +The trouble is that the list pairlist will grow to be the same size as the +argument list, if the latter doesn't contain a cycle. What we need is to +find a way to keep the auxiliary list of already-seen pairs without using +up any extra space. + +Here is the very cleverest possible solution: + +(define (cycle? lst) + (define (iter fast slow) + (cond ((not (pair? fast)) #f) + ((not (pair? (cdr fast))) #f) + ((eq? fast slow) #t) + (else (iter (cddr fast) (cdr slow))) )) + (if (not (pair? lst)) + #f + (iter (cdr lst) lst) )) + +This solution runs in Theta(1) space and Theta(n) time. We send two +pointers CDRing down the list at different speeds. If the list is not a +cycle, the faster one will eventually hit the end of the list, and we'll +return false. If the list is a cycle, the faster one will eventually +overtake the slower one, and we'll return true. (You may think that this +will only work for odd-length cycles, or only for even-length cycles, +because in the opposite case the fast pointer will leapfrog over the slow +one, but if that happens the two pointers will become equal on the next +iteration.) + +If you didn't come up with this solution, don't be upset; most folks don't. +This is a classic problem, and struggling with it is a sort of initiation +ritual in the Lisp community. Here's a less clever solution that runs in +Theta(1) space but needs Theta(n^2) time. It is like the first solution, the +one that uses an auxiliary pairlist, but the clever idea is to use the +argument list itself as the pairlist. This can be done by clobbering its cdr +pointers temporarily. It's important to make sure we put the list back +together again before we leave! The idea is that at any time we will have +looked at some initial sublist of the argument, and we'll know for sure that +that part is cycle-free. We keep the tested part and the untested part +separate by changing the cdr of the last tested pair to the empty list, +remembering the old cdr in the single extra pointer variable that this +algorithm requires. + +(define (cycle? lst) + (define (subq? x list) + (cond ((null? list) #f) + ((eq? x list) #t) + (else (subq? x (cdr list))))) + (define (iter lst pairlist pairlist-tail) + (cond ((not (pair? lst)) + (set-cdr! pairlist-tail lst) + #f) + ((subq? lst pairlist) + (set-cdr! pairlist-tail lst) + #t) + (else + (let ((oldcdr (cdr lst))) + (set-cdr! pairlist-tail lst) + (set-cdr! lst '()) + (iter oldcdr pairlist lst) )))) + (cond ((null? lst) #f) + (else (let ((oldcdr (cdr lst))) + (set-cdr! lst '()) + (iter oldcdr lst lst))))) + +Be wary of computing (cdr lst) before you've tested whether or not lst is +empty. + + +3.23 Double-ended queue + +The only tricky part here is rear-delete-deque!. All the other deque +operations can be performed in Theta(1) time using exactly the same structure +used for the queue in 3.3.2. The trouble with rear-delete is that in order +to know where the new rear is, we have to be able to find the next-to-last +member of the queue. In the 3.3.2 queue, the only way to do that is to cdr +down from the front, which takes Theta(n) time for an n-item queue. To +avoid that, each item in the queue must point not only to the next item but +also to the previous item: + ++---+---+ +| | | --------------------------------------------+ ++-|-+---+ | + | | + V V ++---+---+ +---+---+ +---+---+ +---+--/+ +| | | --------->| | | --------->| | | --------->| | | / | ++-|-+---+ +-|-+---+ +-|-+---+ +-|-+/--+ + | ^ | ^ | ^ | + | +-----+ | +-----+ | +-----+ | + V | V | V | V ++--/+---+ | +---+---+ | +---+---+ | +---+---+ +| / | | | +------ | | | +------ | | | +------ | | | ++/--+-|-+ +---+-|-+ +---+-|-+ +---+-|-+ + | | | | + V V V V + a b c d + + +Whew! The first pair, at the top of the diagram, is the deque header, just +like the queue header in 3.3.2. The second row of four pairs is a regular +list representing the deque entries, again just like 3.3.2. But instead of +each car in the second row pointing to a queue item, each car in this +second row points to another pair, whose car points to the previous element +on the second row and whose cdr points to the actual item. + +;; data abstractions for deque members + +;; we use front-ptr, rear-ptr, set-front-ptr!, and set-rear-ptr! from p. 263 + +(define deque-item cdar) +(define deque-fwd-ptr cdr) +(define deque-back-ptr caar) +(define set-deque-fwd-ptr! set-cdr!) +(define (set-deque-back-ptr! member new-ptr) + (set-car! (car member) new-ptr)) + +;; Now the things we were asked to do: + +(define (make-deque) (cons '() '())) + +(define (empty-deque? deque) (null? (front-ptr deque))) + +(define (front-deque deque) + (if (empty-deque? deque) + (error "front-deque called with empty queue") + (deque-item (front-ptr deque)))) + +(define (rear-deque deque) + (if (empty-deque? deque) + (error "rear-deque called with empty queue") + (deque-item (rear-ptr deque)))) + +(define (front-insert-deque! deque item) + (let ((new-member (list (cons '() item)))) + (cond ((empty-deque? deque) + (set-front-ptr! deque new-member) + (set-rear-ptr! deque new-member) + "done") + (else + (set-deque-fwd-ptr! new-member (front-ptr deque)) + (set-deque-back-ptr! (front-ptr deque) new-member) + (set-front-ptr! deque new-member) + "done")))) + +(define (rear-insert-deque! deque item) + (let ((new-member (list (cons '() item)))) + (cond ((empty-deque? deque) + (set-front-ptr! deque new-member) + (set-rear-ptr! deque new-member) + "done") + (else + (set-deque-back-ptr! new-member (rear-ptr deque)) + (set-deque-fwd-ptr! (rear-ptr deque) new-member) + (set-rear-ptr! deque new-member) + "done")))) + +(define (front-delete-deque! deque) + (cond ((empty-deque? deque) + (error "front-delete-deque! called with empty queue")) + ((null? (deque-fwd-ptr (front-ptr deque))) + (set-front-ptr! deque '()) + (set-rear-ptr! deque '()) + "done") + (else + (set-deque-back-ptr! (deque-fwd-ptr (front-ptr deque)) '()) + (set-front-ptr! deque (deque-fwd-ptr (front-ptr deque))) + "done"))) + +(define (rear-delete-deque! deque) + (cond ((empty-deque? deque) + (error "rear-delete-deque! called with empty queue")) + ((null? (deque-back-ptr (rear-ptr deque))) + (set-front-ptr! deque '()) + (set-rear-ptr! deque '()) + "done") + (else + (set-deque-fwd-ptr! (deque-back-ptr (rear-ptr deque)) '()) + (set-rear-ptr! deque (deque-back-ptr (rear-ptr deque))) + "done"))) + +I could also have gotten away with leaving garbage in the rear-ptr of +an emptied deque, but the ugliness involved outweighs the slight time +saving to my taste. Notice an interesting property of the use of data +abstraction here: at the implementation level, set-deque-back-ptr! and +set-deque-fwd-ptr! are rather different, but once that difference is +abstracted away, rear-delete-deque! is exactly like front-delete-deque! +and ditto for the two insert procedures. + +The reason these procedures return "done" instead of returning deque, +like the single-end queue procedures in the book, is that the deque is a +circular list structure, so if we tried to print it we'd get in trouble. +We should probably invent print-deque: + +(define (print-deque deque) + (define (sub member) + (if (null? member) + '() + (cons (deque-item member) + (sub (deque-fwd-ptr member))))) + (sub (front-ptr deque))) + +But I'd say it's a waste of time to cons up this printable list every time +we insert or delete something. + + +2. cxr-name + +This is a harder problem than its inverse function cxr-function! We are +given a function as a black box, not knowing how it was defined; the only +way we can get any information about it is to invoke it on a cleverly +chosen argument. + +We need three ideas here. The first one is this: Suppose we knew that we +were given either CAR or CDR as the argument. We could determine which +of them by applying the mystery function to a pair with the word CAR as its +car, and the word CDR as its cdr: + +(define (simple-cxr-name fn) + (fn '(car . cdr))) + +You might think to generalize this by building a sort of binary tree with +function names at the leaves: + +(define (two-level-cxr-name fn) + (fn '((caar . cdar) . (cadr . cddr)))) + +But there are two problems with this approach. First, note that this +version *doesn't* work for CAR or CDR, only for functions with exactly +two CARs or CDRs composed. Second, the argument function might be a +composition of *any* number of CARs and CDRs, so we'd need an infinitely +deep tree. + +So the second idea we need is a way to attack the mystery function one +component at a time. We'd like the first CAR or CDR applied to our argument +(that's the rightmost one, don't forget) to be the only one that affects the +result; once that first choice has been made, any CARs or CDRs applied to the +result shouldn't matter. The clever idea is to make a pair whose CAR and CDR +both point to itself! So any composition of CARs and CDRs of this pair will +still just be the same pair. + +Actually we'll make two of these pairs, one for the CAR of our argument pair +and one for the CDR: + +(define car-pair (cons '() '())) +(set-car! car-pair car-pair) +(set-cdr! car-pair car-pair) + +(define cdr-pair (cons '() '())) +(set-car! cdr-pair cdr-pair) +(set-cdr! cdr-pair cdr-pair) + +(define magic-argument (cons car-pair cdr-pair)) + +(define (rightmost-part fn) + (if (eq? (fn magic-argument) car-pair) + 'car + 'cdr)) + +It's crucial that we're using EQ? rather than EQUAL? here, since car-pair +and cdr-pair are infinite (circular) lists. + +Okay, we know the rightmost component. How do we get all but the rightmost +component? (Given that, we can recursively find the rightmost part of that, +etc.) Our third clever idea is a more-magic argument that will give us +magic-argument whether we take its car or its cdr: + +(define more-magic-arg (cons magic-argument magic-argument)) + +(define (next-to-rightmost-part fn) + (if (eq? (fn more-magic-arg) car-pair) + 'car + 'cdr)) + +We're going to end up constructing a ladder of pairs whose car and cdr are +both the next pair down the ladder. We also need a base case; if we apply +fn to magic-argument and get magic-argument itself, rather than car-pair +or cdr-pair, we've run out of composed CAR/CDR functions. + +Here's how it all fits together: + +(define (cxr-name fn) + (word 'c (cxr-name-help fn magic-argument) 'r)) + +(define (cxr-name-help fn arg) + (let ((result (fn arg))) + (cond ((eq? result car-pair) + (word (cxr-name-help fn (cons arg arg)) 'a)) + ((eq? result cdr-pair) + (word (cxr-name-help fn (cons arg arg)) 'd)) + (else "")))) ; empty word if result is magic-argument |