EBoard 32: Merge sort (and assorted other material)

This class will be recorded! Its use is limited to members of the class. Please do not share with others.

Approximate overview

  • General administrative stuff [~10 min]
  • Q&A [~10 min]
  • Quiz [~10 min]
  • Memory Models [~10 min]
  • Largest in List [~10 min]
  • Fun with trees [~10 min]
  • Merge sort lab [whatever’s left]

Administrative stuff

General Notes

  • Attendance in Wednesday’s class is optional; Sam will answer questions before you start the SoLA.
  • Attendance in Thursday’s class is expected.
  • Additional office hours (email or DM to reserve times)
    • Today, 2:30-4:00 (2:30-3:00 and 4:00-4:30 claimed, OH claimed)
    • Wednesday, 2:30-4:30 plus 10:00-11:00 OH
    • Thursday, 2:30-4:30 plus 10:00-11:00 OH
    • Those in other time zones can request evening hours.
  • I’ll also continue to hang out on Teams and email when I can.
  • I have added tree procedures to the csc151 library. Please update your library asap. You will need the updated library for the SoLA.

Notes on SoLA 4

  • Sample problems released.
    • Some are likely longer or more complicated that those on the SoLA.
    • There are likely to be some similarities.
  • Everyone gets 60 minutes for every problem. (But you should try to finish in 20.)
  • There will be repeats of all problems except (a) Primitive Types (from Group 1) and (b) Collaboration (also from Group 1). [If you still need one of those two, let me know.]
  • I will do my best to grade it as speedily as I can so that you can decide whether or not to take the last SoLA (and what to study for).

Tutoring reminders

  • Please use our tutors! They like to work.
  • Evening tutoring will be available
    • 8-10 p.m. Sundays through Thursdays
    • 3-5 p.m. Sundays
    • In the “Drop in tutoring” channel on the CS team.
    • Sam will generally not be available during these times.

Upcoming activities

Info:

  • Attend (or watch recording within a day or so) and send a one-paragraph reflection asap afterwards.
  • Only those activities I list count.
  • But you can suggest others.
  • Links are in the Announcements channel.
  • Unless otherwise specified, these each earn one token.

Activities

  • 5:00 p.m. Thursday, 17 December 2020: Thinking ahead until summer (tentative)
  • 12:00 m. Monday, 21 December 2020: CS Table

Upcoming work

Mini Projects

  • Redo of Mini-project 5 due Sunday, December 20 at 10:30 p.m. CST
  • Re-Redo of any other mini projects (costs one token) due Tuesday, December 22 at 11:59 p.m. CST

Readings

  • Readings for Thursday
    • None
  • And that’s it!

Quizzes

  • Quiz Today: Sorting
  • Quiz Thursday: A surprise [OUR LAST QUIZ]
  • (Change made because of yesterday’s quiz isses)

Other

  • SoLA 4 Wednesday (note different day of week)

Q&A

SoLA 4

Do we have to know merge sort for SoLA 4?

No, but it may be on SoLA 5. (Probably an approximate trace.)

Administrative stuff

Merge Sort

Why is new-merge-sort n*log_2(n) rather than n+log_2(n)?

Merging all the elements together to make it shorter takes between n/2 and n steps for n elements.

Once with lists of length 1

’((79) (56) (84) (32) (49) (93) (38) (73) (38) (49) (41) (26) (80) (86) (10) (21))

Once with lists of length 2

’((56 79) (32 84) (49 93) (38 73) (38 49) (26 41) (80 86) (10 21)) ; 16 steps

Once with lists of length 4

’((32 56 79 84) (38 49 73 93) (26 38 41 49) (10 21 80 86)) ; 16 steps

….

Once with lists of length n/2

So we do it log_2(n) times. Each time cost n, so the total is n*log_2(n).

Do we have to calculate this on our own?

You should be able to run experiments. (Using counters or print.)

You should be able to hypothesize as to why things are slower/faster.

But precise analysis is not necessary.

Why is this faster?

Old merge sort and new merge sort are about equivalent.

New merge sort is easier to analyze.

Original merge sort is prettier.

Can we analyze divide-and-conquer algorithms by “we divide logn times, it takes ? work each time we divide”.

Sure, for now.

Why do we use kernel?

It’s the traditional name for helper or go.

It’s a traditional name for a recursive helper.

Why did we have to check insert-number?

Edge cases!

