Homework 5: Linked lists, Efficiency, Scheme

Due by 11:59pm on Wednesday, July 29

Instructions

Download hw05.zip. Inside the archive, you will find a file called hw05.py, along with a copy of the ok autograder.

Submission: When you are done, submit the assignment to Gradescope. You may submit more than once before the deadline; only the final submission will be scored. Check that you have successfully submitted your code on Gradescope. See Lab 0 for more instructions on submitting assignments.

Using Ok: If you have any questions about using Ok, please refer to this guide.

Readings: You might find the following references useful:

Grading: Homework is graded based on correctness. Each incorrect problem will decrease the total score by one point. This homework is out of 2 points.

The 61A Scheme interpreter is included in each Scheme assignment. To start it, type python3 scheme in a terminal. To load a Scheme file called f.scm, type python3 scheme -i f.scm. To exit the Scheme interpreter, type (exit).

We recommend that you install the vscode-scheme extension so that parentheses are highlighted.

Before:

After:

In addition, the 61a-bot (installation instructions) VS Code extension is available for Scheme homeworks. The bot is also integrated into ok.

Required Questions

Linked Lists


Here is the implementation of the Link class:

class Link:
    """A linked list is either a Link object or Link.empty

    >>> s = Link(3, Link(4, Link(5)))
    >>> s.rest
    Link(4, Link(5))
    >>> s.rest.rest.rest is Link.empty
    True
    >>> s.rest.first * 2
    8
    >>> print(s)
    (3 4 5)
    """
    empty = ()

    def __init__(self, first, rest=empty):
        assert rest is Link.empty or isinstance(rest, Link)
        self.first = first
        self.rest = rest

    def __repr__(self):
        if self.rest:
            rest_repr = ', ' + repr(self.rest)
        else:
            rest_repr = ''
        return 'Link(' + repr(self.first) + rest_repr + ')'

    def __str__(self):
        string = '('
        while self.rest is not Link.empty:
            string += str(self.first) + ' '
            self = self.rest
        return string + str(self.first) + ')'
Mutable Linked Lists Regular Linked Lists
Traits - Represented with custom object, Link
- Has methods and attributes
- Mutable
- Represented with abstract data type
- Has selector functions
- Not mutable
Examples
>>> s = Link(1, Link(2, Link(3, Link(4))))
>>> s
Link(1, Link(2, Link(3, Link(4))))
>>> s.first = 9
>>> s
Link(9, Link(2, Link(3, Link(4))))
>>> s = link(1, link(2, link(3, link(4))))
>>> s
[1, [2, [3, [4, 'empty']]]]
>>> first(s) = 9
  File "", line 1
SyntaxError: can't assign to function call
Explanations Link is mutable because we have created an object Link with attributes that allow direct access to the elements inside of it. This allows us to change the elements, thus mutating the Link link is not mutable because its selector functions only return to us the values of its elements. We are not able to change or reassign these values.

Write a function add_link that takes in two linked lists, link1 and link2, and returns a new linked list that concatenates link2 to the end of link1.

You may assume that the input list is shallow; none of its elements are themselves another linked list.

Note: You may not assume that the input lists are of the same length.

Challenge (Optional): Do NOT assume that the input list is shallow (i.e. your input can be a nested linked list). Hint: use the built-in type function.

def add_links(link1: Link, link2: Link) -> Link:
    """Adds two Links, returning a new Link

    >>> l1 = Link(1, Link(2))
    >>> l2 = Link(3, Link(4, Link(5)))
    >>> new = add_links(l1, l2)
    >>> print(new)
    (1 2 3 4 5)
    >>> new2 = add_links(l2,l1)
    >>> print(new2)
    (3 4 5 1 2)
    """
    "*** YOUR CODE HERE ***"

Use Ok to test your code:

python3 ok -q add_links

Q2: Mutable Mapping

