TCX1002 | Notebook

Exam-ready reference for NUS TCX1002: iteration traps, data structure gotchas, functional patterns, algorithm templates

NUS TCX1002 exam-focused gotchas & patterns. For general Python notes, see my Python Notebook.

 1TCX1002 | Python Notebook
 2
 3├── 1. Iteration Traps
 4   ├── Mutation during iteration (pop/remove shifts indices  crash)
 5   ├── Loop variable scope leaking (Python  JS/Java block scope)
 6   ├── range() locking (for: ONCE at start, while: every iteration)
 7   └── Safe patterns (backwards iteration, list comprehension copy)
 8
 9├── 2. Data Structure Gotchas
10   ├── List multiplication mutability ([[]] * 3 = shared refs!)
11   ├── Negative indexing silent bug (tree[-1] returns last, not error)
12   ├── Dict overwrites duplicate keys (use defaultdict)
13   ├── map() returns iterator (not list  must convert)
14   └── round() banker's rounding vs math.ceil()
15
16├── 3. Functional Programming
17   ├── Lambda with sorted() (key= required!)
18   ├── map / filter / reduce trio
19   ├── Functional composition (filtermapsum pipeline)
20   ├── Python 3: no tuple unpack in lambda (PEP 3113)
21   └── for...else pattern ("if not broken")
22
23├── 4. Algorithm Patterns
24   ├── Tuple (index, value) sorting pattern
25   ├── Sliding window template (+ O(1) optimization)
26   ├── K-length optimization (group by length)
27   └── Boyer-Moore string search (bad character heuristic)
28
29├── 5. Meta-Learning
30   ├── "Tests pass ≠ logic correct" (Q6 convergence)
31   └── "Tweak until green" anti-pattern (Q3 trailing whitespace)
32
33└── 6. Memory Tricks (speed reference)
34    ├── sorted() vs .sort() ("ED = nEw Data")
35    ├── Lambda = "Lazy function"
36    └── * = "explode operator"

1: Iteration Traps

1.1 Mutation During Iteration

Modifying a list while iterating over it causes index shifts that skip elements or crash.

1nums = [1, 2, 3, 4, 5]
2for i in range(len(nums)):
3    if nums[i] % 2 == 0:
4        nums.pop(i)  # IndexError! After pop, list shrinks but range doesn't

Why it crashes — the index shift trace:

StepiList beforeActionList after
10[1, 2, 3, 4, 5]1 is odd, skip[1, 2, 3, 4, 5]
21[1, 2, 3, 4, 5]2 is even, pop(1)[1, 3, 4, 5]
32[1, 3, 4, 5]4 is even, pop(2)[1, 3, 5]
43[1, 3, 5]IndexError! len=3, i=3💥

After pop(1) removes 2, everything shifts left: 3 slides from index 2 → index 1. But i increments to 2 regardless, so it now points at 4 (original index 3) — 3 was never checked! Then pop(2) shrinks the list again, and when i=3 the list only has 3 elements (indices 0-2), so nums[3] crashes with IndexError. The core problem: range(len(nums)) was locked to 5 at the start, but each pop() makes the list shorter.

Safe alternatives:

 1# Option 1: Backwards iteration (indices don't shift for earlier items)
 2for i in range(len(nums) - 1, -1, -1):
 3    if nums[i] % 2 == 0:
 4        nums.pop(i)
 5
 6# Option 2: List comprehension (creates new list, no mutation)
 7nums = [x for x in nums if x % 2 != 0]
 8
 9# Option 3: Copy and iterate
10for x in nums[:]:  # [:] makes a copy
11    if x % 2 == 0:
12        nums.remove(x)

1.2 Loop Variable Scope Leaking

Python loop variables survive after the loop — unlike Java/JS block scope.

1for x in range(3):
2    pass
3print(x)  # 2 (NOT an error!)
4
5# Even in list comprehensions (Python 2 leaked, Python 3 doesn't):
6result = [i for i in range(5)]
7# print(i)  # NameError in Python 3 (list comp has own scope)
8# BUT regular for loops still leak!

Trap: If you use x after a loop, it holds the last value from the iteration.


1.3 range() Evaluation Timing

 1# FOR loop: range() evaluated ONCE at start
 2x = [1, 2, 3]
 3for i in range(len(x)):  # range(3) — locked!
 4    x.append(99)          # List grows, but range stays 3
 5# Runs exactly 3 times (not infinite)
 6
 7# WHILE loop: condition re-evaluated EVERY iteration
 8x = [1, 2, 3]
 9i = 0
10while i < len(x):         # len(x) re-checked each time!
11    x.append(99)           # List grows AND condition updates
12    i += 1
13# Infinite loop!
Loop TypeWhen EvaluatedMutation Effect
for i in range(len(x))Once at startSafe from growth
while i < len(x)Every iterationDangerous with mutation

