Week 07 · Phase 3 · The language of growth

Counting Steps Instead of Seconds

Seconds depend on your laptop; step counts don't. Meet T(n) and the dominant term.

How do we compare algorithms without comparing computers?

Lesson

Choose one operation to count

Run in Colab · predict the result first
n = 5
count = 0
for i in range(n):
    for j in range(i):
        count += 1
print(count)
n = 5: only pairs with j < i are visited
i = 0
no pairs
i = 1
(1,0)
i = 2
(2,0)(2,1)
i = 3
(3,0)(3,1)(3,2)
i = 4
(4,0)(4,1)(4,2)(4,3)
Count the inner-body executionsT(n) = 0 + 1 + … + (n − 1) = n(n − 1)/2

At n = 5, T = 10. The counter counts the chosen action, not every machine instruction.

Combine blocks before simplifying

StructureExact count of body actionsGrowth
Two separate n-turn loopsn + n = 2nlinear
An n-turn loop inside anothern × n = n²quadratic
The triangular loop above(n² − n)/2quadratic
Repeatedly halve a positive integer⌊log₂ n⌋logarithmic

Read bounds carefully. An inner loop of fixed length 10 gives 10n actions, not n².

Keep exact counts for small inputs

Example countT(n) = 3n² + 5n + 2 ≈ 3n² for large n
n3n²5n + 2
137
1030052
10030,000502
Which is smaller: 100n or n²?100n = n² at n = 100, for n > 0

Below 100, n² is smaller; above 100, 100n is smaller. Growth classes describe the long run and do not settle every small-input comparison.

State what one step means

Our simple model treats a fixed-size number comparison, array access or arithmetic operation as one step. This makes the count independent of processor speed.

Safe classroom assumptionWhen it needs refinement
Compare small integers in one stepVery large integers need work proportional to their length.
Read one list position in one stepSearching for a value may read many positions.
One call is one line of codeThe function may contain a loop or a sort.

State the input size, the counted operation and the assumptions alongside T(n).

Practice

Practice questions

10 test questions · 3 written questions · 13 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

The running time of for i = 1 to n: for j = 1 to i: (constant work) is:

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

Answer: option C. The body runs 1 + 2 + … + n = n(n + 1)/2 times.

Question 2 · medium · course question

A loop performs one counted operation for each of n items. What is its exact operation count?

  1. 1
  2. n + 1
  3. n
  4. n²

Answer: option C. There is one counted operation per item, so the total is n. This excludes any setup or condition checks not included in the chosen count.

Question 3 · medium · course question

Two consecutive loops each perform one counted operation per item over n items. What is the total?

  1. 2n
  2. n²
  3. n
  4. 2ⁿ

Answer: option A. The loops run one after the other, so add n + n. Multiplication would describe one full loop nested inside each iteration of another.

Question 4 · medium · course question

An outer loop runs n times. Each time, an inner loop performs exactly 3 counted operations. What is the total?

  1. n + 3
  2. n³
  3. 3ⁿ
  4. 3n

Answer: option D. The inner count is the fixed number 3, independent of n. Repeating it n times gives 3n.

Question 5 · medium · course question

For T(n) = 4n² + 7n + 2, which term eventually dominates?

  1. 2
  2. 4n²
  3. 7n
  4. all three grow equally

Answer: option B. The quadratic term eventually grows faster than the linear term and the constant. Its coefficient affects size but not that ordering.

Question 6 · medium · course question

A maximum scan remembers the first item, then compares every remaining item once. How many comparisons are made for 6 items?

  1. 6
  2. 15
  3. 5
  4. 3

Answer: option C. The first item initialises the remembered maximum. Only the remaining 6 − 1 = 5 items require comparisons.

Question 7 · medium · course question

A pair-checking loop visits each unordered pair of 5 distinct positions exactly once. How many pairs does it check?

  1. 10
  2. 25
  3. 20
  4. 5

Answer: option A. Each of five positions has four partners, but that counts each unordered pair twice. The count is 5 × 4 / 2 = 10.

Question 8 · medium · course question

A search finds its target in the first position. What does that single trace establish?

  1. all inputs need one check
  2. the worst case is constant
  3. the list must be sorted
  4. this input needed one check

Answer: option D. The trace describes this input. Other placements, including a missing target, can require more work.

Question 9 · medium · course question

Which comparison makes operation counts meaningful across two implementations?

  1. count lines in one and comparisons in the other
  2. define the same kind of operation and input size for both
  3. count only operations with long variable names
  4. ignore every loop condition in only one implementation

Answer: option B. A cost comparison needs a consistent unit of work and input-size definition. Different counting conventions can produce incomparable numbers.

Question 10 · medium · course question

A loop performs n additions and then one final output action. If these are the only actions being counted, what is T(n)?

  1. n²
  2. n
  3. n + 1
  4. 1

Answer: option C. Add the n additions to the single output action. The exact count is n + 1; its eventual growth is linear.

