TCX1002 (NUS Python) series: Notebook · Midterm cheatsheet · PE cheatsheet · Finals helpsheet (current) · Midterm reflection
Constraints & Scope
- 1 × A4 double-sided (confirmed Prof Jiang, Apr 18)
- Examplify fill-blank format — no compiler, no run-code
- Relaxed vs PE: recursion ✅ allowed, NumPy ✅ allowed
- TBC (still pending Apr 18 tutorial answers): OOP, regex
- Strategy anchor: decomposition into subproblems → recursive thinking (Prof Jiang Apr 18 lunch)
Scope Priorities
Tier 1 — exam-time first lookup (high frequency × tested-weak):
- NumPy distance matrix (Apr 18 live-walk → strongest signal)
reduce(from functools import reduce;reduce(fn, iter, init)arg order)- Backtracking template (base case =
[]notNone; combine[(curr,)] + res) - OOP
@property(TBC hedge — read-only / write-only) - Regex (TBC hedge — midterm + T3 evidence)
Tier 2 — secondary lookup (high freq × tested-OK):
- filter / sorted / map / lambda
- Recursive RLE + recursive shrink idioms (
% / //,s[1:],lst[1:]) - 2D Grid (zigzag + column-RTL)
- OOP class + inheritance + polymorphism
- try / except
- Mock Test Solutions MT1–MT10
Tier 3 — 1-line compressed (foundation, won’t blank):
- enumerate · defaultdict / Counter · @lru_cache · two-pointer merge · sliding window · LCS · wildcard match
READ-FIRST checklist (before filling any blank):
- What does the comment say the function returns? (list? tuple? bool?)
- What types are the args? (str? int? list of tuples?)
- What does the surrounding code do with the blank’s value?
NumPy — Distance Matrix Pattern
1import numpy as np
2
3# Build NxN pairwise distance matrix
4A = np.array(arr) # 1D list → np.ndarray
5D = (A[:, None] - A) ** 2 # squared diff, shape (N, N)
6M = np.abs(A[:, None] - A) # Manhattan diff, shape (N, N)
7
8# Poison diagonal so argmin/argmax skips self-vs-self
9np.fill_diagonal(D, D.max()) # finding CLOSEST → diag = max
10np.fill_diagonal(D, -1) # finding FARTHEST → diag = -1
11
12# Find extreme + convert flat idx → (i, j)
13flat = D.argmin() # OR D.argmax() — returns flat index
14i, j = np.unravel_index(flat, D.shape) # flat → (row, col) tuple
15
16# Key idioms:
17# A[:, None] = inserts new axis pos 1: (N,) → (N, 1)
18# (N,1) - (1,N) = broadcasting → (N, N), D[i,j] = A[i] - A[j]
19# argmin/argmax = return FLAT (1D) index → must unravel_index for (i, j)
20# fill_diagonal = overwrites D[i, i] in place
21
22# Higher-dim Euclidean (input = (N, D) matrix of points)
23diff = A[:, None, :] - A[None, :, :] # (N, N, D)
24E = (diff ** 2).sum(axis=-1) # (N, N), squared euclidean
25
26# === Worked examples ===
27
28# Closest pair — min squared diff (i < j)
29def min_sqdiff_pair(arr):
30 A = np.array(arr)
31 D = (A[:, None] - A) ** 2
32 np.fill_diagonal(D, D.max())
33 return np.unravel_index(D.argmin(), D.shape)
34# min_sqdiff_pair([1,4,7,10]) → (0,1) | (1,2) | (2,3)
35
36# Farthest pair — max abs diff (no fill_diagonal: diag=0 already min)
37def farthest_pair(arr):
38 A = np.array(arr)
39 M = np.abs(A[:, None] - A)
40 return np.unravel_index(M.argmax(), M.shape)
41# farthest_pair([3,7,1,10]) → (2,3) | (3,2)
Regex
1import re
2
3# findall — all matches as list. With groups () → only group content
4re.findall(r'\d+', "age 25, score 99") # ['25', '99']
5re.findall(r'(\d+)-(\d+)', "12-34 56-78") # [('12','34'), ('56','78')]
6
7# search — first match anywhere. Returns match obj or None — ALWAYS check
8m = re.search(r'(\d+)-(\d+)', "call 123-456")
9if m:
10 m.group(0) # '123-456' full match
11 m.group(1) # '123' first group
12
13# sub / split
14re.sub(r'\d+', 'X', "abc123def456") # 'abcXdefX'
15re.split(r'[,;\s]+', "a, b;c d") # ['a','b','c','d']
16
17# Greedy vs non-greedy
18re.findall(r'<.+>', '<a><b>') # ['<a><b>'] greedy
19re.findall(r'<.+?>', '<a><b>') # ['<a>','<b>'] non-greedy
20
21# Pattern cheat:
22# \d \D = digit / non-digit \w \W = word [a-zA-Z0-9_] / non-word
23# \s \S = whitespace / non \b = word boundary . = any except \n
24# + * ? = 1+ / 0+ / 0-or-1 {n} {n,m} = exact n / range
25# ^ $ = start / end (...) = capture group (?=...) = lookahead
OOP — class · inheritance · polymorphism · @property
1class Parent:
2 def __init__(self, name):
3 self.name = name
4 self._val = 0 # convention: leading _ = "private"
5 def method(self): return "base" # overridable
6 def __str__(self): return f"Parent({self.name})"
7 def __repr__(self): return f"Parent('{self.name}')"
8
9 @property # READ-ONLY: getter only
10 def val(self): return self._val
11
12 @property # WRITE-ONLY: getter raises
13 def deposit(self): raise AttributeError("write-only")
14 @deposit.setter
15 def deposit(self, amt): self._val += amt # x.deposit = 50 → triggers setter
16
17class Child(Parent): # inheritance
18 def __init__(self, name, extra):
19 super().__init__(name) # call parent __init__
20 self.extra = extra
21 def method(self): return "override" # polymorphism (no isinstance check)
22 def __str__(self): return f"Child({self.name})"
23
24# Use:
25c = Child("A", 9); c.method() # "override"
26c.val # 0 (read-only)
27c.deposit = 50; c.val # 50
28c.val = 99 # ⚠️ AttributeError (no setter)
Functional — filter · map · reduce · sorted · lambda
1from functools import reduce # ⚠️ MUST import — reduce is NOT builtin
2# reduce signature: reduce(fn, iterable, initial) ← 3rd arg = SEED (not iterable!)
3# lambda convention: lambda acc, x: ... ← acc FIRST
4
5# filter — keep items where fn returns True
6valid = list(filter(lambda x: x[1] != "NA", data))
7
8# map — apply fn to each. ⚠️ map/filter return one-shot iterators — wrap list() once
9upper = list(map(lambda s: s.upper(), words)) # OR: map(str.upper, words)
10
11# reduce — fold to single value
12total = reduce(lambda acc, x: acc + x, [1,2,3], 0) # 6
13prod = reduce(lambda acc, x: acc * x, [1,2,3,4], 1) # 24
14joined = reduce(lambda acc, x: acc + x, ["h","i","!"], "") # "hi!"
15
16# Build dict with reduce — MUTABLE acc (faster, MUST return acc!)
17def collect(acc, item):
18 acc.setdefault(item[0], []).append(item[1])
19 return acc
20result = reduce(collect, data, {})
21# Immutable style (slower but lambda-friendly)
22reduce(lambda acc, x: {**acc, x[0]: acc.get(x[0], 0) + x[1]}, data, {})
23
24# sorted — NEW list. .sort() = in-place, returns None
25sorted(data, key=lambda x: (-x[1], x[0])) # desc by [1], asc by [0] tie-break
26sorted(data, reverse=True) # OR explicit reverse flag
27# ⚠️ -x[N] negation trick: numbers ONLY (strings → TypeError)
28
29# ⚠️ Mutable default arg = shared across calls!
30def f(x, target=None): # safe pattern
31 if target is None: target = []
String Methods
1# Rule: x.method() not method(x). Free fns: len, int, str, sum, max, min, sorted, abs.
2" hello ".strip() # 'hello' both ends
3" hello ".lstrip() # 'hello ' left only
4" hello ".rstrip() # ' hello' right only
5"hello world".split() # ['hello', 'world'] by whitespace
6"a,b,c".split(",") # ['a', 'b', 'c'] by delimiter
7"a b c".split(" ") # ['a', 'b', '', 'c'] exact space (keeps empty!)
8# ⚠️ "".split() → [] but "".split(" ") → [""]
9", ".join(["a", "b", "c"]) # 'a, b, c' list → str
10"hello".replace("ll", "r") # 'hero'
11"hello".find("ll") # 2 (-1 if not found)
12"abcabc".count("a") # 2
13"hello".startswith("he") # True
14"file.py".endswith(".py") # True
15"hello".upper() # 'HELLO'
16"HELLO".lower() # 'hello'
17"hello world".title() # 'Hello World'
18"123".isdigit() # True
19"abc".isalpha() # True
20"abc123".isalnum() # True
21
22# ord / chr — character ↔ number
23ord('A') # 65 ord('a') # 97 ord('0') # 48
24chr(65) # 'A' chr(97) # 'a'
25ord('c') - ord('a') # 2 (letter → index)
26chr(ord('a') + 3) # 'd' (index → letter)
27chr((ord(c) - ord('a') + shift) % 26 + ord('a')) # Caesar cipher
List / Set / Dict Methods
1# INIT (1D / 2D / 3D)
2[0] * N # 1D, length N: [0,0,0,...]
3[[0] * C for _ in range(R)] # 2D, R rows × C cols ✓ SAFE
4[[[0] * D for _ in range(C)] for _ in range(R)] # 3D, R × C × D ✓ SAFE
5[[0] * C] * R # ⚠️ BROKEN — all rows same ref
6list(range(N)) # [0, 1, 2, ..., N-1]
7set() # empty set (NOT {} — that's dict!)
8{} # empty dict
9dict.fromkeys(['a','b','c'], 0) # {'a':0, 'b':0, 'c':0}
10
11# NumPy (if needed)
12np.zeros((R, C)) # 2D zeros
13np.ones((R, C, D)) # 3D ones
14np.full((R, C), 99) # 2D filled with 99
15np.arange(N).reshape(R, C) # 0..N-1 reshaped to RxC
16
17# LIST
18lst.append(x) # add to end (in-place)
19lst.insert(i, x) # insert at index (in-place)
20lst.pop() # remove + return last
21lst.pop(0) # remove + return first
22lst.remove(x) # remove first x (ValueError if missing)
23lst.index(x) # index of first x (ValueError if missing)
24lst.reverse() # in-place, returns None!
25lst.extend([1,2]) # add each item vs lst.append([1,2]) → nested
26# ⚠️ for n in nums: nums.remove(n) → skips elements; use [n for n in nums if ...]
27# ⚠️ y = x; y += [6] → mutates x! safe: y = y + [6]
28# ⚠️ a[:] = outer new, inner shared → deep: [row[:] for row in a]
29# ⚠️ (1,) = tuple (1) = int ← single-elem tuple needs the comma
30# ⚠️ tuple immutable: out[-1][1] += 1 crashes → out[-1] = (key, count+1)
31
32# SET
33s.add(x) # add one
34s.discard(x) # remove (no error if missing)
35s.remove(x) # remove (KeyError if missing)
36a & b # intersection
37a | b # union
38a - b # difference
39a ^ b # symmetric difference
40
41# COMPREHENSIONS
42[x**2 for x in lst if x > 0] # list
43{k: v for k, v in pairs} # dict
44{x for x in lst} # set
45[x for row in matrix for x in row] # flatten 2D
46
47# DICT TRICKS
48d.get(key, default) # no KeyError
49d.setdefault(key, []).append(item) # group without defaultdict
50counts[x] = counts.get(x, 0) + 1 # count without Counter
try / except
1try:
2 risky_thing()
3except ValueError: # catch specific
4 print("bad value")
5except (TypeError, KeyError): # catch multiple
6 print("wrong type or key")
7
8raise ValueError("insufficient balance") # throw your own
9raise AttributeError("write-only")
enumerate
1for i, val in enumerate(lst): # i=0,1,2...
2for i, val in enumerate(lst, start=1): # i=1,2,3...
Formatting & Type Checks
1import math # for ceil, gcd, sqrt, inf
2f"${balance:.2f}" # 2 decimal places
3isinstance(x, (int, float)) # True for subclasses too
4type(x) == int # exact match only
5# ⚠️ round(2.5) → 2 (banker's rounding!) — use math.ceil() to "round up"
Backtracking — Wishful Thinking
1# Pattern: choose → recurse → undo.
2# ⚠️ Base case = SUCCESS ([] / state / True), NOT None.
3# ⚠️ None = "all branches failed" only.
4def backtrack(remaining, state):
5 if not remaining:
6 return state # ✓ SUCCESS base
7 curr = remaining[0]
8 for option in get_options(curr):
9 if not is_valid(option, state): continue
10 apply(option, state) # CHOOSE
11 result = backtrack(remaining[1:], state)
12 if result is not None:
13 return result # combine: return [(curr, option)] + result
14 undo(option, state) # UNDO
15 return None # all failed → propagate fail
Result-builder variant (collect pairs/groups):
1def pair_up(items, ok):
2 if not items: return [] # ✓ success = [] (NOT None!)
3 first = items[0]
4 for partner in items[1:]:
5 if ok(first, partner):
6 rest = pair_up([x for x in items if x not in (first, partner)], ok)
7 if rest is not None:
8 return [(first, partner)] + rest # ⚠️ MUST prepend current
9 return None
Concrete — student scheduling:
1def assign(students, classes):
2 def bt(rem, cls, res):
3 if not rem: return res
4 curr = rem[0]
5 for pref in curr["preferences"]:
6 if cls[pref] == 0: continue
7 res.setdefault(pref, []).append(curr["name"])
8 cls[pref] -= 1
9 ans = bt(rem[1:], cls, res)
10 if ans is not None: return ans
11 cls[pref] += 1; res[pref].pop() # UNDO
12 return None
13 return bt(students, classes.copy(), {})
Edit Distance (Levenshtein) — canonical recursive template
1# Recursive shrink ops:
2# ints → n // 10 (REST) vs n % 10 (LAST digit) ← recursive call uses //
3# str → s[1:] list → lst[1:]
4# safe digit iter: int(ch) for ch in str(n) ← handles leading 0
5# Pattern: multi-choice recursion (match → no-cost; mismatch → 1 + min(3 sub-calls))
6# Same shape works for: LCS, wildcard match, sub-sequence problems.
7def edit_distance(s1, s2):
8 def dp(i, j):
9 if i == 0: return j # base 1: empty s1 → insert all of s2
10 if j == 0: return i # base 2: empty s2 → delete all of s1
11 if s1[i-1] == s2[j-1]: return dp(i-1, j-1) # match → no cost
12 return 1 + min(dp(i-1, j), # delete s1[i-1]
13 dp(i, j-1), # insert s2[j-1]
14 dp(i-1, j-1)) # replace
15 return dp(len(s1), len(s2))
RLE (Run-Length Encoding)
1# Group consecutive identical elements → [(key, count), ...]
2# Only `bucket` changes per problem (e.g. parity, char-type, vowel/cons)
3
4# Iterative (recursive form: see MT2 for canonical worked example)
5def rle(seq, bucket=lambda x: x):
6 out = []
7 for x in seq:
8 key = bucket(x)
9 if out and out[-1][0] == key:
10 out[-1] = (key, out[-1][1] + 1) # tuple immutable → replace
11 else:
12 out.append((key, 1))
13 return out
Two-Pointer Merge (sorted lists, O(M+N))
1def merge(l1, l2):
2 p1 = p2 = 0; out = []
3 while p1 < len(l1) and p2 < len(l2):
4 if l1[p1] <= l2[p2]: out.append(l1[p1]); p1 += 1
5 else: out.append(l2[p2]); p2 += 1
6 return out + l1[p1:] + l2[p2:]
Dense Ranking (1-2-2-3)
1# Same score = same rank. Next different score = rank+1 (no skip).
2def dense_rank(rows, key=lambda x: (-x[1], -x[2], -x[3], x[0])):
3 s = sorted(rows, key=key)
4 rank = 1; out = [(rank, *s[0])]
5 for i in range(1, len(s)):
6 if s[i][1:] != s[i-1][1:]: rank += 1
7 out.append((rank, *s[i]))
8 return out
Sliding Window (running sum, O(n))
1def sma(prices, w):
2 s = sum(prices[:w]); out = [None]*(w-1) + [s/w]
3 for i in range(w, len(prices)):
4 s += prices[i] - prices[i-w]; out.append(s/w)
5 return out
2D Grid
1# Setup: smallest N s.t. N*N >= len(s)
2N = 1
3while N * N < len(s): N += 1
4grid = [[" "] * N for _ in range(N)]
5
6# Zigzag (odd rows reversed)
7row, col = i // N, i % N
8if row % 2 == 1: col = N - col - 1
9
10# Column fill right-to-left
11for col_offset in range(N):
12 for row in range(N):
13 grid[row][N - col_offset - 1] = s[p]; p += 1
14 if p >= len(s): break
Formulas
1import math
2
3# Weighted average (e.g. GPA)
4sum(v * w for v, w in data) / sum(w for _, w in data)
5
6# Trimmed mean — drop highest + lowest
7(sum(scores) - max(scores) - min(scores)) / (len(scores) - 2)
8
9# Euclidean distance (straight line)
10math.sqrt(sum((a - b) ** 2 for a, b in zip(p1, p2)))
11
12# Manhattan distance (grid, no diagonals)
13sum(abs(a - b) for a, b in zip(p1, p2))
14
15# Prime check — trial division up to sqrt(n)
16def is_prime(n):
17 if n < 2: return False
18 for i in range(2, int(n**0.5) + 1):
19 if n % i == 0: return False
20 return True
21
22math.gcd(a, b) # built-in GCD
23
24# Newton's sqrt — stop when guess stabilises
25x_next = (x_prev + n / x_prev) / 2
26# converge: abs(x_next - x_prev) < tol
27
28math.ceil(total * pct / 100) # "top X% rounded up" — never round()
29day = i % 7 # circular indexing (wraparound)
30
31# Diagonal detection (same sum / same diff)
32same_pos_diag = (r1 + c1) == (r2 + c2)
33same_neg_diag = (r1 - c1) == (r2 - c2)
34
35# 90° rotation — perpendicular of (dx, dy) is (-dy, dx) or (dy, -dx)
36dx, dy = x2 - x1, y2 - y1
37c1, d1 = (x2 - dy, y2 + dx), (x1 - dy, y1 + dx) # CCW
38c2, d2 = (x2 + dy, y2 - dx), (x1 + dy, y1 - dx) # CW
39
40# Distance between 2D points (no math import needed)
41dist = ((x1 - x2)**2 + (y1 - y2)**2) ** 0.5
42# compare with tolerance: abs(dist - target) < 1e-9
MT1 — Char Type Map (Recursive)
1def f(s):
2 if not s: return ""
3 c = s[0]
4 if c.lower() in "aeiou": pre = "V"
5 elif c.isalpha(): pre = "C"
6 else: pre = "-"
7 return pre + f(s[1:])
MT2 — DNA Grouping (Recursive RLE)
1def group_bases(dna):
2 if not dna: return ()
3 rest = group_bases(dna[1:])
4 if rest and rest[0][0] == dna[0]:
5 return ((dna[0], rest[0][1] + 1),) + rest[1:]
6 return ((dna[0], 1),) + rest
MT3 — Zigzag Grid
1def zigzag_fill(s):
2 if not s: return [[]]
3 N = 1
4 while N * N < len(s): N += 1
5 grid = [[" "] * N for _ in range(N)]
6 for i in range(len(s)):
7 row, col = i // N, i % N
8 if row % 2 == 1: col = N - col - 1
9 grid[row][col] = s[i]
10 return grid
MT4 — Avg Rating (Functional)
1from functools import reduce
2def avg_rating(votes):
3 valid = list(filter(lambda x: x[1] != "NA", votes))
4 def collect(acc, item):
5 acc.setdefault(item[0], {"t": 0, "c": 0})
6 acc[item[0]]["t"] += item[1]; acc[item[0]]["c"] += 1
7 return acc
8 temp = reduce(collect, valid, {})
9 return {k: v["t"]/v["c"] for k, v in temp.items()}
MT7 — Column Grid R→L
1def col_fill_rtl(s):
2 if not s: return [[]]
3 N = 1
4 while N * N < len(s): N += 1
5 grid = [["?"] * N for _ in range(N)]
6 p = 0
7 for i in range(N):
8 for j in range(N):
9 grid[j][N - i - 1] = s[p]; p += 1
10 if p >= len(s): return grid
11 return grid
MT8 — High/Low Rating (Functional)
Approach 1: named function (clean, O(n), recommended)
1from functools import reduce
2def high_low(records):
3 valid = list(filter(lambda x: type(x[1]) == int, records))
4 def collect(acc, item):
5 k = item[0]
6 prev = acc.get(k, (float("inf"), float("-inf")))
7 acc[k] = (min(prev[0], item[1]), max(prev[1], item[1]))
8 return acc
9 return reduce(collect, valid, {})
Approach 2: pure lambda (one-liner flex, O(n²) — new dict each step)
1from functools import reduce
2def high_low(records):
3 valid = list(filter(lambda x: type(x[1]) == int, records))
4 return reduce(lambda acc, item: {**acc, item[0]: (
5 min(acc.get(item[0], (float("inf"), float("-inf")))[0], item[1]),
6 max(acc.get(item[0], (float("inf"), float("-inf")))[1], item[1]),
7 )}, valid, {})