Note: N = len(x) before the loop is just readability — range(len(x)) is already locked.


2: Data Structure Gotchas

2.1 List Multiplication with Mutables

 1# SAFE — immutables (each is independent)
 2zeros = [0] * 5           # [0, 0, 0, 0, 0]
 3zeros[0] = 1              # [1, 0, 0, 0, 0] ✓
 4
 5# DANGEROUS — mutables (all share SAME reference!)
 6matrix = [[]] * 3         # [[], [], []]
 7matrix[0].append(1)       # [[1], [1], [1]]  ← ALL changed!
 8# matrix[0] is matrix[1] is matrix[2]  → True
 9
10# SAFE alternative
11matrix = [[] for _ in range(3)]  # Each [] is a NEW object
12matrix[0].append(1)              # [[1], [], []]  ✓

Rule: [mutable] * n creates n references to the same object. Use list comprehension for independent copies.


2.2 Negative Indexing (Silent Bug)

Python doesn’t error on negative indices — it wraps around:

1tree = [10, 20, 30]
2pointer = -1
3tree[pointer]  # 30 (last element, NOT an error!)

The trap: When using -1 as a sentinel value (like “no child” in BST):

1# ❌ Wrong — accesses last element instead of signaling "not found"
2(value, left, right) = tree[pointer]  # tree[-1] = last element!
3
4# ✅ Correct — check sentinel BEFORE indexing
5if pointer == -1:
6    return -1
7(value, left, right) = tree[pointer]  # Now safe

Lesson: Always validate bounds BEFORE array access when -1 is a sentinel.


2.3 Dict Overwrites Duplicate Keys

1rules = [("a", "X"), ("a", "Y")]
2d = {}
3for pattern, replacement in rules:
4    d[pattern] = replacement  # Last wins!
5# d = {"a": "Y"}  ← Lost "X"!

When you need first match wins (rule precedence), use defaultdict(list):

 1from collections import defaultdict
 2
 3grouped = defaultdict(list)
 4for pattern, replacement in rules:
 5    grouped[pattern].append((pattern, replacement))
 6
 7# Now iterate in insertion order:
 8for match, res in grouped["a"]:
 9    if match == target:
10        return res  # First match wins! ✓

When: Rule-based systems, config precedence, any time order matters for duplicate keys.


2.4 map() Returns Iterator (Not List)

1out = map(lambda x: x**2, [1, 2, 3])
2out[0]      # ❌ TypeError: 'map' object is not subscriptable
3len(out)    # ❌ TypeError: object of type 'map' has no len()
4out.sort()  # ❌ AttributeError: 'map' object has no attribute 'sort'

Fix: Convert or use iterator-accepting functions:

 1# Convert to list
 2out = list(map(lambda x: x**2, data))
 3out.sort()  # Now works!
 4
 5# Or use sorted() directly (accepts iterators)
 6out = sorted(map(lambda x: x**2, data))
 7
 8# These accept iterators natively (no conversion needed):
 9sum(map(...))     # ✓
10any(map(...))     # ✓
11all(map(...))     # ✓

Why iterators? Memory efficiency — map() computes on demand instead of loading everything into memory.


2.5 round() vs math.ceil()

 1import math
 2
 3# round() = nearest integer (banker's rounding!)
 4round(2.2)   # → 2
 5round(2.5)   # → 2  ← Surprise! Banker's rounds to nearest EVEN
 6round(2.6)   # → 3
 7round(3.5)   # → 4  ← Rounds to nearest even (4, not 3)
 8
 9# math.ceil() = always rounds UP
10math.ceil(2.2)   # → 3
11math.ceil(2.5)   # → 3
12math.ceil(2.0)   # → 2  (already integer)

Lesson: “Round up” in requirements = math.ceil(), NOT round(). Read carefully!


3: Functional Programming

3.1 Lambda with sorted()

1# ❌ Common mistake — missing key=
2sorted(data, lambda x: -x[1])       # TypeError!
3
4# ✅ Must use key= keyword
5sorted(data, key=lambda x: -x[1])   # Descending by index 1

90% use cases:

 1# Sort by specific element
 2sorted(data, key=lambda x: x[1])
 3
 4# Sort descending
 5sorted(nums, key=lambda x: -x)
 6
 7# Sort by multiple keys (first ascending, second descending)
 8sorted(data, key=lambda x: (x[0], -x[1]))
 9
10# Sort with string comparison
11sorted(words, key=lambda x: x.lower())

Memory trick: “Lambda = Lazy function” (too lazy to write def)