Get students to find errors in Sam’s code.

What’s going on with the funky let in kernel?

(let kernel ([param1 val1] [param2 val2] ...) ...) “named let”

is shorthand for

`(let ([kernel (lambda (param1 param2 …) ..)]) (kernel val1 val2 ..))

I think it’s fixed in the lab.

Other Questions

Can we talk about tracing tree recursion?

Tomorrow.

Quiz

You know the drill. However, you won’t need Racket, just your notes from yesterday.

“Quiz for Class 32: Sorting”

Sort alphabetically.

Costs of Reverse

Even though I said “You don’t need to do careful analysis”, we’re going to try an example or two.

;;; (list-append l1 l2) -> list?
;;;   l1, l2 : list?
;;; Returns the list formed by placing the elements of l2 after the elements
;;; of l1, preserving the order of the elements of l1 and l2.
(define list-append
  (lambda (l1 l2)
    (write (list 'list-append l1 l2)) (newline)
    (cond
      [(null? l1)
       l2]
      [else
       (cons (car l1)
             (list-append (cdr l1) l2))])))

;;; (list-reverse l) -> list?
;;;   l : list?
;;; Returns l with the elements in the opposite order.
(define list-reverse-1
  (lambda (l)
    (match l
      ['()
       null]
      [(cons x tail)
       (list-append (list-reverse-1 tail) (list x))])))
  • (list-append l1 l2) gets called how many times?
    • 1 + The number of elements in l1 (there’s still a call when it’s empty)
  • On to reverse. Suppose we reverse ‘(1 2 3 4 5).
    • (list-append '(5 4 3 2) '(1))
    • To turn '(2 3 4 5) into '(5 4 3 2), we’ll need (list-append '(5 4 3) '(2))
    • To turn '(3 4 5) into '(5 4 3), we’ll need (list-append '(5 4) '(3))
    • To turn '(4 5) into '(5 4), we’ll need (list-append '(5) '(4))
    • To turn '(5) into '(5), we’ll need (list-append '() '(5))
    • To turn '() into '(), we do nothing.
    • (list-append '() '(5)) : 1
    • (list-append '(5) '(4)) : 2
    • (list-append '(5 4) '(3)) : 3
    • (list-append '(5 4 3) '(2)) : 4
    • (list-append '(5 4 3 2) '(1)) : 5
  • 1 + 2 + 3 + 4 + 5 = 15
  • If our original list was of length n, we’ll spend 1 + 2 + 3 + 4 + … + n. (termial n).

Computing (termial n) quickly.

  • 1 + 2 + 3 + … 100 = 5050
  • There’s a formula. Let’s figure it out.

      1  +  2  +  3  +  4  + ... + n-2 + n-1 +  n     = x
      n  + n-1 + n-2 + n-3 +     +  3  +  2  +  1     = x (addition is comm.)
      -------------------------------------------       -
      n+1+ n+1 + n+1 + ..................... + n+1    = 2x
    
      (n+1)*n = 2x
    
      x = n*(n+1)/2
    
      1 + 2 + 3 .... + 100 = 100*101/2 = 50*101 = 5050.
    

Why did we do this?

  • So that you know that if you see a running time that is 1 + 2 + 3 + … + n. That’s n(n+1)/2, which is about n*n/2, which is quadratic. (It will quadruple in steps each time you double the input size.)

SoLA 3: Memory Models and Side Effects

On immutable lists

Here’s a sample list.

(define cat (list "c" "a" "t"))

        +---+---+    +---+---+    +---+---+
cat --> | * | -----> | * | *----> | * | / |
        +-|-+---+    +-|-+---+    +-|-+---+
          v            v            v
         "c"          "a"          "t"

Here’s an attempt to “change” that list (build a new list)

(define hat (cons "h" (cdr cat)))

         "h"
          ^
        +-|-+---+
hat --> | * | *--------+
        +---+---+      |
                       v
                     (cdr cat)
        +---+---+    +---+---+    +---+---+
cat --> | * | -----> | * | *----> | * | / |
        +-|-+---+    +-|-+---+    +-|-+---+
          v            v            v
         "c"          "a"          "t"

Important issues

  • cons creates a new list
  • But lists may share structure.
  • cdr only extracts part of a list.
  • list still builds a series of cons cells (pairs)

On let

Incorrect comment from some students: “The let introduces a temporary change to a vector.”

Consider the following code.

;;; (vector-swap! vec i j) -> vec
;;;   vector : vector?
;;;   i : integer?
;;;   j : integer?
;;; Swap the elements at indices i and j of vec, returning the
;;;   now-modified vector.
;;; Pre: 0 <= i < (vector-length vec)
;;; Pre: 0 <= j < (vector-length vec)
(define vector-swap!
  (lambda (vec i j)
    (let ([tmp (vector-ref vec i)])
      (vector-set! vec i (vector-ref vec j))
      (vector-set! vec j tmp)
      vec)))

;;; (vector-something! vec i) -> any?
;;;    vec : vector?
;;;    i : integer?
;;; Swap elements at positions 0 and i, returning the old value
;;;   at position i.
;;; Pre: 0 <= i < (vector-length vec)
(define vector-something!
  (lambda (vec i)
    (let ([newvec (vector-swap! vec 0 i)])
      ; Temporary: Verify that the vector changed
      (write newvec) (newline) 
      (vector-ref vec 0)))) 

What do we expect for the following?

> (define sample (vector "c" "a" "t")
> (vector-something! sample 1)
Output: #("a" "c" "t")
Result: "a"
> sample 
; possibly #um or #("a" "c" "t") or #("c" "a" "t")
#("a" "c" "t")
  • Moral: lets do temporary bindings of names to values.
  • But if those values are mutable, and we mutate them, all references to those values change.

From the last SoLA

Elided

Detour: Sorting Reading

Sam’s predictions

(helper '(7 6 12 4 10 8 5 1) '())
(helper '(6 12 4 10 8 5 1) '(7))
(helper '(12 4 10 8 5 1) '(6 7))
(helper '(4 10 8 5 1) '(6 7 12))
(helper '(10 8 5 1) '(4 6 7 12))

The actual thing

> (numbers-insertion-sort '(7 6 12 4 10 8 5 1))
(helper (7 6 12 4 10 8 5 1) ())
(helper (6 12 4 10 8 5 1) (7))
(helper (12 4 10 8 5 1) (6 7))
(helper (4 10 8 5 1) (6 7 12))
(helper (10 8 5 1) (4 6 7 12))
(helper (8 5 1) (4 6 7 10 12))
(helper (5 1) (4 6 7 8 10 12))
(helper (1) (4 5 6 7 8 10 12))
(helper () (1 4 5 6 7 8 10 12))

Largest in List

Why was alphabetically-first so slow for lists arranged last to first?

;;; (alphabetically-first string) -> string
;;;   strings: both (listof string?) nonempty?
;;; Find the alphabetically first string in the list.
(define alphabetically-first-1
  (lambda (strings)
    ; (counter-increment! AFC 'alphabetically-first-1)
    (cond
      [(null? (cdr strings))
       (car strings)]
      [(string-ci<=? (car strings) (alphabetically-first-1 (cdr strings)))
       (car strings)]
      [else
       (alphabetically-first-1 (cdr strings))])))

(define alphabetically-first-2
  (lambda (strings)
    ; (counter-increment! AFC 'alphabetically-first-2)
    (if (null? (cdr strings))
        (car strings)
        (first-of-two (car strings)
                      (alphabetically-first-2 (cdr strings))))))

;;; (first-of-two str1 str2) -> string?
;;;   str1 : string?
;;;   str2 : string?
;;; Find the alphabetically first of str1 and str2.
(define first-of-two
  (lambda (str1 str2)
    (if (string-ci<=? str1 str2)
        str1
        str2)))

Fun with Trees

Aka: Making your brain hurt a little bit more.

Our normal pattern for tree recursion.

(define tree-proc
  (lambda (tree)
    (if (empty-tree? tree)
        base-case
        (combine (process (btt tree))
                 (tree-proc (btl tree))
                 (tree-proc (btr tree))))))

For tree-size,

  • base-case is 0
  • combine is +
  • process is just 1, (lambda (x) 1)
(define tree-size
  (lambda (tree)
    (if (empty-tree? tree)
        0
        (+ 1
           (tree-size (btl tree))
           (tree-size (btr tree))))))

For tree-sum

  • base-case is 0
  • combine is +
  • process is “use the value of the root”, (lambda (x) x)
(define tree-sum
  (lambda (tree)
    (if (empty-tree? tree)
        0
        (+ (btt tree)
           (tree-sum (btl tree))
           (tree-sum (btr tree))))))

For tree->list

  • base-case is empty list
  • combine is append
  • process is list
(define tree->list
  (lambda (tree)
    (if (empty-tree? tree)
        null
        (append (list (btt tree))
                (tree->list (btl tree))
                (tree->list (btr tree))))))

Side note: append is the built-in procedure, list-append is what we’ve written.

For tree-increment, which adds 1 to each value in a tree of numbers.

(btn-increment (btn 1 (leaf 5) (leaf 6))) -> (btn 2 (leaf 6) (leaf 7)) (btn-increment empty-tree) -> empty-tree

  • base-case is empty-tree
  • combine is btn
  • process is “add 1”
(define tree-increment
  (lambda (tree)
    (if (empty-tree? tree)
        empty-tree
        (btn (+ 1 (btt tree))
             (tree-increment (btl tree))
             (tree-increment (btr tree))))))

Note; btn is a shorthand for binary-tree, it stands for binary-tree-new or binary-tree-node.

Hmm … we’re doing a lot of boring, repetitive work. Isn’t that what computers are good for?

;;; (make-tree-proc base-case combine process) -> procedure?
;;;    base-case : value
;;;    combine : procedure?
;;;    process : procedure?
;;; Create a procedure for processing trees using the standard template
(define make-tree-proc
  (lambda (base-case combine process)
    (letrec [(tree-proc
              (lambda (tree)
                (if (empty-tree? tree)
                    base-case
                    (combine (process (btt tree))
                             (tree-proc (btl tree))
                             (tree-proc (btr tree))))))]
      tree-proc)))

Does this really work?

Let’s see.

> (define tree-size (make-tree-proc 0
                                    +
                                    (lambda (x) 1)))
> tree-size
#<procedure:tree-proc>
> (tree-size empty-tree)
0
> (tree-size (btn 4 (btn 5 (leaf 6) (leaf 7))
                  empty-tree))
4
> (define tree-sum (make-tree-proc 0 + (lambda (x) x)))
> (tree-sum (btn 4 (btn 5 (leaf 6) (leaf 7))
                  empty-tree))
22
> (define tree->list (make-tree-proc null append list))
> (tree->list (btn 4 (btn 5 (leaf 6) (leaf 7))
                  empty-tree))
'(4 5 6 7)
> (define tree-increment (make-tree-proc empty-tree btn (section + 1 <>)))
> (tree-increment (btn 4 (btn 5 (leaf 6) (leaf 7))
                  empty-tree))
'(5 (6 (7  ) (8  )) )

Normally, we use lambda to define procedures. (Except when we use o or section.) For tree procs, we normally have (lambda (tree) ...).

make-tree-proc includes the lambda for us, and returns the procedure with that lambda implied.

I did say your brain would hurt.

We’ve written a procedure that makes procedures.

Followup from after class.

Let’s trace the construction of tree-size.

     (make-tree-proc 0 + (lambda (x) 1)))
     ; Grab the body of make-tree-proc
     ; (letrec [(tree-proc
     ;        (lambda (tree)
     ;           (if (empty-tree? tree)
     ;               base-case
     ;               (combine (process (btt tree))
     ;                        (tree-proc (btl tree))
     ;                        (tree-proc (btr tree))))))]
     ; tree-proc)))
     ; Substitute 0 for base-case, + for combine, (lambda (x) 1 for process
---> (letrec [(tree-proc
              (lambda (tree)
                (if (empty-tree? tree)
                    0
                    (+ ((lambda (x) 1) (btt tree))
                             (tree-proc (btl tree))
                             (tree-proc (btr tree))))))]
       tree-proc)))
     ; We normally write 1 instead of the ugly
     ; `((lambda (x) 1) (btt tree))`, but `make-tree-proc` forces us 
     ; to do something with the root.  `(lambda (x) 1)` is
     ; "throw away my parameter and return 1"
     ;
     ; We've defined tree-proc has we'd expect tree-size to look.
     ; The procedure returns tree-proc; we're *not* calling the
     ; new procedure we defined, we're just returning it.
(define adder
  (lambda (n)
    (let [(addn (lambda (x) (+ n x)))]
      ; addn is a procedure that takes one input, x, and adds
      ; n to x
      addn))) ; We've returned a procedure that takes one parameter

> (define add5 (adder 5))
> add5
#<procedure:addn>
> (add5 11)
16
> (add5 10)
15
> (add5 1213)
1218

Lab

Not held.