CS 61A /
midterm2.md
CS 61A Textbook Summary for Midterm 2
1. Recursive Functions (1.7)
Structure
def f(args):
if base_condition(args):
return base_value
else:
return combine(f(simpler_args), other computation)
Examples
def sum_digits(n):
if n < 10:
return n
else:
return sum_digits(n // 10) + n % 10
def fact(n):
if n == 1:
return 1
else:
return n * fact(n-1)
Mutual recursion:
def is_even(n):
if n == 0: return True
return is_odd(n-1)
def is_odd(n):
if n == 0: return False
return is_even(n-1)
Tree recursion:
def fib(n):
if n == 1: return 0
if n == 2: return 1
return fib(n-1) + fib(n-2)
Counting partitions:
def count_partitions(n, m):
if n == 0: return 1
if n < 0 or m == 0: return 0
return count_partitions(n-m, m) + count_partitions(n, m-1)
Notes
- Always have a base case.
- Avoid infinite recursion.
- Use memoization to optimize overlapping subproblems.
printcan trace recursive execution.
2. Introduction (2.1)
- Introduces structured program design:
- Start from functions → data abstraction → OOP → efficiency.
- Focus on separating what (specification) from how (implementation).
3. Data Abstraction (2.2)
Core Idea
- Separate interface (what you can do) from representation (how it's done).
Example: Rational Numbers
def rational(n, d):
g = gcd(n, d)
return (n//g, d//g)
def numer(x): return x[0]
def denom(x): return x[1]
def add_rationals(x, y):
return rational(numer(x)*denom(y) + numer(y)*denom(x),
denom(x)*denom(y))
Functional Pair
def pair(x, y):
def get(i):
if i == 0: return x
if i == 1: return y
return get
def select(p, i):
return p(i)
Rules
- Only access data via constructors/selectors.
- Changing representation shouldn't affect external code.
- Never cross the abstraction barrier.
4. Sequences (2.3)
Syntax
lst = [1, 2, 3]
lst[0] # indexing
len(lst) # length
lst + [4,5] # concatenation
lst * 2 # repetition
Iteration
for x in lst:
print(x)
for i in range(len(lst)):
print(lst[i])
Unpacking
pairs = [[1, 2], [3, 4]]
for x, y in pairs:
print(x, y)
Range
list(range(5)) # [0,1,2,3,4]
list(range(2, 7)) # [2,3,4,5,6]
Concept
- Treat sequences generically, not as lists specifically.
5. Mutable Data (2.4)
Mutability
- Data that can change after creation.
Lists
lst = [1,2,3]
lst[1] = 5
lst.append(9)
lst.pop()
Dictionaries
d = {}
d['x'] = 10
print(d['x'])
del d['x']
Nonlocal / Global
def outer():
x = 0
def inner():
nonlocal x
x += 1
inner()
return x
Notes
- Use mutation when needed; otherwise prefer immutability.
- Watch for side effects.
- Keep abstraction barriers.
6. Object-Oriented Programming (2.5)
Class Structure
class ClassName:
def __init__(self, x):
self.x = x
def method(self, y):
return self.x + y
obj = ClassName(3)
print(obj.method(4))
Inheritance
class Animal:
def speak(self): print("...")
class Dog(Animal):
def speak(self):
super().speak()
print("Woof")
Notes
self= instance reference.- Class attrs shared; instance attrs unique.
- Override methods for polymorphism.
- Prefer composition when inheritance isn't clear.
7. Object Abstraction (2.7)
Example
class Rational:
def __init__(self, n, d):
g = gcd(n, d)
self.numer = n // g
self.denom = d // g
def __repr__(self):
return f"{self.numer}/{self.denom}"
def __eq__(self, other):
return self.numer * other.denom == other.numer * self.denom
def __add__(self, other):
return Rational(self.numer * other.denom + other.numer * self.denom,
self.denom * other.denom)
Notes
- Use special methods (
__repr__,__eq__,__add__, etc.) for behavior. - Hide internal representation.
- Maintain consistent interface regardless of internals.
8. Efficiency (2.8)
Core Concepts
- Time complexity: growth rate vs input size.
- Space complexity: memory used.
- Asymptotic notation: Big-O, Θ, Ω.
Memoization
cache = {}
def fib(n):
if n in cache: return cache[n]
if n <= 2: result = n-1
else: result = fib(n-1) + fib(n-2)
cache[n] = result
return result
Fast Power
def fast_pow(x, n):
if n == 0: return 1
if n % 2 == 0:
y = fast_pow(x, n//2)
return y * y
return x * fast_pow(x, n-1)
Notes
- Use caching to avoid recomputation.
- Choose algorithms with lower growth orders.
- Balance time vs space.
9. Implicit Sequences (4.2)
Concept
- Infinite or lazy sequences — compute elements on demand.
Example
class Stream:
def __init__(self, first, compute_rest):
self.first = first
self._compute_rest = compute_rest
self._rest = None
@property
def rest(self):
if self._rest is None:
self._rest = self._compute_rest()
return self._rest
Notes
- Use
.firstand.restto access. - Avoid recomputation — cache results.
- Enables working with infinite data streams.
- Be mindful of recursion depth.
10. Recursion Deep Dive
10.1 Structure of Recursive Functions
def func(args):
if base_condition(args):
return base_value # base case
smaller = simplify(args)
return combine(func(smaller), args) # recursive case
- Base case: stops recursion, must be reached.
- Recursive case: must simplify input toward base.
- Each call creates a new stack frame with its own local variables.
10.2 Common Patterns
1. Linear recursion
Processes one subproblem at a time.
def sum_list(lst):
if not lst:
return 0
return lst[0] + sum_list(lst[1:])
- Stack depth proportional to input size
n. - Common for list traversal and counting.
2. Tree recursion
Multiple recursive calls per level.
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
- Exponential growth in calls.
- Use memoization to reduce to O(n).
3. Tail recursion (conceptual)
Recursive call is the last action.
def factorial(n, acc=1):
if n == 0:
return acc
return factorial(n-1, acc*n)
- Can be optimized into iteration (Python doesn't optimize tail calls).
- Useful when converting recursion to loops.
4. Mutual recursion
Functions calling each other.
def even(n):
if n == 0: return True
return odd(n-1)
def odd(n):
if n == 0: return False
return even(n-1)
- Used in alternating conditions (e.g. games, parity checks).
5. Helper recursion
Encapsulate state or accumulator in a helper function.
def reverse_list(lst):
def helper(rest, acc):
if not rest:
return acc
return helper(rest[1:], [rest[0]] + acc)
return helper(lst, [])
- Reduces arguments in main call.
- Keeps clean external API.
10.3 Recursion on Structures
Lists
def count_occurrences(lst, x):
if not lst:
return 0
return (lst[0] == x) + count_occurrences(lst[1:], x)
Trees
def tree_sum(t):
if not t:
return 0
return t.label + sum(tree_sum(b) for b in t.branches)
Nested data
def flatten(lst):
if not lst:
return []
first, rest = lst[0], lst[1:]
if isinstance(first, list):
return flatten(first) + flatten(rest)
return [first] + flatten(rest)
10.4 Optimization
Memoization
cache = {}
def f(x):
if x in cache: return cache[x]
result = compute(x)
cache[x] = result
return result
Divide & Conquer
Split into halves to reduce recursion depth.
def merge_sort(lst):
if len(lst) <= 1:
return lst
mid = len(lst)//2
left = merge_sort(lst[:mid])
right = merge_sort(lst[mid:])
return merge(left, right)