#lang racket |
|
; Often it makes sense to only evaluate a term once and |
; then use that value where needed |
|
(+ (* 3 4) (- (* 3 4) (/ 1 (* 3 4)))) |
|
; replace each (* 3 4) by variable p which is pre-computed |
(define p (* 3 4)) |
(+ p (- p (/ 1 p))) |
|
; wrap the expression in a closure that captures the p |
; and apply it to the result of computing the term |
( (lambda (p) (+ p (- p (/ 1 p))) ) |
(* 3 4) |
) |
|
; you can do this as much as you want |
( (lambda (t1 t2 t3) |
(if (< t1 t2) (* t1 t3) (* t2 t3)) |
) |
|
(+ 1 3) |
(- 4 6) |
7 |
) |
|
|
; in unrestricted Scheme, use the let construction, which places |
; the computation of the value beside the variable it is assigned to |
(let ( (p (* 3 4)) ) |
(+ p (- p (/ 1 p))) |
) |
|
(let ( |
(t1 (+ 1 3)) |
(t2 (- 4 6)) |
(t3 7) |
) |
(if (< t1 t2) (* t1 t3) (* t2 t3)) |
) |
Fiannly, first correctness deal with correctness, then worry about efficiency! If compute time or space is an issue, consider how to restructure the algorithm, say to make it tail-recrusive, or reduce redundant computation.
#lang racket |
|
; find position of max in a list |
; helper function simply passes along all the state that is needed for |
; the computation. |
|
; note the tail recursion, so the stack is never more than constant size |
|
; Pre: L is a non-empty list of numbers |
; Post: returns position of first maximum in list, 0-origin indexing |
(define lmax-index |
(lambda (L) |
(lmax-index-r (first L) 0 1 (rest L) ) |
) |
) |
|
; lmax-index recursor |
; cur-max - value of the current maximum |
; cur-index - position of the current maximum |
; L-index - position of the first element of the remaining list |
; L - remaining list of numbers to be processed |
|
(define lmax-index-r |
( lambda (cur-max cur-index L-index L) |
(if (null? L) |
cur-index |
(if (>= cur-max (first L)) |
; max still good (it is at least as big as the first element) |
(lmax-index-r cur-max cur-index (+ 1 L-index) (rest L)) |
; first is new max |
(lmax-index-r (first L) L-index (+ 1 L-index) (rest L)) |
) |
) |
) |
) |
|
(lmax-index '(1 2 3 4)) |
; NOTE: what happens if the >= test above is changed to a > test? |
|
; Now create a function that returns a list of the positions of |
; all the maximums in L. |
; This would work for an empty list, since the list of positions |
; would be empty |
#lang racket |
|
|
; If X = (x_0 x_1 ... x_n) and Y = (y_0 y_1 ... y_m) then |
; (fmap2lcm fn X Y) |
; is the list (r_0 r_1 ... r_l) where where the element r_i |
; in position i is (fn x_(i mod n) y_(i mod m)) |
; and l = lcm(m, n) where lcm - least common multiple. |
; |
; In other words the map is performed by reusing the lists X and Y until |
; they both end at the same time. |
|
; Example: (fmap2lcm list '(1 2) '(3 4 5)) is |
; ((1 3) (2 4) (1 5) (2 3) (1 4) (2 5)) |
|
; This is a very ugly implementation |
; We have a seriously nested set of if-statments, and are dragging the |
; original input lists through the recursion. |
|
(define fmap2lcm-r |
(lambda (fn X-cur Y-cur X-orig Y-orig) |
(if (and (null? X-cur) (null? Y-cur)) |
'() |
(if (null? X-cur) |
(fmap2lcm-r fn X-orig Y-cur X-orig Y-orig) |
(if (null? Y-cur) |
(fmap2lcm-r fn X-cur Y-orig X-orig Y-orig) |
(cons (fn (first X-cur) (first Y-cur)) |
(fmap2lcm-r fn (rest X-cur) (rest Y-cur) X-orig Y-orig) |
) |
))))) |
|
(define fmap2lcm |
(lambda (fn X Y) |
; handle special boundary case of empty input lists |
(if (or (null? X) (null? Y)) '() (fmap2lcm-r fn X Y X Y))) |
#lang racket |
|
|
; If X = (x_0 x_1 ... x_n) and Y = (y_0 y_1 ... y_m) then |
; (fmap2lcm fn X Y) |
; is the list (r_0 r_1 ... r_l) where where the element r_i |
; in position i is (fn x_(i mod n) y_(i mod m)) |
; and l = lcm(m, n) where lcm - least common multiple. |
; |
; In other words the map is performed by reusing the lists X and Y until |
; they both end at the same time. |
|
; Example: (fmap2lcm list '(1 2) '(3 4 5)) is |
; ((1 3) (2 4) (1 5) (2 3) (1 4) (2 5)) |
|
; Cleanup: |
; Since the agorithm is more a sequence of guarded commands instead of a |
; decision tree, we replace the nested if-statements with a cond-statement |
; |
|
(define fmap2lcm-r |
(lambda (fn X-cur Y-cur X-orig Y-orig) |
(cond |
; both lists done at same time - have reached lcm |
[ (and (null? X-cur) (null? Y-cur)) '() ] |
|
; X runs out first, recycle |
[ (null? X-cur) (fmap2lcm-r fn X-orig Y-cur X-orig Y-orig) ] |
|
; Y runs out first, recycle |
[ (null? Y-cur) (fmap2lcm-r fn X-cur Y-orig X-orig Y-orig) ] |
|
; process first of both and recurse. |
[ else (cons |
(fn (first X-cur) (first Y-cur)) |
(fmap2lcm-r fn (rest X-cur) (rest Y-cur) X-orig Y-orig)) ] |
))) |
|
; We are still dragging the original input lists through the recursion. |
|
(define fmap2lcm |
(lambda (fn X Y) |
; handle special boundary case of empty input lists |
(if (or (null? X) (null? Y)) '() (fmap2lcm-r fn X Y X Y)))) |
#lang racket |
|
; If X = (x_0 x_1 ... x_n) and Y = (y_0 y_1 ... y_m) then |
; (fmap2lcm fn X Y) |
; is the list (r_0 r_1 ... r_l) where where the element r_i |
; in position i is (fn x_(i mod n) y_(i mod m)) |
; and l = lcm(m, n) where lcm - least common multiple. |
; |
; In other words the map is performed by reusing the lists X and Y until |
; they both end at the same time. |
|
; Example: (fmap2lcm list '(1 2) '(3 4 5)) is |
; ((1 3) (2 4) (1 5) (2 3) (1 4) (2 5)) |
|
; Cleanup: |
; We move the definition of the recursor function fmap2lcm-r inside the main |
; function. This means that the recursor has access to the original X and Y |
; lists, so they do not have to be passed down the recursion. |
|
(define fmap2lcm |
(lambda (fn X Y) |
|
(define fmap2lcm-r |
(lambda (fn X-cur Y-cur) |
(cond |
; both lists done at same time - have reached lcm |
[ (and (null? X-cur) (null? Y-cur)) '() ] |
|
; X runs out first, recycle |
[ (null? X-cur) (fmap2lcm-r fn X Y-cur) ] |
|
; Y runs out first, recycle |
[ (null? Y-cur) (fmap2lcm-r fn X-cur Y) ] |
|
; process first of both and recurse. |
[ else (cons |
(fn (first X-cur) (first Y-cur)) |
(fmap2lcm-r fn (rest X-cur) (rest Y-cur) )) ] |
))) |
|
; handle special boundary case of empty input lists |
(if (or (null? X) (null? Y)) '() (fmap2lcm-r fn X Y)) |
)) |
#lang racket |
|
; If X = (x_0 x_1 ... x_n) and Y = (y_0 y_1 ... y_m) then |
; (fmap2lcm fn X Y) |
; is the list (r_0 r_1 ... r_l) where where the element r_i |
; in position i is (fn x_(i mod n) y_(i mod m)) |
; and l = lcm(m, n) where lcm - least common multiple. |
; |
; In other words the map is performed by reusing the lists X and Y until |
; they both end at the same time. |
|
; Example: (fmap2lcm list '(1 2) '(3 4 5)) is |
; ((1 3) (2 4) (1 5) (2 3) (1 4) (2 5)) |
|
; Cleanup: |
; we make a final cleanup by observing that we can have just one recursive |
; call point if we reset the list that ran out. Then we can eliminate the |
; multiple cases in the cond and restore the if. |
|
(define fmap2lcm |
(lambda (fn X Y) |
|
; recursor |
(define fmap2lcm-r |
(lambda (fn X-cur Y-cur) |
( let ( |
; wrap X-cur and Y-cur if they are finished |
[ X-r (if (null? X-cur) X X-cur) ] |
[ Y-r (if (null? Y-cur) Y Y-cur) ] |
) |
|
(if (and (null? X-cur) (null? Y-cur)) |
; when both empty have hit the lcm |
'() |
|
; otherwise process one more element and recurse |
(cons |
(fn (first X-r) (first Y-r)) |
(fmap2lcm-r fn (rest X-r) (rest Y-r) )) |
) |
))) ; end of fmap2lcm-r |
|
; handle special boundary case of empty input lists |
(if (or (null? X) (null? Y)) '() (fmap2lcm-r fn X Y)) |
)) |
|
#lang racket |
|
; tail recursion is very cheap, structure your code to use it when |
; possible. You may have to introduce a helper function |
|
; notice how the stack never gets deeper than two calls |
|
(define lreverse-acc |
(lambda (L) (lreverse-r L '())) |
) |
|
; the recursor helper function accumulates the final result in out-list |
; out-list is called an accumulator |
|
; notice how the stack never gets deeper than two calls |
|
(define lreverse-r |
(lambda (in-list out-list) |
(if (null? in-list) |
out-list |
(lreverse-r (rest in-list) (cons (first in-list) out-list)) |
) |
) |
) |
|
(lreverse-r '() '()) |
(lreverse-r '(1) '()) |
(lreverse-r '(1 2 3 4) '()) |
(lreverse-acc '(1 2 3 4)) |
#lang racket |
|
; If X = (x_0 x_1 ... x_n) and Y = (y_0 y_1 ... y_m) then |
; (fmap2lcm fn X Y) |
; is the list (r_0 r_1 ... r_l) where where the element r_i |
; in position i is (fn x_(i mod n) y_(i mod m)) |
; and l = lcm(m, n) where lcm - least common multiple. |
; |
; In other words the map is performed by reusing the lists X and Y until |
; they both end at the same time. |
|
; Example: (fmap2lcm list '(1 2) '(3 4 5)) is |
; ((1 3) (2 4) (1 5) (2 3) (1 4) (2 5)) |
|
; Cleanup: |
; This is not so much a cleanup as an optimization to show to use accumulators |
; to introduce tail-recursion |
|
(define fmap2lcm |
(lambda (fn X Y) |
|
; recursor |
(define fmap2lcm-r |
(lambda (fn X-cur Y-cur result) |
( let ( |
; wrap X-cur and Y-cur if they are finished |
[ X-r (if (null? X-cur) X X-cur) ] |
[ Y-r (if (null? Y-cur) Y Y-cur) ] |
) |
|
(if (and (null? X-cur) (null? Y-cur)) |
; when both empty have hit the lcm, finish by reversing the |
; result accumulator |
(reverse result) |
|
; otherwise process one more element and recurse |
(fmap2lcm-r fn (rest X-r) (rest Y-r) |
; add the element to the accumulator, and then recurse |
(cons |
(fn (first X-r) (first Y-r)) |
result)) |
) |
|
) |
)) ; end of fmap2lcm-r |
|
; handle special boundary case of empty input lists |
; result accumulator needs to be reversed |
(if (or (null? X) (null? Y)) '() |
(fmap2lcm-r fn X Y '() ) ) |
)) |
|
#lang racket |
|
; cons and rest let you push onto and pop off of the head of a list. |
; these are constant time operations. |
|
; suppose you want to push onto and pop off of the tail of a list |
; what are the time and space requirements for this? |
|
; get the element at the end of list L if L is non-empty |
; this function uses constant stack space because rtop is |
; in tail recursive position |
|
(define rtop |
(lambda (L) |
(if (null? (rest L)) |
(first L) |
(rtop (rest L)) |
) |
) |
) |
|
(rtop '(1 2 3 4)) |
|
; push an element e onto the end of list L |
; this implementation causes the call stack to grow, why? |
; because the result of the recursion is used in constructing |
; the final return value. |
|
(define rpush |
(lambda (L e) |
(if (null? L) |
(cons e '()) |
(cons (first L) (rpush (rest L) e)) |
) |
) |
) |
|
|
(rpush '() '()) |
(rpush '(()) '()) |
(rpush '(1) '()) |
(rpush '(1 2 3 4) '()) |
(rpush '(1 2 3 4) '(5 6)) |
(rpush '(1 2 3 4) 5) |
|
|
|
; pop an element e off the end of list L |
; this implementation causes the call stack to grow, why? |
(define rpop |
(lambda (L) |
(if (null? (rest L)) |
'() |
(cons (first L) (rpop (rest L))) |
) |
) |
) |
|
(rpop '(1 2 3 4)) |
|
; observing that (rpop L) = (lreverse (rest (lreverse L))) |
; and |
; that (rpush e L) = (lreverse (cons e (lreverse L))) |
; gives a constant stack size algorithm |
|
; are there tail recursive versions of rpush and rpop simular to lreverse? |
#lang racket |
|
; given an associative operator op, and |
; a list (e1 e2 e3 ... en) |
; compute the list |
; e1, e1 op e2, e1 op e2 op e3, ..., e1 op e2 op ... op en |
|
|
; what does this function construct as it recurses? |
; this function is not tail recursive. Why? |
|
(define prefix-list |
(lambda (op L) |
(if (null? (rest L)) |
L |
(cons (first L) |
(map (lambda (x)(op (first L) x) ) |
(prefix-list op (rest L)) |
) |
) |
) |
) |
) |
|
;(prefix-list + '(1)) |
;(prefix-list + '(1 2 3 4 5)) |
|
A less clever and more transparent version using the accumulator-todo idiom, but uses the expensive
#lang racket |
(define (prefix-list op L) |
; the prefixes are stacked so need to be reversed to get into |
; proper order |
(reverse (foldl (lambda (e p) |
(if (null? p) |
; base case is single element e from head of |
; list remaining to be processed |
(list e) |
; p is a stack of prefixes, most recent on top |
; so e is added on right of top of stack, and |
; then pushed on |
|
(cons (op (first p) e) p) )) |
|
; starting case of foldl, list to process |
'() L)) |
) |
|
(prefix-list + '(0 1 2 3 4 5)) |