Week 09 · Phase 3 · The language of growth

One Problem, Four Algorithms: The Anagram Story

The classic case study — four correct solutions, four completely different costs.

Same answer, same computer: why is one version a million times slower?

Lesson

Anagrams have equal letter counts

listen and silent: order changes, counts do not
letter
eilnst
listen
111111
silent
111111

First define the rules: here inputs contain only lowercase English letters, and every character counts. Repeated letters matter: aab and abb are not anagrams.

Four methods, one decision

MethodWork for length nMain idea
Cross off a matching letterO(n²)Search remaining positions for each letter.
Sort both stringsO(n log n)Equal sorted sequences have equal counts.
Try every permutationO(n · n!)Generate candidates and compare each.
Count k possible lettersO(n + k)Compare the two frequency arrays.

These are upper bounds. The permutation bound includes up to n work per candidate. Early mismatches can finish sooner. With a fixed alphabet k = 26, counting is linear.

Build a count, then cancel it

Run in Colab · predict the result first
def anagram(a, b):
    if len(a) != len(b):
        return False
    counts = [0] * 26
    for ch in a:
        counts[ord(ch) - ord("a")] += 1
    for ch in b:
        counts[ord(ch) - ord("a")] -= 1
    return all(c == 0 for c in counts)
Trace aab against abb; show only a and b
counts for aab
a: 2b: 1
subtract abb
a: 1b: −1

The final nonzero entries show the mismatch. This implementation requires a–z input; validate or normalise first if the input rules differ.

Compare on the same job

Frequency-array workn increments + n decrements + k checks

Test empty strings, repeated letters, unequal lengths, a valid pair and a same-length non-pair. Time each method on the same families. For general text, a dictionary counter can handle a larger alphabet; its lookup cost is average-case, and case/spacing rules must be explicit.

Practice

Practice questions

10 test questions · 2 written questions · 12 total

Each question is followed by its answer and explanation. Hard questions also include a starting hint and smaller reasoning steps.

Test questions

Choose one option. Cost questions state their model and whether the bound is tight, expected or worst-case. Here lg means log₂, and heap positions start at 1.

Question 1 · medium

For nonnegative cost functions f(n) = O(n²) and g(n) = O(n), what is the tightest upper bound on f(n)·g(n) guaranteed by this information?

  1. O(n)
  2. O(n²)
  3. O(n³)
  4. impossible to bound

Answer: option C. Products of bounds multiply.

Question 2 · medium · course question

Two strings are anagrams when they have:

  1. the same first character
  2. the same set of distinct characters only
  3. the same characters with the same multiplicities, in any order
  4. the same length only

Answer: option C. Multiplicity means the number of occurrences. For example, aab and aba are anagrams, but aab and abb are not.

Question 3 · medium · course question

Why does comparing only set("aab") and set("abb") give the wrong anagram result?

  1. sets discard repeated occurrences
  2. sets reverse strings
  3. sets require equal input lengths
  4. sets preserve every occurrence separately

Answer: option A. Both sets are {a, b}, but the strings contain different counts of a and b. Set equality loses the information needed here.

Question 4 · medium · course question

Which pair is an anagram pair under exact, case-sensitive character matching?

  1. ab and aa
  2. A and a
  3. abc and ab
  4. listen and silent

Answer: option D. listen and silent contain the same six characters once each. Case-sensitive matching treats A and a as different characters.

Question 5 · medium · course question

Before counting characters, what can safely reject two strings as non-anagrams?

  1. different first characters
  2. different lengths
  3. different last characters
  4. different existing order

Answer: option B. Anagrams preserve every occurrence, so their lengths must agree. Their first, last and intermediate positions may differ.

Question 6 · medium · course question

For exact character matching, which sorting-based test correctly checks anagrams?

  1. compare the first sorted characters only
  2. compare the unsorted strings only
  3. compare the complete sorted character sequences
  4. compare the number of distinct characters only