Written questions

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

Question 11 · easy

Name the three parts of a proof by induction.

Answer & reasoning

the base case (it holds for the smallest size), the assumption or induction hypothesis (it holds for sizes up to n − 1), and the general case (using the assumption, it holds for size n).

Question 12 · medium

Prove by induction that 1 + 2 + 4 + … + 2ⁿ = 2ⁿ⁺¹ − 1.

Answer & reasoning

base case n = 0: 1 = 2¹ − 1. Assume it holds for n − 1, so 1 + … + 2ⁿ⁻¹ = 2ⁿ − 1. Add 2ⁿ to both sides: the left is the sum up to 2ⁿ and the right is 2ⁿ − 1 + 2ⁿ = 2ⁿ⁺¹ − 1.

Question 13 · hard

Prove that any comparison-based algorithm that finds the maximum of n distinct numbers must make at least n − 1 comparisons.

In simpler words: Count how many values must be ruled out as the maximum.

Starting hint: Every value except the winner must lose at least once.

Answer & reasoning
Step by step
  1. Initially each of n distinct values could be the maximum.
  2. A comparison rules out at most one new candidate: its loser.
  3. To leave one candidate, at least n−1 candidates must be ruled out. A running-maximum scan meets this bound.

the algorithm can only declare x the maximum if every other element has lost a comparison to something; otherwise an unlosing element could be the true maximum and the algorithm could not tell. Each comparison produces exactly one loser, and n − 1 elements must each lose at least once, so n − 1 comparisons are needed. The obvious scan achieves it.

Three core tasks

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

1. Trace

Draw the triangular-loop cells for n = 4. Count them.

Check your reasoning

Rows have 0, 1, 2, 3 cells: 6 total.

2. Calculate

Find the exact count for n = 100, then name its dominant term.

Check your reasoning

100 × 99 / 2 = 4,950. The dominant term is n²/2.

3. Change one thing

Replace range(i) with range(10). How does the count change?

Check your reasoning

The inner body now runs 10n times: linear instead of quadratic.

Explore the animations & more worked tasks

7.7Counting practice

Three snippets. Count before reading the answer.

snippet A
total = 0
for i in range(n):
    total = total + i
for j in range(n):
    total = total + j
snippet B
count = 0
for i in range(n):
    for j in range(n):
        count = count + 1
snippet C
count = 0
for i in range(n):
    for j in range(10):
        count = count + 1
Answers

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 for counter initialization and updates → keep n. This is the trap. A nested loop is only quadratic when both loops grow with the input.

The rule people get wrong

"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

Task 1 — count, then verify

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?

Animate it — count every assignment and compare with T(n)
What to look for

Using the same counting convention should give an exact match. Including only setup adds a constant; including loop-variable assignments or more operations per iteration may add input-dependent terms. Reconcile those choices before simplifying the count.

Task 2 — simplify

Reduce each to its dominant term, no constants:

  1. T(n) = 3n + 17
  2. T(n) = n² + n + 1
  3. T(n) = 2n³ + 500n² + 10 000n
  4. T(n) = 42
  5. T(n) = 6n + 3n log n
Animate it — watch each term's share as n grows
Answers

n; n²; n³; a constant (does not grow); n log n — because n log n grows faster than n.

Task 3 — the crossover, measured

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.

Animate it — find where n² overtakes 100n
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.

Task 4 — count a different operation

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?

Animate it — run three counters side by side
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.

Task 5 — the triangular loop

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?

Animate it — see which half of the grid gets 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.

Task 6 — adding vs multiplying

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?

Animate it — race adding against halving
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.

Check your understanding

7.9Self-check

T(n) = 4n² + 1000n + 50 000. Which term decides the behaviour for large n?

Growth beats size. At n = 1 000 the n² term is already 25 times everything else combined, and the gap keeps widening.

Two loops written one after the other, each over n items, give:

Sequential loops add. Only nesting multiplies.

Method A takes 100n steps, method B takes n². For which inputs is B actually faster?

They cross at n = 100. Below it the constant factor dominates and B wins; above it the growth pattern takes over and A wins by ever more.

A loop variable starts at n and is halved each pass until it reaches 1. The number of passes grows like:

Multiplying (here, halving) the variable each pass is geometric growth: it reaches the target in about log₂ n steps. Adding a constant instead would be linear.

You count comparisons instead of assignments and get a different constant factor. The dominant term:

Different operations differ by a constant factor, and we drop constants. The growth pattern belongs to the algorithm; the constant belongs to your choice of operation and your machine.
Extra material & reference
Optional depth · full technical reference

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.

Both, not either

Step counting establishes a result under a cost model; timing tests how that model relates to an implementation. A disagreement calls for checking the model, input scope and measurement conditions. 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 — as an explicit teaching cost model. Check that omitted work is bounded per iteration before using this count to describe overall growth.

