Counting Steps Instead of Seconds
Seconds depend on your laptop; step counts don't. Meet T(n) and the dominant term.
- explain why seconds are a poor way to compare two algorithms;
- write down a step-count formula T(n) for short pieces of code;
- find the dominant term of a formula and justify dropping the rest;
- show that constant factors do not change the growth pattern;
- decide which operation to count, and see why the choice never changes the dominant term;
- tell arithmetic growth (adding a step) from geometric growth (multiplying), and read log n out of the second;
- connect a step-count formula to the ratio column you measured last week.
7.1The problem with seconds
Last week you measured seconds, and seconds were useful. But they carry passengers. A time of 0.04 s describes:
- the algorithm — the thing you actually care about;
- the machine — a 2015 laptop or a 2026 server;
- the language — Python is far slower than C for the same steps;
- the moment — what else was running, whether the cache was warm.
Publish "0.04 seconds" and it is obsolete when you upgrade your laptop. Publish "one step per item in the list" and it is true forever, on every machine, in every language. That is why the field settled on counting steps as the primary description and uses timing as supporting evidence.
Step counting predicts; timing verifies. A theory that disagrees with the stopwatch is wrong, and a stopwatch reading you cannot explain is a mystery worth chasing. Every serious report in this course carries both.
7.2T(n): the step-count formula
T(n) is simply "how many steps this code takes when the input size is n".
We count assignments — the cost of storing one value — because they are easy to see and
roughly proportional to everything else.
def sum_to(n):
total = 0 # 1 assignment
for number in range(1, n + 1): # n assignments to 'number'
total = total + number # n assignments to 'total'
return total
So T(n) = 1 + 2n. For n = 10 that is 21 steps; for n = 1 000, 2 001; for
n = 1 000 000, 2 000 001.
Now the formula version of the same job:
def sum_formula(n):
return n * (n + 1) // 2 # a fixed handful of operations
Here T(n) = 3 or so, whatever n is. That single fact — the formula's step
count does not contain n — is exactly what you saw in week 5, when its
timings refused to grow.
7.3The dominant term
Real formulas are messier. Suppose you carefully count a piece of code and get:
T(n) = 5n² + 200n + 3000
Which part matters? Put numbers in and watch:
| n | 5n² | 200n | 3000 | share of total from 5n² |
|---|---|---|---|---|
| 10 | 500 | 2 000 | 3 000 | 9% |
| 100 | 50 000 | 20 000 | 3 000 | 68% |
| 1 000 | 5 000 000 | 200 000 | 3 000 | 96% |
| 100 000 | 50 000 000 000 | 20 000 000 | 3 000 | 99.96% |
For small inputs the constant 3 000 rules. But we are never interested in small inputs — small inputs are fast under any method. As n grows, the n² term takes over completely and everything else becomes rounding error.
So we throw away the small stuff and keep the shape:
- Keep only the fastest-growing term. 5n² + 200n + 3000 → 5n².
- Drop the constant multiplier. 5n² → n². The 5 is a property of your machine and your coding style, not of the method.
7.4Why dropping the 5 is honest
It feels like cheating. It is not, and the reason is worth a moment. Compare two methods:
- Method A: T(n) = 100n — clumsy, a hundred steps per item.
- Method B: T(n) = n² — tidy, but nested.
At n = 50, A costs 5 000 and B costs 2 500: B wins. At n = 100 they tie at 10 000. At n = 1 000, A costs 100 000 and B costs 1 000 000 — A wins by ten times. At n = 100 000, A wins by a factor of a thousand.
A constant factor buys you a fixed head start; a better growth pattern eventually wins by any margin you like. Buying a computer twice as fast halves your constant — and moves the crossover a little. Choosing a better algorithm changes the shape of the curve, and that is the only thing that survives more data.
In real work, if n is genuinely always small and always will be, a "worse" algorithm with a tiny constant can be the right choice. Analysis tells you which method wins eventually; engineering judgment tells you whether you live in "eventually". Say so explicitly in your reports rather than pretending the constant does not exist.
7.5Which operation do you count?
A fair question hangs over §7.2: we chose to count assignments, but why those? A loop also does comparisons, additions, and list look-ups. Would counting a different one change the answer?
Take a linear search and count the thing that actually does the work — the
comparison item == target:
def contains(data, target):
comparisons = 0
for item in data:
comparisons += 1
if item == target:
return True, comparisons
return False, comparisons
haystack = list(range(1000))
print(contains(haystack, 999)) # found at the very end
print(contains(haystack, -1)) # missing: full pass
In the worst case (target missing, or last) the loop makes n
comparisons — so counting comparisons gives T(n) = n. Counting assignments to
item would also give n. Counting the loop's additions: also n. Every
reasonable choice lands on "some constant times n", so the dominant term — the part we
keep — is n whichever operation you picked.
Counting comparisons might give T(n) = n; counting every basic operation in the loop body might give T(n) = 4n. Different constant, identical growth. Since we throw the constant away anyway (§7.3), the choice of operation cannot change the class. Pick the operation that is easiest to see, and say which one you counted.
This is also why the constant is machine-dependent and the shape is not. On a fast CPU a comparison might take 2 nanoseconds; on a slow one, 20. In C it is faster than in Python. Every one of those facts scales the constant up or down — none of them turns an n into an n². The growth pattern is a property of the algorithm; the constant is a property of everything else.
7.6Arithmetic growth vs geometric growth
There is one more distinction that decides an algorithm's class, and it hides in how a loop variable changes. Watch two loops that both stop at n:
def arithmetic_steps(n): # i goes 0, 1, 2, 3, ... up to n
steps = 0
i = 0
while i < n:
i = i + 1 # ADD a constant each pass
steps += 1
return steps
def geometric_steps(n): # i goes n, n/2, n/4, ... down to 1
steps = 0
i = n
while i > 1:
i = i // 2 # MULTIPLY (by 1/2) each pass
steps += 1
return steps
for n in [8, 1024, 1000000]:
print(f"n={n:>8} adding: {arithmetic_steps(n):>8} halving: {geometric_steps(n)}")
The gap is enormous, and it comes entirely from add versus multiply.
When a loop variable adds a fixed amount, it needs about n passes to
reach n — that is arithmetic growth, and it gives O(n). When a loop
variable multiplies (here, halves) each pass, it reaches its target in
only about log₂ n passes — geometric growth, and it gives
O(log n). Doubling n adds exactly one pass to the halving loop, which is the
"small fixed addition, not a multiple" row you spotted in last week's ratio table.
Look at what happens to the loop variable each pass. Adding a constant → linear in that variable. Multiplying by a constant → logarithmic. This single question separates a scan (O(n)) from a halving search like the one in week 8's binary search (O(log n)).
A second dominant-term example: the triangular loop
Nested loops are not always a clean n². Here the inner loop's length depends on the outer counter:
def triangular(n):
count = 0
for i in range(n):
for j in range(i): # runs i times, not n
count += 1
return countThe inner loop runs 0 times, then 1, then 2, … up to n−1. Add those up:
T(n) = 0 + 1 + 2 + … + (n−1) = n(n−1)/2 = ½n² − ½n → keep n²
Only half the full grid of n² is visited, yet the dominant term is still n²: the ½ is a constant multiplier, and we drop it. So this loop is O(n²) and its ratio column will still read ≈ 4 when you double n — exactly the behaviour you will meet again in week 9's "checking off" anagram solution. Half of a quadratic is still a quadratic.
7.7Counting practice
Three snippets. Count before reading the answer.
total = 0
for i in range(n):
total = total + i
for j in range(n):
total = total + jcount = 0
for i in range(n):
for j in range(n):
count = count + 1count = 0
for i in range(n):
for j in range(10):
count = count + 1Answers
A: two loops one after another, T(n) = 1 + 2n + 2n = 4n + 1 → keep n. Sequential loops add.
B: nested loops, T(n) = 1 + n² (counting the inner assignment) → keep n². Nested loops multiply.
C: nested, but the inner loop runs a fixed 10 times regardless of n: T(n) = 1 + 10n → keep n. This is the trap. A nested loop is only quadratic when both loops grow with the input.
"Nested loop means n²" is false. What matters is how many times each loop runs as a function of the input. Two loops side by side add; loops inside each other multiply; a loop with a fixed count is just a constant factor.
7.8Try it yourself
For each of snippets A, B and C above: write down T(n), then add a step counter and run at n = 10, 100 and 1 000. Does the counter match your formula exactly? Where does it differ, and why?
What to look for
The counter should match to within the one or two setup assignments you may have counted differently. That "off by one or two" is precisely the constant term the simplification rules tell you to discard.
Reduce each to its dominant term, no constants:
- T(n) = 3n + 17
- T(n) = n² + n + 1
- T(n) = 2n³ + 500n² + 10 000n
- T(n) = 42
- T(n) = 6n + 3n log n
Answers
n; n²; n³; a constant (does not grow); n log n — because n log n grows faster than n.
Write one function that does 100 steps per item (a loop of 100 inside a loop of n) and one that does a full nested pass (n inside n). Time both from n = 10 upward and find the n where the second becomes slower. Compare with the predicted crossover at n = 100.
What you should see
A crossover near n = 100, though the exact point wobbles with machine noise and Python overheads. The lesson is not the exact number: it is that the crossover exists, is predictable from the formulas, and that beyond it the gap only widens.
Instrument the worst-case contains from §7.5 twice: once counting
comparisons, once counting every assignment in the loop body. Run both at
n = 100, 200, 400 on a missing target. Do the two counts differ? Do they grow in the
same proportion? What does that tell you about which operation to count?
Answer
The raw counts differ by a constant factor (say n vs 3n), but both double when n doubles — the ratio column is identical. The choice of operation set the constant, not the shape, so either is fine as long as you state which you counted.
Predict T(n) for the triangular function in §7.6, then add a counter and
run it at n = 500, 1 000, 2 000. Does count match n(n−1)/2? What is the
ratio when n doubles, and why is it ≈ 4 even though only half the grid is visited?
Expected
count equals n(n−1)/2 exactly: 124 750, 499 500, 1 999 000. The ratio
is ≈ 4 because the dominant term is ½n² — doubling n multiplies ½n² by 4. The ½ is a
constant we drop; half a quadratic is still a quadratic.
Run arithmetic_steps and geometric_steps from §7.6 at
n = 1 000, 1 000 000 and 1 000 000 000. For each, say how the step count changes when
you multiply n by 1 000. Which one barely moves, and which log connects it to week 6's
"adds a fixed amount per doubling" row?
Expected
The adding loop's count multiplies by 1 000 each time (linear). The halving loop's count rises by only about 10 each time (log₂ 1000 ≈ 10) — that is O(log n). Doubling n adds exactly one step to the halving loop, which is precisely the logarithmic row of last week's ratio table.
7.9Self-check
T(n) = 4n² + 1000n + 50 000. Which term decides the behaviour for large n?
Two loops written one after the other, each over n items, give:
Method A takes 100n steps, method B takes n². For which inputs is B actually faster?
A loop variable starts at n and is halved each pass until it reaches 1. The number of passes grows like:
You count comparisons instead of assignments and get a different constant factor. The dominant term:
7.10Homework
- Create
AA_Week07.ipynband complete Tasks 1–6. - Take any three functions you wrote in weeks 3–6. For each: write T(n), simplify it, then verify with a step counter at three sizes.
- For one of them, put the predicted step counts and the measured seconds side by side in one table. Do they rise in the same proportions?
- Using Task 6, write two sentences contrasting arithmetic and geometric growth in your own words, and name one algorithm from an earlier week that is each kind.
- Write five sentences on this question: your colleague says "just buy a faster computer". Using your own crossover measurement from Task 3, explain when they are right and when they are badly wrong.
7.11Words from this week
| Term | Meaning in plain words |
|---|---|
| T(n) | The number of steps a piece of code takes for input size n. |
| dominant term | The fastest-growing part of a formula; the only part that matters for large n. |
| constant factor | The multiplier in front (the 5 in 5n²) — machine, language and style, not method. |
| basic operation | The step you choose to count (comparison, assignment…); it sets the constant, not the shape. |
| arithmetic growth | A variable that adds a constant each pass — reaches n in about n steps (linear). |
| geometric growth | A variable that multiplies each pass — reaches n in about log n steps. |
| crossover point | The input size where a method with better growth overtakes one with a smaller constant. |
| machine independence | The reason we count steps: the answer stays true when the hardware changes. |
7.12The RAM model — counting the way computer scientists do
You have been counting steps all week without asking a slightly awkward question: what
exactly is a step? Adding two numbers, comparing two letters, reading
data[i] out of a list — are those all one step, or does a big multiplication
cost more than a small one? The computer scientist Steven Skiena answers this with a
deliberately simple picture called the Random Access Machine, or RAM
model, and it is the quiet foundation under everything in this course.
The idea is a bargain. We agree to pretend that every simple operation costs exactly one time unit, and that this one unit is the same no matter what the values are. On this imaginary machine:
- arithmetic —
+,−,×, a division — is one step each; - an assignment
=is one step; - a comparison such as
a < borx == yis one step; - reading or writing one array slot,
data[i], is one step — the "random access" part, meaning any slot is reachable in the same single unit, whether it is the first or the millionth; - the bookkeeping of a function call — the one step of handing control over — counts too.
Once we accept that, the running time of a program is just the total number of
those steps it performs. That is the whole model. It is exactly the number your
step counters have been printing: a program that runs a one-step body n
times costs n, and one that runs it n² times costs
n². Counting assignments in §7.2 was a first, honest instance of RAM-model
counting — we simply picked one operation and tallied it.
Is this true? Not literally. On a real chip a multiply can take longer than an addition, a value already in cache is fetched far faster than one out in main memory, and a number too big to fit a machine word costs more still. The RAM model wilfully ignores all of that. It is a simplification, and a proud one — the same kind of move a physicist makes when they drop air resistance to see the shape of a falling body. What we buy with the simplification is enormous: we can compare two algorithms with pencil and paper, on no particular machine, in no particular language, and get an answer that stays true when the hardware is replaced. That is the machine independence you met in §7.1, now given a name and a rule.
Because we throw the constant factor away anyway (§7.3), the exact per-operation cost
never reaches the final answer. Whether a multiply "really" costs one unit or three,
the dominant term of a loop that runs n² times is still n².
The RAM model lets us stop arguing about nanoseconds and start comparing shapes — which
is the only comparison that survives the next generation of laptops.
7.13Chapter problem set — Skiena 2.10
Here are four classic exercises from Skiena's The Algorithm Design Manual, worked at our level. Three of them are little loop puzzles written in pseudocode; for each we will first rewrite the loops as readable Python — so you can drop them into a notebook and run them — then trace what they count and read off the growth. The fourth is about counting the real work inside a familiar formula. Reach for the same tools you have used all week: the RAM-model step count, the dominant term, and a Python counter to check the arithmetic.
Skiena gives this triple-nested loop and asks: what value does it return as a function
of n, and what is its worst-case running time in Big-O? Written out in Python:
def mystery(n):
r = 0
for i in range(1, n): # i = 1 .. n-1
for j in range(i + 1, n + 1): # j = i+1 .. n
for k in range(1, j + 1): # k = 1 .. j
r += 1
return rWorked solution
Start from the inside and work out. The innermost loop runs k = 1 .. j,
which is exactly j passes, and each pass adds 1 to r. So
the inner loop contributes j to the total. That means the whole function
is really adding up a pile of j-values:
r = Σi=1n−1 Σj=i+1n j
Do not panic at the double sum — the point is only its shape. There are three
loops stacked inside each other, and all three ranges grow with n: the
outer runs about n times, the middle about n times, and the
inner up to n times. A quantity that is "about n, times about n, times
about n" is cubic — proportional to n³. Every extra
factor of n from a nested loop that grows with the input multiplies the
work again, exactly the "loops inside each other multiply" rule from §7.7, here applied
three times.
So the returned value r is a cubic polynomial in n, and the
worst-case running time is Θ(n³), i.e. O(n³). (There is no early
return or data-dependent branch, so best and worst case are the same — the
loops always run to completion.) Confirm both claims with a counter, and watch the
tell-tale ×8 when you double n:
prev = None
for n in [5, 10, 20]:
r = mystery(n)
ratio = "-" if prev is None else f"{r / prev:.2f}x"
print(f"n={n:>3} r={r:>6} ratio {ratio}")
prev = r
The counter matches the value the function returns, and each doubling of n
multiplies r by roughly 8. A ×8 response to a ×2 input is the fingerprint
of cubic growth: 2³ = 8, just as a ×4 response meant an exponent of 2 in §7.6.
Counting by j instead of by the loops makes it exact: each value j (from 2 to n) is reached by j−1 choices of i, so r = Σj=2n j(j−1) = (n3 − n)/3. Check: n = 5 gives (125−5)/3 = 40, exactly what the function returns.
Answer: mystery(n) returns r = (n3 − n)/3, and its worst-case running time is Θ(n3). Doubling n multiplies the value by about 8, confirming the cube.
A second triple-nested loop, this time with the inner range depending on both counters. What does it return, and what is its running time? In Python:
def pesky(n):
r = 0
for i in range(1, n + 1): # i = 1 .. n
for j in range(1, i + 1): # j = 1 .. i
for k in range(j, i + j + 1): # k = j .. i+j
r += 1
return rWorked solution
Again begin at the innermost loop. It runs k = j .. i+j. The number of
integers from j to i+j inclusive is
(i + j) − j + 1 = i + 1.
Notice what happened: the j cancelled clean out. However far up the range
starts, its length is always i + 1 — it does not depend on
j at all. That is the whole trick of this problem. So the inner loop
contributes i + 1 every time, and the two outer loops just repeat that:
r = Σi=1n Σj=1i (i + 1) = Σi=1n i·(i + 1)
The middle line simplifies because the summand (i + 1) does not mention
j, so adding it up i times (as j runs from 1 to
i) is just multiplying it by i — giving i(i+1).
Now i(i+1) = i² + i, and summing a term that grows like i²
across n values of i lands us at something proportional to
n³. (The tidy closed form is
n(n+1)(n+2)/3, plainly a cubic — but you do not need it to see the shape.)
Confirm the value and the class with a counter, comparing against that closed form:
for n in [5, 10, 20]:
r = pesky(n)
formula = n * (n + 1) * (n + 2) // 3
print(f"n={n:>3} r={r:>5} n(n+1)(n+2)/3={formula:>5}")
The counter sits exactly on n(n+1)(n+2)/3. The doubling ratios here
(70 → 440 → 3080, about 6.3× then 7.0×) are still climbing towards 8 rather than sitting
on it, because at these small sizes the lower-order +n² and +n
parts of the cubic have not yet faded — push n higher and the ratio settles
on 8, exactly the "small inputs are noisy, the trend is cubic" story of §7.3.
Answer: The inner loop always runs (i+j)−j+1 = i+1 times,
so pesky(n) returns Σi=1n i(i+1) = n(n+1)(n+2)/3, a
cubic polynomial, and its running time is Θ(n³) = O(n³).
This one asks you to describe the work rather than compute a return value. The
pseudocode prints "foobar" from inside three nested loops (take n
even). (a) Write its running time T(n) as three nested summations; (b) simplify
to a Big-O class. In Python:
def foobar(n):
for i in range(1, n // 2 + 1): # i = 1 .. n/2
for j in range(i, n - i + 1): # j = i .. n-i
for k in range(1, j + 1): # k = 1 .. j
print("foobar")Worked solution
(a) The nested-sum form. Each loop becomes one summation sign, and the printed line — the thing we are counting — is the "1" being summed at the very centre. Reading the ranges straight off the loops:
T(n) = Σi=1n/2 Σj=in−i Σk=1j 1
The innermost sum, Σk=1j 1, is just "add 1 to yourself
j times", which equals j. Substituting that collapses the triple
sum to a double one:
T(n) = Σi=1n/2 Σj=in−i j
The inner sum now adds up the whole numbers from j = i up to
j = n − i. That is a run of consecutive integers, and a run of consecutive
integers up to about n adds up to something on the order of n²
(the triangular-number idea from §7.6: 1 + 2 + … + m ≈ m²/2). So the inner sum alone is
already quadratic in n for the early values of i.
(b) Simplify. We then repeat that quadratic amount of work for each
i from 1 up to n/2 — that is, about n more times.
"About n²" of work, done "about n" times, is cubic. So
T(n) = Θ(n³) = O(n³). The n/2 and the shrinking
i .. n−i window only change constant factors — half a cube is still a cube,
exactly as half a square was still a square in §7.6 — they do not touch the class.
Confirm the cube with a counter that tallies the prints instead of doing them:
def foobar_count(n):
total = 0
for i in range(1, n // 2 + 1):
for j in range(i, n - i + 1):
for k in range(1, j + 1):
total += 1 # one 'print' would fire here
return total
prev = None
for n in [10, 20, 40]:
t = foobar_count(n)
ratio = "-" if prev is None else f"{t / prev:.1f}x"
print(f"n={n:>3} prints={t:>5} ratio {ratio}")
prev = tEach doubling multiplies the print count by exactly 8 — a clean ×8 on a ×2, the signature of Θ(n³).
Show the work explicitly. The inner sum of j from i to n−i is [(n−i)(n−i+1) − (i−1)i]/2; writing n = 2m:
T(n) = Σ [ (2m-i)(2m-i+1) - (i-1)i ] / 2 (i = 1..m, n = 2m)
= Σ m(2m - 2i + 1)
# numerator simplifies to 2m(2m - 2i + 1)
= m [ (2m-1) + (2m-3) + ... + 1 ]
# the odd numbers 1 .. 2m-1
= m · m2 = m3 = (n/2)3 = n3/8
# the first m odd numbers sum to m^2Answer: (a) T(n) = Σi=1n/2 Σj=in−i Σk=1j 1,
which simplifies to Σi=1n/2 Σj=in−i j once the
inner sum of 1's is replaced by j. (b) Carrying the sums through exactly (write n = 2m; the inner sum of j from i to n−i summed over i = 1..m) collapses to the clean closed form T(n) = m3 = (n/2)3 = n3/8. Check: n = 4 gives 8 and n = 10 gives 125, both matching a direct count. So T(n) = n3/8 = Θ(n3) — confirmed by the ×8-per-doubling counter.
The straightforward way to evaluate a polynomial
p(x) = a₀ + a₁x + a₂x² + … + aₙxⁿ is to build each power of x as
you go. Skiena asks: (a) how many multiplications and additions does it do in the worst
case; (b) how many multiplications on average; (c) can you do better? The method, as Python:
def evaluate_naive(a, x):
# a is the list of coefficients: a[0], a[1], ..., a[n]
n = len(a) - 1
p = a[0]
xpower = 1
for i in range(1, n + 1):
xpower = x * xpower # one multiplication
p = p + a[i] * xpower # one multiplication, one addition
return pWorked solution
(a) Worst case. Look inside the loop and just count the arithmetic.
The line xpower = x * xpower is one multiplication. The line
p = p + a[i] * xpower is one multiplication
(a[i] * xpower) and one addition (p + …). So
every pass of the loop costs two multiplications and one addition, and
the loop runs n times (for i = 1 .. n). Total:
2n multiplications and n additions.
(b) Average case. Here is the quiet insight: there is no
if, no break, nothing in this loop that depends on the
values of the coefficients or of x. The loop always runs the same
n times and always does the same arithmetic each pass. When behaviour never
branches on the data, the average case cannot differ from the worst case — so the average
is also 2n multiplications. (Contrast the linear search of §7.5, whose
count did depend on where the target sat.)
(c) Can we do better? Yes — Horner's rule. The waste in the naive method
is that first multiplication, the one that keeps rebuilding xpower. Horner's
rule rewrites the polynomial by factoring x out repeatedly —
a₀ + x(a₁ + x(a₂ + … )) — so each step folds in one coefficient with a single
multiply-and-add:
def evaluate_horner(a, x):
n = len(a) - 1
p = a[n] # start from the top coefficient
for i in range(n - 1, -1, -1): # i = n-1, n-2, ..., 0
p = p * x + a[i] # one multiplication, one addition
return p
Now the loop body is a single p * x + a[i]: one multiplication and
one addition per pass, over n passes. That is n
multiplications and n additions — the additions are unchanged, but the
multiplications have been halved, from 2n to n. The
reason it needs only n multiplications is that it never builds the powers of
x separately; each existing running total is multiplied by x
exactly once as the next coefficient is folded in.
Check that both agree on the answer while the counts differ:
def naive_counts(a, x):
n = len(a) - 1; m = adds = 0; p = a[0]; xpower = 1
for i in range(1, n + 1):
xpower = x * xpower; m += 1
p = p + a[i] * xpower; m += 1; adds += 1
return p, m, adds
def horner_counts(a, x):
n = len(a) - 1; m = adds = 0; p = a[n]
for i in range(n - 1, -1, -1):
p = p * x + a[i]; m += 1; adds += 1
return p, m, adds
a = [2, -3, 1, 4] # 2 - 3x + x^2 + 4x^3, so n = 3
print("naive :", naive_counts(a, 2)) # (value, mults, adds)
print("horner:", horner_counts(a, 2))Both return 32, but the naive method spent 6 multiplications (2n = 2×3) where Horner's rule spent only 3 (n = 3). Same additions, half the multiplications — and both are still O(n), the best class possible since you must at least touch every coefficient once.
Answer: (a) 2n multiplications and n additions in the worst case.
(b) 2n multiplications on average too, because the loop has no data-dependent branch, so
average equals worst. (c) Yes — Horner's rule, p = p*x + a[i] evaluated from
the top coefficient down, uses only n multiplications and n additions, halving the
multiplications.
You can boil a step count down to its dominant term. Week 8 gives that shape its standard name and shorthand: Big-O.