Answer: option C. Sorting places equal characters together while preserving multiplicity. Equal complete sorted sequences therefore encode the same character counts.

Question 7 · medium · course question

If both strings have length n, sorting each takes Θ(n log n) and comparing them takes Θ(n). What is the total asymptotic cost?

  1. Θ(n log n)
  2. Θ(n² log n)
  3. Θ(log n)
  4. Θ(n³)

Answer: option A. The phases are consecutive: two sorting costs plus one scan. Add them; the n log n term dominates the linear scan.

Question 8 · medium · course question

A count dictionary is built from "aab". After consuming one a and one b from a second string, what positive count remains?

  1. b: 1
  2. a: 2
  3. none
  4. a: 1

Answer: option D. The original counts are a:2 and b:1. Subtracting one of each leaves a:1 and b:0.

Question 9 · medium · course question

To treat spaces and capitalisation as irrelevant to anagrams, what should be done?

  1. silently remove arbitrary characters only from the first string
  2. define and apply the same normalisation rule to both strings
  3. compare only their lengths
  4. sort only the first string

Answer: option B. The definition of equality comes first. Apply the same stated normalisation, such as removing spaces and converting case, to both inputs.

Question 10 · medium · course question

For strings over a fixed 26-letter alphabet, what extra storage does a 26-counter frequency method need as string length n grows?

  1. Θ(n²) counters
  2. Θ(n) counters
  3. Θ(1) counters with respect to n
  4. no counters

Answer: option C. The number of counters stays at 26 regardless of n. This counts fixed-word counters; an unrestricted growing alphabet or bit-level integer analysis changes the assumptions.

Written questions

Read each question together with its explanation, trace or proof. Numbering continues from the test questions.

Question 11 · medium

Explain in words why Rabin-Karp can move its window one character to the right in O(1) time.

Answer & reasoning

With a fixed-word modulus and a precomputed α^(m−1) modulo that modulus, remove the outgoing character contribution, multiply by α, then add the incoming character, all modulo the modulus. That uses constant word operations. Without bounded arithmetic or the precomputed power, this constant-time claim needs qualification; confirming a hash match still compares characters.

Question 12 · medium

Given n integers and a target T, decide whether some two of them add up to T. Give an O(n) expected-time method and an O(n log n) worst-case method.

Answer & reasoning

Expected linear method: scan once with an initially empty hash set. For each x, test whether T − x is already present before inserting x, so the same record is never used twice. This permits two distinct records with equal values. For an O(n log n) worst-case comparison method, sort and move pointers inward while their positions differ; advance the left pointer if the sum is too small and the right pointer if it is too large. Include sorting and any required copy in the costs.

Three core tasks

Predict on paper, run the code, then explain any difference. Use the animations below to inspect individual steps.

1. Trace

Write the a/b counts for aab and aba, then aab and abb.

Check your reasoning

aab/aba both have a:2, b:1 → True. abb has a:1, b:2 → False.

2. Calculate

For n = 100 and k = 26, count the two character passes plus final count checks.

Check your reasoning

100 + 100 + 26 = 226, excluding initialisation and loop overhead.

3. Change one thing

Allow uppercase letters. What must change before using the 26-slot code?

Check your reasoning

Choose case-sensitive or case-insensitive rules. For case-insensitive English input, lowercase and validate a–z before indexing; otherwise choose a larger mapping.

Explore the animations & more worked tasks

9.9Try it yourself

Task 1 — instrument solution 1

Add a comparison counter to anagram_checking_off and report it for n = 100, 200, 400 on genuine anagram pairs. Compare the counts with the predicted n(n+1)/2.

Animate it — count the comparisons, then scale up to n = 400
Expected

Exactly 5 050, 20 100 and 80 200 — with doubling ratios approaching four, just as predicted.

Task 2 — how far can brute force go?

Time anagram_brute_force at n = 6, 7, 8, 9 (stop there, or earlier if slow). Use equal-length non-anagrams to force exhaustion, for example "a" * n and "a" * (n-1) + "b". Then predict n = 12 from the pattern, and say whether you would wait for it.