Implement deep_map_mut(func, s), which applies the function func to each element in the linked list s. If an element is itself a linked list, recursively apply func to its elements as well.

Your implementation should mutate the original linked list. Do not create any new linked lists. The function returns None.

Hint: You can use the built-in isinstance function to determine if an element is a linked list.

>>> s = Link(1, Link(2, Link(3, Link(4))))
>>> isinstance(s, Link)
True
>>> isinstance(s, int)
False

Construct Check: The final test case for this problem checks that your function does not create any new linked lists. If you are failing this doctest, make sure that you are not creating link lists by calling the constructor, i.e.

s = Link(1)
def deep_map_mut(func, s: Link) -> None:
    """Mutates a deep link s by replacing each item found with the
    result of calling func on the item. Does NOT create new Links (so
    no use of Link's constructor).

    Does not return the modified Link object.

    >>> link1 = Link(3, Link(Link(4), Link(5, Link(6))))
    >>> square = lambda x: x * x
    >>> print(link1)
    (3 (4) 5 6)
    >>> link2 = Link(1, Link(Link(Link(2, Link(3))), Link(4)))
    >>> double = lambda x: x * 2
    >>> print(link2)
    (1 ((2 3)) 4)
    >>> # Disallow the use of making new Links before calling deep_map_mut
    >>> Link.__init__, hold = lambda *args: print("Do not create any new Links."), Link.__init__
    >>> try:
    ...     deep_map_mut(square, link1)
    ...     deep_map_mut(double, link2)
    ... finally:
    ...     Link.__init__ = hold
    >>> print(link1)
    (9 (16) 25 36)
    >>> print(link2)
    (2 ((4 6)) 8)
    """
    "*** YOUR CODE HERE ***"

Use Ok to test your code:

python3 ok -q deep_map_mut

Efficiency


Tricky efficiency examples are for 61B, not 61A. We explore efficiency on a basic level only; for example, we treat the runtime of multiplication and other primitive operations as constant even for large inputs.

In addition to classifying algorithms as Exponential, Quadratic, Linear, Logarithmic, or Constant, we also introduce Big Theta notation, but do not go into the details of runtime analysis.

There's a slight nuance here: in CS 61A, we do runtime analysis with respect to the input. In later classes (such as 61B, CS 170, etc.) runtime analysis is done with respect to the size (i.e. the number of bits) of the input. If this confuses you, don't worry about it too much. Just try to be consistent and tell students that we are considering how a function's runtime is affected when we change the input.

Throughout this class, we have mainly focused on correctness — whether a program produces the correct output. However, computer scientists are also interested in creating efficient solutions to problems. One way to quantify efficiency is to determine how a function's runtime changes as its input changes. In this class, we measure a function's runtime by the number of operations it performs.

A function f(n) has...

  • constant runtime if the runtime of f does not depend on n. Its runtime is Θ(1).
  • logarithmic runtime if the runtime of f is proportional to log(n). Its runtime is Θ(log(n)).
  • linear runtime if the runtime of f is proportional to n. Its runtime is Θ(n).
  • quadratic runtime if the runtime of f is proportional to n^2. Its runtime is Θ(n^2).
  • exponential runtime if the runtime of f is proportional to b^n, for some constant b. Its runtime is Θ(b^n).

Example 1: It takes a single multiplication operation to compute square(1), and it takes a single multiplication operation to compute square(100). In general, calling square(n) results in a constant number of operations that does not vary according to n. We say square has a runtime complexity of Θ(1).

input function call return value operations
1 square(1) 1*1 1
2 square(2) 2*2 1
... ... ... ...
100 square(100) 100*100 1
... ... ... ...
n square(n) n*n 1

Example 2: It takes a single multiplication operation to compute factorial(1), and it takes 100 multiplication operations to compute factorial(100). As n increases, the runtime of factorial increases linearly. We say factorial has a runtime complexity of Θ(n).