3.2 map / filter / reduce

 1from functools import reduce
 2
 3# filter: keep items matching condition
 4evens = filter(lambda x: x % 2 == 0, [1, 2, 3, 4])  # → 2, 4
 5
 6# map: transform each item
 7squares = map(lambda x: x**2, [1, 2, 3])  # → 1, 4, 9
 8
 9# reduce: aggregate to single value
10total = reduce(lambda a, b: a + b, [1, 2, 3, 4])  # → 10

Python vs JavaScript:

FeatureJavaScriptPython
Syntaxarray.map(fn)map(fn, array)
ReturnArray (eager)Iterator (lazy)
Chainingarr.filter().map()Nested: map(fn, filter(fn, arr))
reducearr.reduce(fn, init)reduce(fn, arr, init) (needs import)

Key: Python puts function first, data second. Args are flipped compared to JS.


3.3 Functional Composition (filter → map → sum)

1# Pipeline: filter → map → aggregate
2significant = filter(lambda x: abs(x) > threshold, changes)
3squared = map(lambda x: x**2, significant)
4result = sum(squared)
5
6# One-liner (read right-to-left, like math):
7result = sum(map(lambda x: x**2, filter(lambda x: abs(x) > threshold, changes)))

Tip: When readability suffers, use list comprehension instead:

1result = sum(x**2 for x in changes if abs(x) > threshold)

3.4 Python 3: No Tuple Unpack in Lambda (PEP 3113)

 1# ❌ Python 2 syntax (doesn't work in Python 3!)
 2map(lambda (k, v): (k, v**2), dict.items())  # SyntaxError
 3
 4# ✅ Python 3: use indexing
 5map(lambda kv: (kv[0], kv[1]**2), dict.items())
 6
 7# ✅ Or use a named function (clearer for complex logic)
 8def process(kv):
 9    k, v = kv  # Unpacking OK in regular functions!
10    return (k, v**2)
11
12# ✅ Or list comprehension (built-in unpacking)
13[(k, v**2) for k, v in dict.items()]

3.5 for…else Pattern

else on a for loop runs only if the loop completes without break.

1# "If not broken" pattern
2for item in collection:
3    if item == target:
4        print("Found!")
5        break
6else:
7    # Only runs if NO break (target not found)
8    print("Not found")

Mental model: Think of else as “if exhausted” or “if not broken”:

1for each item:
2    if found → break (skip else)
3else (if not broken):
4    handle not-found case

When to use:

  • Search with fallback (“if not found, do X”)
  • Validation loops (“if all items pass, proceed”)
  • Replacing found = False flag pattern

4: Algorithm Patterns

4.1 (index, value) Tuple Sorting

Problem: Need indices sorted by some computed value.

 1# Build (index, value) tuples
 2data = [(3, 1), (4, 1), (5, 1.67), (6, 2), (7, 1.33)]
 3
 4# Sort by value descending
 5sorted_data = sorted(data, key=lambda x: -x[1])
 6# → [(6, 2), (5, 1.67), (7, 1.33), (3, 1), (4, 1)]
 7
 8# Extract just indices
 9indices = [i for i, _ in sorted_data]
10# → [6, 5, 7, 3, 4]

Pattern reuse: Used in Q2 (Olympic ranking), Q8 (Moving Average) — same template, different problems.


4.2 Sliding Window

 1# Basic template
 2L = 0
 3R = window_size - 1
 4while R < len(data):
 5    window = data[L:R+1]  # Process current window
 6    L += 1
 7    R += 1
 8
 9# O(1) optimization: track running sum instead of re-slicing
10window_sum = sum(data[:window_size])
11for i in range(window_size, len(data)):
12    window_sum = window_sum - data[i - window_size] + data[i]
13    # O(1) per step instead of O(window_size)

4.3 K-Length Optimization (Group by Pattern Length)

Naive: Check all R rules at every position → O(N x R)

Optimized: Group by length, check only relevant lengths → O(N x K) where K = unique lengths

 1from collections import defaultdict
 2
 3# Pre-process: group patterns by length
 4by_length = defaultdict(list)
 5for pattern, replacement in rules:
 6    by_length[len(pattern)].append((pattern, replacement))
 7
 8# Search: longest first, only check matching lengths
 9for i in range(len(text)):
10    for length in sorted(by_length.keys(), reverse=True):
11        for pattern, replacement in by_length[length]:
12            if text[i:i+length] == pattern:
13                # Found match!
14                break

When K « R: Huge speedup. 100 rules but only 5 unique lengths = check 5 groups, not 100 patterns.


4.4 Boyer-Moore String Search (Bad Character Heuristic)

Uses adaptive jumps to skip unnecessary comparisons (faster than brute force):

 1def boyer_moore(text: str, pat: str) -> int:
 2    # 1. Build 'last' map: char → last index in pattern
 3    last = {}
 4    for idx, c in enumerate(pat):
 5        last[c] = idx
 6
 7    # 2. Search with adaptive jumps
 8    i = 0
 9    while i <= len(text) - len(pat):