Animate it — watch every rearrangement, then extrapolate to n = 12
Expected

Under a constant-cost-per-candidate model, 12!/9! = 10 × 11 × 12 = 1 320. An illustrative one-second run at n = 9 predicts 22 minutes at n = 12 and 15!/9! = 3 603 600 seconds, about 41.7 days, at n = 15. The full n · n! model adds factors 12/9 and 15/9, giving about 29.3 minutes and 69.5 days. These are extrapolations for exhaustive searches, not guaranteed durations.

Task 3 — find the crossover

Sorting may beat counting at the sizes you test. Increase n within a practical limit (build inputs outside the timer) and look for a crossover. If none occurs, report the tested range and input family rather than inventing a crossover.

Animate it — race sort against count and report what you see
Task 4 — a fifth solution

Python has collections.Counter. Write anagram_counter(s1, s2) in one line using it, benchmark it against your solution 4, and explain why the class is the same but the time differs.

Animate it — run the 26-slot lists and Counter side by side
Solution
one line
from collections import Counter

def anagram_counter(s1, s2):
    return Counter(s1) == Counter(s2)

Expected O(n) time under ordinary hash-table assumptions: Counter visits each character and stores counts for the k distinct characters, using O(k) space. It also supports a broader alphabet than the 26-slot function. Implementation constants can differ; benchmark rather than assume it is faster.

Task 5 — a variation: are they permutations of digits?

Adapt the counting solution to decide whether two lists of integers (each 0–9) are rearrangements of each other — e.g. [3, 1, 4, 1] and [1, 4, 3, 1]. What is the class, and what changed from the letters version?

Animate it — add for a, subtract for b, check for zeros
Solution
digits version
def same_multiset(a, b):
    if len(a) != len(b):
        return False
    counts = [0] * 10          # one slot per digit 0-9
    for x in a:
        counts[x] += 1
    for x in b:
        counts[x] -= 1         # cancel out
    return all(c == 0 for c in counts)

Still O(n): two passes over the data and a fixed 10-slot check. The only change is a smaller alphabet (10 slots instead of 26) and a tidy trick — add for one list, subtract for the other, and everything should cancel to zero. Same idea, same class.

Task 6 — confirm by counting, not timing

Run the operation-counter from §9.7 for the checking-off and counting methods at n = 100, 200, 400, 800. Put the two ratio columns side by side and state, in one sentence each, what class the underlying count formula proves and whether the column agrees — without calling time.perf_counter.

Animate it — build both ratio columns without a stopwatch
Expected

The checking-off ratios approach 4 and counting ratios approach 2. Deriving n(n+1)/2 and 2n+26 for every genuine length-n anagram establishes Θ(n²) and Θ(n); the finite table checks those formulas rather than proving them alone.

Check your understanding

9.10Self-check

Why do we use an O(n log n) worst-case sorting upper bound for sorted(s1) == sorted(s2)?

The cost is inside the built-in. Short code can hide expensive work; always price the built-ins you call.

What buys the counting solution its linear time?

Trading a little space for a lot of time is one of the most reliable moves in computing — and the whole idea behind week 11.

Exhaustively checking all 20! positional permutations at ten million candidates per second is:

2.4 × 10¹⁸ arrangements is thousands of years at ten million per second. Factorial growth ends the conversation.

In the benchmark, the O(n log n) version beat the O(n) version at n = 1 000. Does that disprove the analysis?

Report the bound, input family and observed times, including whether any crossover was actually observed.

The checking-off formula n(n+1)/2 produces ratios approaching four when n doubles. Its tight growth is:

The formula derived for all valid anagram sizes proves the bound. A finite ratio table provides a check rather than a proof by itself.

Inputs are a dozen letters and the check runs a handful of times. Which do you ship, and why?

