CS 201 - Fall 2021.


[Home]

Running time of programs!

Running time of programs I.

Summary.

Youtube insertion sort

The pseudocode for insertion sort from the Wikipedia article:

for i ← 1 to length(A)
    x ← A[i]
    j ← i
    while j > 0 and A[j-1] > x
        A[j] ← A[j-1]
        j ← j - 1
    A[j] ← x
This is (slightly) incorrect: the array A has 0-based indexing, so when i = length(A), the array reference A[i] will be out of bounds; imagine the first line corrected to have length(A)-1 instead of length(A). We analyzed this program by thinking of it translated into TC-201 assembly language and estimating the number of TC-201 assembly language instructions that would be executed to sort an array of n numbers.

The new element here is how to account for the time to access the array A. In this pseudocode, the array A is a finite sequence of mutable elements (like a vector in Racket) each of which is accessed by its index, a number from 0 to the length of the array minus 1. If we consider a TC-201 implementation of an array A of numbers we could allocate a number of contiguous memory registers equal to the length of the array, and store the numbers in order in those memory locations. In addition, we would store the address of the start of the array in another memory location, call it astart. Then the TC-201 instructions to find the value of A[i] would load astart into the accumulator, add the value of i to it, and store it in a temporary location, say temp, and then execute "loadi temp" to get the value of A[i] into the accumulator. This is a constant number of TC-201 instructions to access any of the elements of the array. To change the value of A[i], we could use a similar address calculation to get the memory address of the i-th element of A into temp, and then execute "load value" and "storei temp" to change the value of A[i] to the number in the memory register value. This is similarly a constant number of instructions. Thus it makes sense to count array references as constant time operations.

With that understanding of array references, a straightforward translation of a for-loop into TC-201 instructions, and the observation that assignment, addition, subtraction and comparison are constant time operations, our conclusions about the running time of this implementation of insertion sort were: best case time of Theta(n) (Θ(n)) and worst case time of Theta(n2) (Θ(n2)).

Please see the notes: Running time of a program.

Running time of programs II.

Summary.

Please see the notes: Running time of a program and List representation. For the insert procedure, see Sorting.

Running time of programs III.

Summary. Running times of insertion sort and merge sort.

Please see the notes: Sorting.

Mutators (and a little bit about objects).

Summary.

  • Representing an "object" as a procedure with local data.
  • The Racket mutator set! and how to use it to implement a counter object.

    This topic is useful for solving problem 7 in hw 8.

    Please see the notes: Environments in Racket and the Mutator set!.

    counter.rkt

    In a setting of pure functional programming, calling a procedure with the same arguments will always produce the same result. This is not the case with Racket's library procedure random. Calling (random 10) might return 6 one time and 0 the next. The random procedure has some local state that changes with each call and allows it to produce different responses to the same argument. An "object" in the sense of object oriented programming can be considered to be a bundle of data and procedures (or "methods") to operate on the data. We can represent an object in Racket as a procedure with local state.

    Before we do that, we look at one of Racket's mutators, namely set! The exclamation point is part of the name of the procedure, and is a convention to indicate that the procedure is a mutator, that is, changes the value of a variable or other data. The form of set! is

        (set! variable expression)
    
    The expression is evaluated to get a value, and then the variable is looked up in the relevant environment (just as we would look it up to find its value) -- in that environment its binding is changed to the value of the expression. Note that the variable must already be defined in order to use it in a set! expression. Attempting to set! a variable that is not defined is an error. This is analogous to having to declare a variable before it can have a value assigned to it.

    Example of the use of set! in the top-level environment.

    > (define count 0)
    > count
    0
    > (set! count (+ 1 count))
    > count
    1
    >
    
    Evaluating the expression (define count 0) adds count to the top-level environment with its value bound to 0. Then evaluating the expression (set! count (+ 1 count)) evaluates the expression (+ 1 count) in the usual way to get 1, and looks up count, finding it in the top-level environment, where its value is currently 0. The value of count is changed to 1 in the top-level environment; now when its value is looked up, it will be 1.

    This behavior is enough to define a simple counter procedure which will return different values for the same arguments, depending on the value of count. We write the following procedure.

    > (define (counter cmd)
        (case cmd
          [(increment!) (set! count (+ 1 count))]
          [(zero!) (set! count 0)])
        count)
    > (counter 'increment!)
    2
    > (counter 'increment!)
    3
    > (counter 'zero!)
    0
    > (counter 'increment!)
    1
    > count
    1
    >
    
    Note that when this procedure refers to count, the search to find the relevant environment finds it in the top-level environment, (where we assume we previously defined it and incremented it to 1.) Thus in this case the variable count functions as a "global" variable, accessible everywhere in the file. (We could also have written counter without the case statement, using a cond expression and equal? tests.)

    One property we might want for an object is that its data is not global, but local and private to the object, so that it cannot be read or changed without invoking the "methods" (or procedures) associated with the object. We can achieve this goal with the following definition. Assume that we have re-entered the Racket interpreter afresh, so that the preceding definition of count and counter are gone. (This is not what we did in lecture, but should be less confusing for the notes.)

    > (define counter1
        (let ((count 0))
          (lambda (cmd)
            (case cmd
              [(increment!) (set! count (+ 1 count))]
              [(zero!) (set! count 0)])
            count)))
    > (counter1 'increment!)
    1
    > (counter1 'increment!)
    2
    >
    
    This creates a procedure named counter1 with a private local variable count, whose value can only be inspected and changed by calls to the procedure. This protects against other procedures accidentally or deliberately changing the value of count other than through the interface provided by this procedure. For an analysis of (a variant of) this procedure, and a higher-level counter-creation procedure, in terms of Racket environments, please see the notes: Environments in Racket and the Mutator set!.

    If we arrange the lets and lambdas in a different fashion, we get a counter procedure that UTTERLY FAILS at its task.

        (define not-a-counter
          (lambda (cmd)
            (let ((count 0))
              (case cmd
                [(increment!) (set! count (+ 1 count)) count]
                [(zero!) (set! count 0) count]))))
    
    As examples of the behavior of this procedure, we have the following.
    > (not-a-counter 'increment!)
    1
    > (not-a-counter 'increment!)
    1
    > (not-a-counter 'zero!)
    0
    > (not-a-counter 'increment!)
    1
    >
    
    Think about what is happening in terms of environments to keep this from behaving like a counter.


    [Home]