Final Review

Final Particulars

When and where
December 14 7:00-10:00PM or December 15 9:00AM-12:00PM in 75 SHS 202. You can take the exam in either slot (no notification or approval needed to do so).
What can I use?
  • The cheat sheet handout on the course web page (I will bring copies of the cheat sheet to the exam so you don’t have to)
  • One piece of letter-sized paper with notes on both sides
What can’t I bring?
Anything else, e.g., book, computer, other notes, etc.
Are there additional office hours?
Yes! I will hold regular office hours on Monday, December 12, and then additional office hours: Dec. 13 10:30AM-noon, 1:00-2:30PM and Dec. 14 1:00-2:30PM

What will the exam cover?

The exam is cumulative (as is the course itself), but with an emphasis on topics we have covered since the midterm.

Topics since midterm:

The exam will NOT include material that was in the readings but that we did not discuss in class, or use in our labs, or practice in the problem sets.

Types of questions

  1. Determine the output of code
  2. Rewrite code with better style
  3. Identify bugs in code
  4. Translate code from NumPy/datascience to “built-in” Python and vice-versa
  5. Write code to solve problem
  6. Describe time complexity (Big-O) of algorithms

Review Questions

  1. Rewrite the following function to improve the style: Show answer.

    def longwinded(a, b, c):
        if a > b:
            if a > c:
                return True
            else:
                return False
        else:
            return False
    
    def longwinded(a, b, c):
        return (a > b) and (a > c)
    

    If we are returning boolean values from within a conditional expression, it is much clearer to return the boolean expression directly, so we don’t need to both figure out the conditional and the mapping between that conditional and the return value. Similarly, we want to avoid comparing boolean values to True or False with ==, e.g.,

    value = True
    # Undesired
    if value == True:
        # Do something...
    
    # Better
    if value:
        # Do something
    

    At best comparing booleans is redundant, but it also doesn’t take into account other “truthy” or “falsy” values, e.g., None, that we could want to use in a conditional, but are not strictly True or False.

  2. What does the following function do (in one sentence) assuming x is a list: Show answer.

    def mystery(x):
        if len(x) <= 1:
            return True
        else:
            if x[0] < x[1]:
                return False
            else:
                return mystery(x[1:])
    

    mystery returns True if the list is in descending sorted order.

  3. Draw the memory model, as would be generated by Python Tutor/Visualizer (pythontutor.com), after the following code executes: Show answer.

     x = [[1, 2], 3]
     y = x[:] + [3]*2
     x[1] = 5
    
  4. Write a function that takes two parameters: a dictionary and a number. The function should update the dictionary by adding the number to each value in the dictionary. Show answer.

    def add_num(a_dict, number):
        for key in a_dict:
            a_dict[key] += number
    

    Recall that for key in a_dict: is the same as for key in a_dict.keys():. We don’t need to return the dictionary. Instead this function modifies its arguments, i.e., it modifies the dictionary provided as the argument:

    >>> a = { 1: 2 }
    >>> add_num(a, 10)
    >>> a
    {1: 12}
    
  5. Which of the following statements evaluates to True? Show answer.

    1. ['a', 'b', 'c'] == list("abc")
    2. [1, 2, 3] == list(range(1, 4))
    3. ['a', 'b', 'c'] == list({ 'a', 'b', 'c' })
    4. [1, 2, 3] == list({ 'a': 1, 'b': 2, 'c': 3 }.values())

    All of the above inputs (string, range, set and dictionary) to the list function are iterables, and thus we can create lists from the elements, use the types as loop sequences, etc..

    1. ['a', 'b', 'c'] == list("abc") is True
    2. [1, 2, 3] == list(range(1, 4)) is True
    3. ['a', 'b', 'c'] == list({ 'a', 'b', 'c' }) may or may not be True. We can’t predict just by looking at the code at the iteration order of the values of a set (i.e. we treat sets as unordered).
    4. [1, 2, 3] == list({ 'a': 1, 'b': 2, 'c': 3 }.values()) is True. Historically we could not predict the iteration order of the items in a dictionary. However, as of Python 3.7, dictionaries now maintain insertion order (that is when you iterate the keys/values are in the order you inserted them into the dictionary).
  6. Write a function named strip_upper that takes a string as a parameter and returns the string with all the uppercase letters removed. Recall that the string class has an isupper method that checks if it is all uppercase. Show answer.

    def strip_upper(a_string):
        result = ""
        for char in a_string:
            if not char.isupper():
                result += char
        return result
    

    A recursive implementation could be:

    def strip_upper(a_string):
        if a_string == "":
            return ""
        else:
            if a_string[0].isupper():
                return strip_upper(a_string[1:])
            else:
                return a_string[0] + strip_upper(a_string[1:])
    
  7. Write a recursive function all_upper that takes a list of strings as a parameter and returns a list of booleans, True for strings in the list that are all uppercase, False otherwise. Recall that the string class has an isupper method that checks if it is all uppercase. Show answer.

    def all_upper(strings):
        if len(strings) == 0:
            return []
        else:
            return [strings[0].isupper()] + all_upper(strings[1:])
    
  8. What decimal numbers are represented by the following binary numbers:
    1. 1101 (Show answer)

      13

    2. 111 (Show answer)

      7

    3. 10010+01011 (Show answer)

           1
         10010  18
        +01011  11
        ------  --
         11101  29
      
  9. What is the Big-O worst-case time complexity of the following Python code? Assume that list11 and list2 are lists of the same length. Show answer.

     def difference(list1, list2):
         result = []
         for val1 in list1:
             if val1 not in list2:
                 result.append(val1)
         return result
    

    The outer loop has n iterations, while the in operation has a worst-case time complexity of n, so the total worst-case time complexity is \(O(n^2)\).

    The not in operator in this context is a shortcut for not (val1 in list2). The worst case time complexity for in on a list is \(O(n)\) because we potentially have to examine all the elements in the list. The average case is still \(O(n)\), because on average we will need to examine half the elements.

    How could solve this more efficiently? With the subtraction operator on sets.

  10. There are several problems with this recursive implementation of fibonacci. What are they? Show answer.

    def fibonacci(n):
        """ Return nth fibonacci number """
        if n == 1 or 2:
            return 1
        else:
            fibonacci(n[1:]) + fibonacci(n[2:])
    
    1. n == 1 or 2 is the same as (n == 1) or 2 and is always True because 2 always evaluates to True. Should be n == 1 or n == 2.
    2. n is an integer, and so the slicing operator is not defined. The recursive case should be n-1 and n-2.
    3. Missing return in the recursive case.
  11. Translate the following function using NumPy to just use Python built-ins. Assume a_list is a list of floats and lower is a single float: Show answer.

     def sum_above(a_list, lower):
         a_list = np.array(a_list)
         return np.sum(a_list[a_list > lower])
    
     def sum_above(a_list, lower):
         """ Sum all value in a_list greater than lower """
         result = 0
         for val in a_list:
             if val > lower:
                 result += val
         return result
    
  12. After the following code executes what are the values of b and c? Show answer.

    a = { 1: "a", 3: "c", 26: "z", 10: "j" }
    b = sorted(a.items())
    c = sorted(list(a.items()) + [(3,"a")])
    
    >>> b
    [(1, 'a'), (3, 'c'), (10, 'j'), (26, 'z')]
    >>> c
    [(1, 'a'), (3, 'a'), (3, 'c'), (10, 'j'), (26, 'z')]
    

    Recall that when tuples are compared, the first elements are compared, if equal, the second elements are compared and so on.

  13. One useful property of averages is that we can compute the average without having the values in memory by maintaining a “running” sum and count of the number of values observed. Implement a class RunningAverage that maintains a running average of values added with an add method. That is it can perform the following computation. Show answer.

     >>> mean = RunningAverage()
     >>> for val in range(1, 5):
         mean.add(val)
            
     >>> mean.average()
     2.5
     >>> mean.add(5)
     >>> mean.average()
     3.0
    
     class RunningAverage:
         def __init__(self):
             self.total = 0
             self.count = 0
            
         def add(self, value):
             self.total += value
             self.count += 1
            
         def average(self):
             return self.total / self.count