At tiny n the class barely matters; readability and correctness do. The counting version earns its keep only when the data is large or the call is hot.
Extra material & reference
Optional depth · full technical reference

9.1The problem

Two words are anagrams if one is a rearrangement of the other: "listen" and "silent"; "python" and "typhon". Write a function that takes two strings of the same length and returns True if they are anagrams.

It sounds trivial, and it is — the answer is easy to get right. What makes it the perfect teaching example is that four natural, correct approaches land in four different complexity classes. Same problem, same computer, same programmer: O(n²), O(n log n), O(n · n!) for the full brute-force code, and O(n).

For all four we will use the length of the words as n, and to keep the focus on the idea we assume only ASCII letters a–z. The 26-slot implementation does not support arbitrary Unicode lowercase characters, spaces or punctuation. It requires validation or an explicit normalization policy at a real interface. Unequal lengths return False; two empty strings are anagrams.

9.2Solution 1 — checking off

The way most people describe it out loud: take each letter of the first word and cross it off in the second; if every letter finds a partner, they are anagrams.

solution_1_checking_off.py
def anagram_checking_off(s1, s2):
    if len(s1) != len(s2):
        return False

    letters = list(s2)                # we need to cross letters off
    for letter in s1:
        found = False
        for i in range(len(letters)):     # search for a partner
            if letters[i] == letter:
                letters[i] = None         # cross it off
                found = True
                break
        if not found:
            return False
    return True

print(anagram_checking_off("listen", "silent"))   # True
print(anagram_checking_off("listen", "silence"))  # False

The cost. For each of the n letters of s1 we scan up to n positions of letters. That is a loop inside a loop, both over n:

For a genuine anagram: comparisons = 1 + 2 + … + n = n(n+1)/2  →  Θ(n²)

The list does not shrink: crossed-off entries are still inspected. On a genuine anagram, each original position is matched once, so the successful search lengths are exactly 1 through n in some order, even with repeated letters. Rejections may stop earlier. Worst-case time is Θ(n²); the list copy uses Θ(n) extra space.

9.3Solution 2 — sort and compare

A neat observation: two words are anagrams exactly when their letters, sorted, are identical. So sort both and compare.

solution_2_sort_and_compare.py
def anagram_sort(s1, s2):
    if len(s1) != len(s2):
        return False
    return sorted(s1) == sorted(s2)

Three lines, obviously correct, and it looks free — which is the trap. The work has not disappeared, it has moved inside sorted(), which is O(n log n) in a usual worst-case comparison-sort bound. Python’s adaptive sorting can do less work on particular inputs. The final comparison is O(n). Adding them and keeping the dominant term:

O(n log n) + O(n) → O(n log n)

Better than solution 1 and much shorter to write. This is the "sensible default" answer, and in most real work it is the right one.

The habit to build

Short code is not cheap code. Whenever a built-in does the heavy lifting, look up what it costs. The n log n expression is a growth bound, not an exact comparison count.

9.4Solution 3 — brute force (do not run this on long words)

The "try everything" approach: generate every possible rearrangement of the first word and check whether the second appears among them.

solution_3_brute_force.py
from itertools import permutations

def anagram_brute_force(s1, s2):
    if len(s1) != len(s2):
        return False
    for candidate in permutations(s1):
        if "".join(candidate) == s2:
            return True
    return False

It is correct, but exhaustive enumeration becomes impractical quickly. A word of n positions has n! positional permutations. Repeated characters create duplicate strings that permutations still emits. The table assumes an illustrative ten million candidates per second, not a measured rate:

Word length nArrangements n!Time at 10 million per second
5120instant
103 628 8000.4 seconds
151 307 674 368 0001.5 days
202 432 902 008 176 640 000about 7 700 years
251.55 × 10²⁵about 49 billion years

At that hypothetical rate, exhaustive enumeration becomes impractical for large n. The anagram problem is tractable because efficient alternatives exist. For this code, joining each candidate costs Θ(n), making exhaustive worst-case time Θ(n · n!), not O(n!) under a full character-operation count. Early success may stop much sooner. The generator avoids storing every result, but its state and current candidate use O(n) extra storage. Keep experimental runs at very small n.