input function call return value operations
1 factorial(1) 1*1 1
2 factorial(2) 2*1*1 2
... ... ... ...
100 factorial(100) 100*99*...*1*1 100
... ... ... ...
n factorial(n) n*(n-1)*...*1*1 n

Example 3: Consider the following function:

def bar(n):
    for a in range(n):
        for b in range(n):
            print(a,b)

Evaulating bar(1) results in a single print call, while evalulating bar(100) results in 10,000 print calls. As n increases, the runtime of bar increases quadratically. We say bar has a runtime complexity of Θ(n^2).

input function call operations (prints)
1 bar(1) 1
2 bar(2) 4
... ... ...
100 bar(100) 10000
... ... ...
n bar(n) n^2

Example 4: Consider the following function:

def rec(n):
    if n == 0:
        return 1
    else:
        return rec(n - 1) + rec(n - 1)

Evaluating rec(1) results in a single addition operation. Evaluating rec(4) results in 2^4 - 1 = 15 addition operations, as shown by the diagram below.

During the evaulation of rec(4), there are two calls to rec(3), four calls to rec(2), eight calls to rec(1), and 16 calls to rec(0).

So we have eight instances of rec(0) + rec(0), four instances of rec(1) + rec(1), two instances of rec(2) + rec(2), and a single instance of rec(3) + rec(3), for a total of 1 + 2 + 4 + 8 = 15 addition operations.

Above: Call structure of rec(4).

As n increases, the runtime of rec increases exponentially. In particular, the runtime of rec approximately doubles when we increase n by 1. We say rec has a runtime complexity of Θ(2^n).

input function call return value operations
1 rec(1) 2 1
2 rec(2) 4 3
... ... ... ...
10 rec(10) 1024 1023
... ... ... ...
n rec(n) 2^n 2^n - 1

Tips for finding the order of growth of a function's runtime:

  • If the function is recursive, determine the number of recursive calls and the runtime of each recursive call.
  • If the function is iterative, determine the number of inner loops and the runtime of each loop.
  • Ignore coefficients. A function that performs n operations and a function that performs 100 * n operations are both linear.
  • Choose the largest order of growth. If the first part of a function has a linear runtime and the second part has a quadratic runtime, the overall function has a quadratic runtime.
  • In this course, we only consider constant, logarithmic, linear, quadratic, and exponential runtimes.

Q3: Prime

Write a function that returns whether a number is prime or not in O(sqrt(n)) time, where sqrt means square root. You can assume n >= 2.

Hint: you don't need to check whether every single number that is smaller than n divides n

from math import sqrt
def is_prime_sqrt(n: int) -> bool:
    """Tests whether a number N is prime or not. Implement this function
    in O(sqrt(n)) time. You can assume n >= 2

    >>> is_prime_sqrt(2)
    True
    >>> is_prime_sqrt(67092481)
    False
    >>> is_prime_sqrt(524287)
    True
    >>> is_prime_sqrt(2251748274470911)
    False
    >>> is_prime_sqrt(6700417)
    True
    >>> is_prime_sqrt(44895587973889)
    False
    >>> is_prime_sqrt(2147483647)
    True
    >>> is_prime_sqrt(67280421310721)
    True
    """
    # sqrt(k) will give the square root of k as a floating point (decimal)
    "*** YOUR CODE HERE ***"

Use Ok to test your code:

python3 ok -q is_prime_sqrt

Scheme


Scheme uses Polish prefix notation, in which the operator expression comes before the operand expressions. For example, to evaluate 3 * (4 + 2), we write:
scm> (* 3 (+ 4 2))
18

Just like in Python, to evaluate a call expression:

  1. Evaluate the operator. It should evaluate to a procedure.
  2. Evaluate the operands, left to right.
  3. Apply the procedure to the evaluated operands.

Here are some examples using built-in procedures:

scm> (+ 1 2)
3
scm> (- 10 (/ 6 2))
7
scm> (modulo 35 4)
3
scm> (even? (quotient 45 2))
#t