10        j = 0
11        while j < len(pat) and text[i + j] == pat[j]:
12            j += 1
13
14        if j == len(pat):
15            return i  # Full match!
16
17        # Mismatch: compute shift
18        bad_char = text[i + j]
19        idx = last.get(bad_char, -1)
20        if idx == -1:
21            i += j + 1        # Char not in pattern → skip past it
22        else:
23            i += max(j - idx, 1)  # Align last occurrence, shift at least 1
24
25    return -1

Why it’s faster:

  • Bad char not in pattern → jump entire pattern length
  • Bad char in pattern → align its last occurrence

Common mistakes:

  • Variable name collision: for i, c in enumerate(pat) overwrites outer i
  • Wrong parameter to shift: pass j (pattern index), not i (text position)
  • Wrong slice: text[i:i+K] for K-length slice, NOT text[i:K+1]

Not a sliding window! Boyer-Moore jumps vary in size — it’s a two-pointer pattern.


5: Meta-Learning

5.1 “Tests Pass ≠ Logic Correct”

The Q6 Newton’s sqrt bug:

 1# ❌ Wrong convergence check (line 24)
 2if round(n, 5) == round(x_next, 5):
 3    return x_next
 4# Checks: "Is my guess equal to the TARGET number?"
 5# For sqrt(2.0): x_next == 2.0? (never true!)
 6
 7# ✅ Correct convergence check
 8if abs(x_next - x_prev) < tol:
 9    return x_next
10# Checks: "Has my guess STOPPED CHANGING?"

Why it “passed” tests: The wrong check NEVER triggered → ran all 1000 iterations → Newton’s formula converged anyway after ~10 iterations. Tests passed because the final answer was correct, but it wasted 990 iterations.

The two checks are asking completely different questions:

(a) Bug: round(n, 5) == round(x_next, 5) asks “is my guess equal to the input number?” For sqrt(2.0), this checks x_next == 2.0 — but the answer is 1.41421..., so the check is never true. The condition never fires, the loop runs all 1000 iterations, and Newton’s formula converges to the correct answer anyway by iteration ~10.

(b) Correct: abs(x_next - x_prev) < tol asks “has my guess stopped improving?” When consecutive guesses differ by less than the tolerance, the algorithm has converged — that’s the actual definition of convergence.

The accident: It only “worked” because max_iter=1000 gave Newton’s method enough runway. If max_iter=5, the wrong check still never triggers, but 5 iterations might not be enough to converge — you’d get an inaccurate answer. The correct check would return early at iteration ~10 with a precise result regardless of max_iter.


5.2 “Tweak Until Green” Anti-Pattern

The Q3 trailing whitespace mystery:

  • Output had 1 extra trailing space per row
  • Fixed by subtracting 1 in the last column: remaining -= 1
  • But couldn’t explain WHY — arrived at fix by trial and error

The danger:

1# What you THINK you learned:
2"Last column needs COLUMN_SIZE - 2"
3
4# What you ACTUALLY learned:
5"Subtracting 2 makes tests pass"  # ← Magic numbers!

Better approach:

  1. Stop. Write down your hypothesis
  2. Test hypothesis on paper (not in code)
  3. If hypothesis holds → implement
  4. If tests fail → hypothesis wrong, go to step 1

Senior SWE discipline: Derive the logic, don’t guess-and-check. Even when frustrated with “tedious” problems.


6: Memory Tricks (Speed Reference)

ConceptTrick
sorted() vs .sort()“sortED = nEw Data” (ED suffix = returns new list)
Lambda“Lambda = Lazy function” (too lazy to write def)
* operatorExplode operator” (flattens/unpacks)
map() return“Map = Maybe convert” (returns iterator, not list)
Merge complexityMust output M+N elements → can’t beat O(M+N)
Dense ranking1-2-2-3 (NOT 1-2-2-4) — rank increments only when values change
Backwards iterationReverse to Remove” (safe mutation pattern)
for…else“else = if not broken

Quick Reference: Python vs JavaScript

FeatureJavaScriptPython
Block scopelet/const block-scopedLoop vars leak after loop
Array methodsarr.map(fn)map(fn, arr) — args flipped
map() returnArray (eager)Iterator (lazy)
Negative indexarr.at(-1) (ES2022)arr[-1] (always works, silent)
Round upMath.ceil()math.ceil()
Tuple unpackDestructuring everywhereNot in lambda (PEP 3113)
for…elseN/Aelse runs if no break
List copy[...arr]arr[:] or arr.copy()

Last updated: 2026-02-10 Sources: T2 REVIEW, T3 notes (8 files), Jan 29 session notes Midterm: Feb 14, 10:30am

comments powered by Disqus