9.5Solution 4 — count and compare

The winning move is to stop comparing letters with letters. Count how many times each letter occurs in each word, then compare the two tallies.

solution_4_count_and_compare.py
def anagram_count(s1, s2):
    if len(s1) != len(s2):
        return False

    counts1 = [0] * 26          # one slot per letter a-z
    counts2 = [0] * 26

    for letter in s1:                          # n steps
        counts1[ord(letter) - ord('a')] += 1
    for letter in s2:                          # n steps
        counts2[ord(letter) - ord('a')] += 1

    for i in range(26):                        # up to 26 checks; all 26 for anagrams
        if counts1[i] != counts2[i]:
            return False
    return True

ord('a') is 97, ord('c') is 99, so ord(letter) - ord('a') turns a letter into a slot number 0–25. Two passes over the words, then up to 26 comparisons. For a genuine pair, counting only tally updates and final comparisons gives exactly:

T(n) = 2n + 26  →  O(n)

Linear in the worst case — optimal in this character-access model, because a correct method must inspect all characters on some inputs. This need not happen on an early rejection. Notice what paid for it: extra memory (two lists of 26) and a cleverer question ("how many of each?" instead of "does this one match?"). That trade — spend a little space to save a lot of time — is one of the most reliable moves in all of computing, and week 11 is built entirely on it.

9.6Head to head

SolutionClassSteps at n = 1 000Extra memoryVerdict
1. Checking offΘ(n²) worst case500 500 comparisons for anagramsa copy of the wordFine for words, hopeless for large data
2. Sort and compareO(n log n) upper boundn log₂ n ≈ 10 000 is a scale estimate, not an exact counttwo sorted copiesExcellent default: short and fast enough
3. Brute forceΘ(n · n!) worst casen! candidates, Θ(n) work eachO(n) generator state and current candidateOnly tiny demonstrations
4. Count and compareΘ(n) worst case2 026 selected operations for anagramstwo 26-slot listsLinear model; measure implementation performance

Now measure it yourself rather than trusting the table. Long "words" made of random letters stand in for real ones:

benchmark_anagrams.py
import random, string, time

def make_pair(n):
    """Genuine anagrams; full checks for checking-off/counting, not every method’s worst case."""
    letters = [random.choice(string.ascii_lowercase) for _ in range(n)]
    shuffled = letters[:]
    random.shuffle(shuffled)
    return "".join(letters), "".join(shuffled)

def time_call(func, s1, s2, repeats=3):
    best = None
    for _ in range(repeats):
        start = time.perf_counter()
        func(s1, s2)
        t = time.perf_counter() - start
        if best is None or t < best:
            best = t
    return best

for n in [500, 1000, 2000, 4000]:
    s1, s2 = make_pair(n)
    a = time_call(anagram_checking_off, s1, s2)
    b = time_call(anagram_sort, s1, s2)
    d = time_call(anagram_count, s1, s2)
    print(f"n={n:>5}  checking-off {a:.5f}s   sort {b:.5f}s   count {d:.5f}s   speedup {a/d:,.0f}x")
n= 500 checking-off 0.00832s sort 0.00006s count 0.00011s speedup 76x n= 1000 checking-off 0.03310s sort 0.00013s count 0.00022s speedup 150x n= 2000 checking-off 0.13245s sort 0.00028s count 0.00044s speedup 301x n= 4000 checking-off 0.52901s sort 0.00061s count 0.00088s speedup 601x

The displayed timings illustrate possible output; run the code for measurements on your machine. A quadratic-versus-linear model predicts a growing speedup, but the particular factor at n = 40 000 must be measured or labelled as an extrapolation. Built-in sorting may outperform Python-level counting over a measured range because implementation constants and input structure matter. Record those conditions.