The define form is used to assign values to symbols. It has the following syntax:
(define <symbol> <expression>)
scm> (define pi (+ 3 0.14))
pi
scm> pi
3.14

To evaluate the define expression:

  1. Evaluate the final sub-expression (<expression>), which in this case evaluates to 3.14.
  2. Bind that value to the symbol (symbol), which in this case is pi.
  3. Return the symbol.

The define form can also define new procedures, described in the "Defining Functions" section.


The cond special form can include multiple predicates (like if/elif in Python):

(cond
    (<p1> <e1>)
    (<p2> <e2>)
    ...
    (<pn> <en>)
    (else <else-expression>))

The first expression in each clause is a predicate. The second expression in the clause is the return expression corresponding to its predicate. The else clause is optional; its <else-expression> is the return expression if none of the predicates are true.

The rules of evaluation are as follows:

  1. Evaluate the predicates <p1>, <p2>, ..., <pn> in order until one evaluates to a true value (anything but #f).
  2. Evalaute and return the value of the return expression corresponding to the first predicate expression with a true value.
  3. If none of the predicates evaluate to true values and there is an else clause, evaluate and return <else-expression>.

For example, this cond expression returns the nearest multiple of 3 to x:

scm> (define x 5)
x
scm> (cond ((= (modulo x 3) 0) x)
            ((= (modulo x 3) 1) (- x 1))
            ((= (modulo x 3) 2) (+ x 1)))
6

Q4: Pow

Implement a procedure pow that raises a number base to the power of a nonnegative integer exp. The number of recursive pow calls should grow logarithmically with respect to exp, rather than linearly. For example, (pow 2 32) should result in 5 recursive pow calls rather than 32 recursive pow calls.

Hint:

  1. x2y = (xy)2
  2. x2y+1 = x(xy)2

For example, 216 = (28)2 and 217 = 2 * (28)2.

You may use the built-in predicates even? and odd?. Also, the square procedure is defined for you.

Scheme doesn't have while or for statements, so use recursion to solve this problem.

(define (square n) (* n n))

(define (pow base exp)
  'YOUR-CODE-HERE
)

Use Ok to test your code:

python3 ok -q pow

Q5: Repeatedly Cube

Implement repeatedly-cube, which receives a number x and cubes it n times.

Here are some examples of how repeatedly-cube should behave:

scm> (repeatedly-cube 100 1) ; 1 cubed 100 times is still 1
1
scm> (repeatedly-cube 2 2) ; (2^3)^3
512
scm> (repeatedly-cube 3 2) ; ((2^3)^3)^3
134217728
(define (repeatedly-cube n x)
    (if (zero? n)
        x
        (begin
            (define y ___)
            ___)))

Use Ok to test your code:

python3 ok -q repeatedly-cube

Q6: Cadr and Caddr

Define the procedure cadr, which returns the second element of a list. Also define caddr, which returns the third element of a list. Try writing cadr and caddr in terms of car and cdr.

(define (cddr s)
  (cdr (cdr s)))

(define (cadr s)
  'YOUR-CODE-HERE
)

(define (caddr s)
  'YOUR-CODE-HERE
)

Use Ok to test your code:

python3 ok -q cadr-caddr

Check Your Score Locally

You can locally check your score on each question of this assignment by running

python3 ok --score

This does NOT submit the assignment! When you are satisfied with your score, submit the assignment to Gradescope to receive credit for it.

Submit Assignment

Submit this assignment by uploading any files you've edited to the appropriate Gradescope assignment. Lab 00 has detailed instructions.

Exam Practice

Homework assignments will also contain prior exam questions for you to try. These questions have no submission component; feel free to attempt them if you'd like some practice!

  1. Fall 2019 Final Q7b: Mull It Over
  2. Spring 2018 Midterm 2 Q4(a): Sequences