count the assignments
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:

no loop at all
def sum_formula(n):
    return n * (n + 1) // 2            # a fixed handful of operations

If we count each arithmetic operation as one step, this expression uses a fixed number of arithmetic operations. Python integers can grow in bit length, so actual arithmetic cost is not constant for arbitrarily large integers. 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:

n5n²200n3000share of total from 5n²
105002 0003 0009%
10050 00020 0003 00068%
1 0005 000 000200 0003 00096%
100 00050 000 000 00020 000 0003 00099.96%

For small inputs the constant 3 000 can dominate, and such inputs may matter in practice. As n grows, the n² term dominates this polynomial: the relative shares of the linear and constant terms approach zero.

So we throw away the small stuff and keep the shape:

Two rules of simplification
  1. Keep only the fastest-growing term. 5n² + 200n + 3000 → 5n².
  2. Drop the constant multiplier. 5n² → n². A fixed multiplier can reflect the method and counting convention (or machine costs for timings), but does not change the growth class.

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.

When constants do matter

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:

count comparisons
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
(True, 1000) (False, 1000)

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.

The choice sets the constant, not the shape

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), these representative counts have the same class. This requires the chosen operation to track total work within constant factors; counting only a rare operation can miss the dominant work. State your counting convention.

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:

adding vs multiplying
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)}")
n= 8 adding: 8 halving: 3 n= 1024 adding: 1024 halving: 10 n= 1000000 adding: 1000000 halving: 19

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.

The one-line test

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:

inner loop grows with i
def triangular(n):
    count = 0
    for i in range(n):
        for j in range(i):       # runs i times, not n
            count += 1
    return count

The 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.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 < b or x == y is 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.

Why the small lie is worth it

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.

From the beginner notes · Lectures 1, 2, 3

Prove that some work cannot be avoided

To find the maximum of n distinct readings using comparisons, every reading except the winner must lose at least one comparison. A comparison can create only one new loser. Therefore at least n − 1 comparisons are necessary in the worst case.

A single scan achieves n − 1 comparisons, so its comparison count meets the lower bound. This is stronger than saying “my code looks short”: it explains why a different method that finds the answer by comparing values cannot remove that work.

State your model. The argument counts comparisons on distinct values; it does not count every instruction or memory access. To prove the answer and the count, state your assumptions, check the starting case, and explain why each next step keeps the claim true.

Engineering use. Use a maximum-temperature scan to separate a proof of the returned value from a claim about controller timing.

Learning goals & class plan
By the end of this week you can
  • 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;
  • state which operation is counted and justify whether its count represents the dominant work;
  • 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.
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

7.10Optional Studio Extension

Optional practice · no submission or deadline
  1. Create AA_Week07.ipynb and complete Tasks 1–6.
  2. 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.
  3. 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?
  4. 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.
  5. 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.
Optional reference · Words from this week

7.11Words from this week

TermMeaning in plain words
T(n)The number of steps a piece of code takes for input size n.
dominant termThe fastest-growing part of a formula; the only part that matters for large n.
constant factorThe multiplier in front (the 5 in 5n²) — machine, language and style, not method.
basic operationThe step you choose to count (comparison, assignment…); it sets the constant, not the shape.
arithmetic growthA variable that adds a constant each pass — reaches n in about n steps (linear).
geometric growthA variable that multiplies each pass — reaches n in about log n steps.
crossover pointThe input size where a method with better growth overtakes one with a smaller constant.
machine independenceThe reason we count steps: the answer stays true when the hardware changes.
Chapter problem set — Skiena 2.10

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.

Problem 2-1 · the mystery function

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:

mystery.py
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 r
Worked 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:

check the cubic growth
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
n= 5 r= 40 ratio - n= 10 r= 330 ratio 8.25x n= 20 r= 2660 ratio 8.06x

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.

Problem 2-2 · the pesky function

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:

pesky.py
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 r
Worked 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:

match the formula, watch the growth
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}")
n= 5 r= 70 n(n+1)(n+2)/3= 70 n= 10 r= 440 n(n+1)(n+2)/3= 440 n= 20 r= 3080 n(n+1)(n+2)/3= 3080

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 toward 8. These exact-count deviations are lower-order terms, not measurement noise.

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³).

Problem 2-36 · nested summations

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:

foobar.py
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:

count the foobars
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 = t
n= 10 prints= 125 ratio - n= 20 prints= 1000 ratio 8.0x n= 40 prints= 8000 ratio 8.0x

Each 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:

simplifying the sum
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^2

Answer: (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.

Problem 2-5 · evaluating a polynomial

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:

evaluate_naive.py
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 p
Worked 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:

evaluate_horner.py
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:

same value, fewer multiplications
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))
naive : (32, 6, 3) horner: (32, 3, 3)

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.

Where this leads

You can boil a step count down to its dominant term. Week 8 gives that shape its standard name and shorthand: Big-O.