The lesson of the last paragraph

Tight growth models explain eventual trends under their assumptions; upper bounds alone do not guarantee which implementation wins. Python sorting is adaptive, and the fixed alphabet and input distribution matter. Look for a crossover in the optional extension, but report honestly if none is observed in the tested range.

9.7Counting operations to confirm the class

Timing is honest but noisy — it mixes in your CPU, the language and whatever else the machine is doing. A code-derived general operation formula can prove a bound under its model. Instrumented counts test that derivation at selected sizes. A tight quadratic model has ratios approaching four; a tight linear model has ratios approaching two. A table of a few ratios cannot prove how the work grows for all large inputs.

For checking off we count letter comparisons; for counting we count tally updates plus the final checks (all 26 on genuine anagrams). These selected operations exclude initialization and indexing arithmetic; they are not full Python instruction counts. Because anagram_sort hides its work inside C, we time it separately in the benchmark; here we compare the two we can instrument.

count_operations.py
import random, string

def make_pair(n):
    letters = [random.choice(string.ascii_lowercase) for _ in range(n)]
    shuffled = letters[:]
    random.shuffle(shuffled)
    return "".join(letters), "".join(shuffled)

def checking_off_ops(s1, s2):
    if len(s1) != len(s2):
        return 0
    ops = 0
    letters = list(s2)
    for letter in s1:
        found = False
        for i in range(len(letters)):
            ops += 1                         # one comparison
            if letters[i] == letter:
                letters[i] = None
                found = True
                break
        if not found:
            return ops
    return ops

def count_ops(s1, s2):
    if len(s1) != len(s2):
        return 0
    ops = 0
    c1 = [0] * 26
    c2 = [0] * 26
    for letter in s1:
        c1[ord(letter) - ord('a')] += 1; ops += 1
    for letter in s2:
        c2[ord(letter) - ord('a')] += 1; ops += 1
    for i in range(26):
        ops += 1
        if c1[i] != c2[i]:
            return ops
    return ops

print(f"{'n':>6}  {'checking-off':>14}  {'ratio':>6}   {'count':>8}  {'ratio':>6}")
prev_a = prev_c = None
for n in [250, 500, 1000, 2000]:
    s1, s2 = make_pair(n)
    a = checking_off_ops(s1, s2)
    c = count_ops(s1, s2)
    ra = "-" if prev_a is None else f"{a/prev_a:.1f}x"
    rc = "-" if prev_c is None else f"{c/prev_c:.1f}x"
    print(f"{n:>6}  {a:>14,}  {ra:>6}   {c:>8,}  {rc:>6}")
    prev_a, prev_c = a, c
n checking-off ratio count ratio 250 31,375 - 526 - 500 125,250 4.0x 1,026 2.0x 1000 500,500 4.0x 2,026 2.0x 2000 2,001,000 4.0x 4,026 2.0x

For these genuine anagrams, the counts are exactly n(n+1)/2 and 2n+26. The first doubling ratio approaches four and the second approaches two; neither is exactly its limiting value at every n. The formulas justify Θ(n²) and Θ(n), and the table checks the implementation. These counts are deterministic for this input family and do not depend on stopwatch noise.

Two tools, one verdict

A derived operation formula and compatible measurements support the model’s relevance. If ratios disagree, investigate noise, input cases, hidden work, lower-order terms and memory effects. A constant multiplier by itself cancels from a doubling ratio; it does not explain every disagreement.

9.8Which would you ship?

"Fastest class" is not automatically "best choice". Real decisions weigh speed against readability, memory, and the size of the data you will actually see. Line the four up against those questions and the answer stops being obvious:

SolutionReads clearly?Extra memoryWins when…
1. Checking offyes, mirrors the spoken ideaO(n)useful for tracing a simple matching strategy; usually avoid for large inputs
2. Sort and compareyes, one obvious lineO(n)ordinary word-sized inputs; you value short, clear code
3. Brute forcesimple exhaustive strategyO(n)tiny demonstrations; factorial work quickly becomes impractical
4. Count and comparea little more codeO(1)*huge inputs, or the same check run millions of times

