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?
Answer: option C. Products of bounds multiply.
The classic case study — four correct solutions, four completely different costs.
Same answer, same computer: why is one version a million times slower?
First define the rules: here inputs contain only lowercase English letters, and every character counts. Repeated letters matter: aab and abb are not anagrams.
| Method | Work for length n | Main idea |
|---|---|---|
| Cross off a matching letter | O(n²) | Search remaining positions for each letter. |
| Sort both strings | O(n log n) | Equal sorted sequences have equal counts. |
| Try every permutation | O(n · n!) | Generate candidates and compare each. |
| Count k possible letters | O(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.
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)The final nonzero entries show the mismatch. This implementation requires a–z input; validate or normalise first if the input rules differ.
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.
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.
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?
Answer: option C. Products of bounds multiply.
Question 2 · medium · course question
Two strings are anagrams when they have:
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?
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?
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?
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?
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?
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?
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?
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?
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.
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.
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.
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.
Predict on paper, run the code, then explain any difference. Use the animations below to inspect individual steps.
Write the a/b counts for aab and aba, then aab and abb.
aab/aba both have a:2, b:1 → True. abb has a:1, b:2 → False.
For n = 100 and k = 26, count the two character passes plus final count checks.
100 + 100 + 26 = 226, excluding initialisation and loop overhead.
Allow uppercase letters. What must change before using the 26-slot code?
Choose case-sensitive or case-insensitive rules. For case-insensitive English input, lowercase and validate a–z before indexing; otherwise choose a larger mapping.
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.
Exactly 5 050, 20 100 and 80 200 — with doubling ratios approaching four, just as predicted.
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.
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.
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.
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.
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.
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?
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.
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.
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.
Why do we use an O(n log n) worst-case sorting upper bound for sorted(s1) == sorted(s2)?
What buys the counting solution its linear time?
Exhaustively checking all 20! positional permutations at ten million candidates per second is:
In the benchmark, the O(n log n) version beat the O(n) version at n = 1 000. Does that disprove the analysis?
The checking-off formula n(n+1)/2 produces ratios approaching four when n doubles. Its tight growth is:
Inputs are a dozen letters and the check runs a handful of times. Which do you ship, and why?
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.
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.
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.
A neat observation: two words are anagrams exactly when their letters, sorted, are identical. So sort both and compare.
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.
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.
The "try everything" approach: generate every possible rearrangement of the first word and check whether the second appears among them.
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 n | Arrangements n! | Time at 10 million per second |
|---|---|---|
| 5 | 120 | instant |
| 10 | 3 628 800 | 0.4 seconds |
| 15 | 1 307 674 368 000 | 1.5 days |
| 20 | 2 432 902 008 176 640 000 | about 7 700 years |
| 25 | 1.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.
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.
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.
| Solution | Class | Steps at n = 1 000 | Extra memory | Verdict |
|---|---|---|---|---|
| 1. Checking off | Θ(n²) worst case | 500 500 comparisons for anagrams | a copy of the word | Fine for words, hopeless for large data |
| 2. Sort and compare | O(n log n) upper bound | n log₂ n ≈ 10 000 is a scale estimate, not an exact count | two sorted copies | Excellent default: short and fast enough |
| 3. Brute force | Θ(n · n!) worst case | n! candidates, Θ(n) work each | O(n) generator state and current candidate | Only tiny demonstrations |
| 4. Count and compare | Θ(n) worst case | 2 026 selected operations for anagrams | two 26-slot lists | Linear model; measure implementation performance |
Now measure it yourself rather than trusting the table. Long "words" made of random letters stand in for real ones:
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")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.
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.
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.
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, cFor 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.
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.
"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:
| Solution | Reads clearly? | Extra memory | Wins when… |
|---|---|---|---|
| 1. Checking off | yes, mirrors the spoken idea | O(n) | useful for tracing a simple matching strategy; usually avoid for large inputs |
| 2. Sort and compare | yes, one obvious line | O(n) | ordinary word-sized inputs; you value short, clear code |
| 3. Brute force | simple exhaustive strategy | O(n) | tiny demonstrations; factorial work quickly becomes impractical |
| 4. Count and compare | a little more code | O(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.
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
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.
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.
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.
AA_Week09.ipynb with all five solutions and complete Tasks 1–6.| Term | Meaning in plain words |
|---|---|
| anagram | A word made of exactly the same letters as another. |
| brute force | Try 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. |
| intractable | Informally, too expensive for the intended input sizes and resources; a poor algorithm does not make its problem intractable. |
| operation count | The number of selected steps; a general derived formula proves a bound, while finite measurements check it. |
| space–time trade-off | Using extra memory to save time, or the reverse. |
| crossover | The input size at which the better-scaling method overtakes the one with a smaller constant. |
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.
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?)
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 n | Largest value ≈ 10ⁿ | Additions needed (worst case) |
|---|---|---|
| 1 | 9 | up to 9 |
| 3 | 999 | up to ~1 000 |
| 6 | 999 999 | up 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.
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?
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:
| Method | Class | Multiplying two 20-digit numbers |
|---|---|---|
| 2-37 repeated addition | O(n · bn) | up to nearly 10²⁰ additions, each with O(n) digit work |
| 2-38 long multiplication | O(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.
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.
You saw that changing strategy — not language or hardware — buys orders of magnitude. Week 10 hunts those costs inside everyday Python list operations.