TCX1002 | Python Finals Helpsheet

A4 double-sided helpsheet for NUS TCX1002 Final Exam (May 4, Examplify fill-blank)

TCX1002 (NUS Python) series: Notebook · Midterm cheatsheet · PE cheatsheet · Finals helpsheet (current) · Midterm reflection

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, {})
comments powered by Disqus

Recent Updates

See all →