* Two fixed 26-slot lists give O(1) slots under the word model. If alphabet size k varies, initialization and comparison cost O(k), total time is O(n+k), and extra space is O(k). Bit-level storage also depends on the size of the integer counters.

So which do you ship? For a word game where inputs are a dozen letters and the check runs occasionally, solution 2 is the right call: one line, obviously correct, fast enough that its worse class never bites. For a bioinformatics tool comparing million-character sequences, or a service that runs the check millions of times a second, solution 4 is a strong candidate when the input uses the fixed set of letters the method allows: it has linear worst-case time and constant extra slots. Validate its implementation and measure representative inputs against the actual latency and memory requirements. The professional answer is not a single winner; it is "solution 2 by default, solution 4 when the requirements and evidence support it". A crossover may or may not appear in the optional extension’s tested range.

Say the trade out loud

A good recommendation names what it is optimising for. "I chose sort-and-compare for readability because our words are short" is a decision. "I used sorting because it was first on the page" is an accident that happens to be fine until the data grows.

From the beginner notes · Lectures 6, 7, 8

Compare whole strategies, including preparation

The notes use sorting and hashing to turn repeated searching into organised work. To find two records whose values sum to T, one strategy sorts then moves pointers inward; another remembers earlier values in a hash set and looks for T − x.

Preparation matters. Sorting is part of the first method's total cost. Hashing needs extra storage, hashing that spreads keys well across storage slots and a rule that prevents using one record twice. Check the complement before inserting the current record, or track counts explicitly.

This is the same habit as comparing anagram methods: state the output contract, trace a small case, count the expensive operations, and include setup. A method that is fast on average can still have a slow worst case.

Engineering use. Use paired loads or calibration values as the test case. Include duplicate values and a case with no valid pair.

Learning goals & class plan
By the end of this week you can
  • solve one problem four different ways and compare the solutions properly;
  • see how a change of strategy — not a change of language — buys orders of magnitude;
  • recognise the counting trick that turns comparison into arithmetic;
  • confirm a complexity class by counting operations, not just by timing;
  • explain why factorial candidate enumeration becomes impractical, and include the work per candidate;
  • weigh readability, memory and speed to say which solution you would actually ship;
  • write a comparison report with a table, a plot and a recommendation.
Where this comes from

This case study is the heart of the classic Anagram Detection Example from Miller & Ranum's Problem Solving with Algorithms and Data Structures, rewritten here for a first-year audience with counters and benchmarks added.

Three-hour interactive studio

00:00–01:00: launch, predict–run–explain cycle, questions  ·  01:00–01:10: break  ·  01:10–02:00: worked variation, peer instruction, questions  ·  02:00–02:10: break  ·  02:10–03:00: core mechatronics practice, exam bridge, and exit ticket.

Ask at any point. Weekly self-checks stay private; optional extensions are not collected.

Need a slower explanation? Open the English + Türkçe reference guide.

Optional extra practice

9.11Optional Studio Extension

Optional practice · no submission or deadline
  1. Create AA_Week09.ipynb with all five solutions and complete Tasks 1–6.
  2. Run the operation-counter from §9.7 and produce a table with the two ratio columns; write one sentence under it naming the class justified by the count formula and checked by that column.
  3. Produce one figure with the three usable solutions plotted against n (log–log axes, labelled).
  4. Write a one-page comparison report: problem, four approaches, table of classes, your measurements, any crossover observed (or the range in which none was found), and a recommendation for (a) words in a game and (b) million-character biological sequences — say explicitly what each recommendation is optimising for.
  5. Answer in five sentences: solution 3 is correct and yet worthless. What does that tell you about "correct" as a standard for software?
Optional reference · Words from this week

9.12Words from this week

TermMeaning in plain words
anagramA word made of exactly the same letters as another.
brute forceTry every candidate systematically; correctness requires complete coverage and a correct test.
factorial n!n × (n−1) × … × 1 — the number of positional permutations; grows faster than cⁿ for every fixed c.
intractableInformally, too expensive for the intended input sizes and resources; a poor algorithm does not make its problem intractable.
operation countThe number of selected steps; a general derived formula proves a bound, while finite measurements check it.
space–time trade-offUsing extra memory to save time, or the reverse.
crossoverThe input size at which the better-scaling method overtakes the one with a smaller constant.
Chapter problem set — Skiena 2.10

9.13Chapter problem set — Skiena 2.10

These two problems from Skiena's The Algorithm Design Manual ask exactly the question week 9 keeps asking — two correct methods, two wildly different costs — but about a problem every child already knows: multiplying two numbers. Read each restated problem, have a go, then open the worked solution.

Problem 2-37 · multiplication by repeated addition

You first learned to multiply as repeated addition: 5 × 4 = 5 + 5 + 5 + 5. What is the time complexity of multiplying two n-digit numbers in base b this way, written as a function of n and b? (Hint: how large can the multiplier get?)

Worked solution

To compute x × y by repeated addition we add x to a running total y times, so the whole cost hangs on how big y can be. An n-digit number in base b is at most b to the power n, minus one — roughly bn. In base 10 a 3-digit number reaches 999 ≈ 10³; in base 2 a 3-bit number reaches 7 ≈ 2³. So y can force us to perform about bn additions.

The running product may need up to 2n digits; each digit-by-digit addition still costs O(n) — you add digit by digit, carrying as you go. Multiply the two together:

(about bn additions) × (O(n) per addition)  →  O(n · bn)

That bn is exponential in n — the same runaway growth week 9 called intractable. A table makes the hopelessness concrete for base 10:

Digits nLargest value ≈ 10ⁿAdditions needed (worst case)
19up to 9
3999up to ~1 000
6999 999up to ~1 000 000
10~10¹⁰up to ~10 billion
20~10²⁰more additions than there are seconds since the Big Bang

Answer: O(n · bn) — exponential in the number of digits. Repeated addition is fine for 5 × 4, but an n-digit multiplier can require nearly bⁿ additions. At twenty decimal digits the worst case approaches 10²⁰ additions, making this method impractical at ordinary processing rates. Correctness alone does not establish suitability for the intended input sizes.

Problem 2-38 · grade-school long multiplication

In grade school you learned to multiply digit by digit — long multiplication, with the partial rows lined up and added. What is the complexity of multiplying two n-digit numbers this way, for a fixed base?

Worked solution

Long multiplication pairs every digit of one number with every digit of the other. With n digits in each, that is n × n = n² single-digit multiplications. Each produces a small partial result, and we then line those partials up and add them — which is another O(n²) of work to combine (there are n rows, each up to about n digits wide).

n² single-digit products + O(n²) additions to combine  →  O(n²)

Now stand the two problems side by side — this is the whole point of the pair:

MethodClassMultiplying two 20-digit numbers
2-37 repeated additionO(n · bn)up to nearly 10²⁰ additions, each with O(n) digit work
2-38 long multiplicationO(n²)400 single-digit products plus carrying and row addition

Answer: O(n²). Long multiplication is astronomically better than repeated addition: for 20-digit inputs compare 400 single-digit products plus combining work against up to nearly 10²⁰ multi-digit additions. And notice what bought that gap — a smarter method, on the same machine, not faster hardware.

A note for the curious

n² is not the end of the story either. Cleverer methods do better still: Karatsuba's algorithm multiplies in about O(n1.585) by replacing four sub-multiplications with three, and modern FFT-based methods get close to O(n log n). Those belong to a later course — but it is worth knowing that even the grade-school n² can be beaten.

Where this leads

You saw that changing strategy — not language or hardware — buys orders of magnitude. Week 10 hunts those costs inside everyday Python list operations.