← back to CS 115
Week 9Guttag §Chapter 9233 min full read
7 concepts17 worked examples27 exercises4 exam-level7 figures
What are you here for?

09 A simplistic Introduction to Algorithmic Complexity (Chapter 9)

Start with this

One question before you read anything. Getting it wrong is the point: it shows you what this section is for.

§09.0 — how many times does the inner line run

Three loops over the same list of five items. None of them does anything useful; the only question is how many times the line marked with a comment is reached.

L = [10, 20, 30, 40, 50]

first = 0
for x in L:
    first = first + 1        # line A

second = 0
for i in range(len(L)):
    for j in range(len(L)):
        second = second + 1  # line B

third = 0
for i in range(len(L)):
    for j in range(2):
        third = third + 1    # line C

print(first, second, third)
Find(a) Write the line this program prints.
Given
  • L has five items.

  • The three counters all start at 0.

IPython console
Hint 1/4

You are being asked for three counts, one per loop.

Hint 2/4

A for over a sequence of k items runs its body k times.

Hint 3/4

Here the list has five items, so loop A runs 5 times. Loop B is five outer passes each with five inner passes. Loop C is five outer passes each with two inner passes.

Hint 4/4

The three counts are 5, 25 and 10.

Show solution

Count each loop separately and write the three numbers down before combining anything.

Price the single loop

$$\texttt{for x in L}$$

L holds five items, so the body runs five times, once per item

$$\texttt{first} = 5$$

the body adds one each pass and started from zero

Price the two nested loops

$$5 \times 5 = 25$$

the inner loop restarts in full on each of the five outer passes, so the pair counts multiply

$$5 \times 2 = 10$$

same shape, but the inner range does not depend on the list, so each outer pass costs two and not five

Answer $$\boxed{5\quad 25\quad 10}$$
Check

Independent check: line B is reached once for every ordered pair of positions in a five item list, and there are 25 such pairs, which is the same number arrived at without counting loop passes at all.

The count of an inner line is a product, and which factors are in that product is decided by what the ranges depend on.

What this looks like in Spyder
The Variable Explorer tab showing a table with Name, Type, Size and Value columns for the names the program created.

The tab next to Help. It lists every name your program left behind with its type and its size, so it answers the one question a listing cannot: what is in this name right now. The stepper on this page shows the same four columns.

Spyder 5.5.1 on Linux, running the program in its own editor. A different version moves a few toolbar icons; the panes are named the same.

Two students hand in the same lab. Both programs print the right answer for the four item test list on the lab sheet, and the grader is about to give both full marks. Then somebody runs them on a list of four hundred items. One program answers while you blink. The other is still going, and it is not stuck: it is doing exactly what it was written to do, one hundred and sixty thousand times.

def pair_count(L):
    """Assumes L is a list. Returns how many times the inner line runs."""
    runs = 0
    for i in range(len(L)):
        for j in range(len(L)):
            runs = runs + 1
    return runs


def triple_count(L):
    """Assumes L is a list. Returns how many times the inner line runs."""
    runs = 0
    for i in range(len(L)):
        for j in range(3):
            runs = runs + 1
    return runs


for size in [4, 40, 400]:
    L = list(range(size))
    print(size, pair_count(L), triple_count(L))

Sample Run:

4 16 12
40 1600 120
400 160000 1200

At four items the slow one is the cheaper of the two. At four hundred it is one hundred and thirty three times dearer. Nothing about the two functions changed between those rows except the length of the list.

By the end of this section you can look at a function you have never seen, say how many times its inner line runs as a formula in the size of its input, turn that formula into one of the six named classes, and say which of two functions will still be usable when the input gets a hundred times bigger.

In 60 seconds

Complexity is a count, not a clock.

One pass of a loop body, priced
$$\text{steps} = (\text{steps per pass}) \times (\text{number of passes})$$

Every loop. Price the body once, then count how many times the body runs. Those two numbers multiply, they do not add.

The two rules that turn a count into a class
$$\text{keep the fastest growing term},\quad \text{then drop its constant factor}$$

Any time you have an exact step count such as 5n+2 and the question asks for the complexity. Apply the rules in that order.

Big O is an upper bound on growth
$$f(n) = O(g(n)) \iff \exists\, c, n_0:\ f(n) \le c\,g(n)\ \text{for all } n \ge n_0$$

When you are asked what the notation means, or when you want to justify dropping a term: the bound only has to hold past some size.

Nested loops multiply, loops in a row add
$$O(n)\ \text{inside}\ O(n) \to O(n^{2}),\qquad O(n)\ \text{then}\ O(n) \to O(n)$$

Reading a class off code. This single rule decides most exam answers, and getting it backwards is the commonest way to lose the mark.

Halving a number n times gives the logarithm
$$n \to \tfrac n2 \to \tfrac n4 \to \cdots \to 1 \quad\text{takes about } \log_2 n \text{ passes}$$

A while loop whose counter is divided rather than decreased. That loop is logarithmic, not linear.

The is the promise, the is the luck
$$\text{worst}(n) = \max_{\text{inputs of size } n} \text{steps}$$

Any question that does not say which case it wants. Report the worst case, and say that is what you reported.

Three most common mistakes
  1. Adding nested loops instead of multiplying them.

  2. Writing the constant inside the name, as O(3n) or O(n/2).

  3. Pricing a list method as one step.

Labs count 20 per cent of the course mark, the midterm 40 and the final 40. This material comes after the midterm, so the two places it is marked are the lab and the final. In the one autumn term recorded in the course material the lab mark was given in coarse steps, the lowest of ten labs was dropped, and there was no makeup lab.

How much time do you have?
10 minutes

The two rules and the multiply against add rule.

The 60-second card · Why 5n + 2 and 5n give the same answer · Reading the class straight off the loops · Formula card
45 minutes

Everything you need to write a justified answer rather than a guessed one: a step count in terms of n, the case you are reporting, and the class with the working shown.

The 60-second card · Counting the steps a program takes, instead of timing it · Why 5n + 2 and 5n give the same answer · Best, worst and average case, and which one to report · The six growth rates the course names, and the loop that makes each one · Reading the class straight off the loops · Scaffolding comes off · practice A · practice C
full read

Also the parts that the lab asks for rather than the exam: the of list methods, when memory is the price you pay for speed, and how to check a claimed class by doubling the input and watching the count.

The opening pages · Recall first · Counting the steps a program takes, instead of timing it · Why 5n + 2 and 5n give the same answer · Big O · Best, worst and average case, and which one to report · The six growth rates the course names, and the loop that makes each one · Reading the class straight off the loops · Space against time, and paying a cost once to spread it over many uses · Method boxes · Look-alike pairs · Scaffolding comes off · Full exam-style question · practice A · practice B · practice C · practice D · Mistake ledger · Check yourself
By the end of this section
  1. Count the of a short function as an exact formula in the size of its input, and say which lines contributed which part of that formula.

  2. Simplify an exact step count to a complexity class by keeping the fastest growing term and dropping its , in that order.

  3. State what the claims, including the two constants it is allowed to choose, and use it to justify dropping a term rather than just asserting it.

  4. Distinguish the best, worst and average case of the same function, and report the worst case when a question does not say which one it wants.

  5. Name the six growth rates the course uses, give a loop shape that produces each one, and order two of them by which will cost more at a large .

  6. Derive the class of a function you have not seen before straight from its loop structure, multiplying nested loops and adding loops that follow one another.

  7. Trade memory for time by building a lookup structure once and say from how many uses onward that trade pays for itself.

Syllabus coverage

A simplistic Introduction to Algorithmic Complexity — covered

  • Why a stopwatch cannot compare two algorithms
  • counting basic steps instead
  • the exact count of a loop as a formula in the input size
  • dropping and constant factors
  • the big O notation as an upper bound on growth
  • best, worst and average case
  • the six named growth rates with a loop shape for each
  • reading a class off nested and sequential loops
  • the hidden cost of the list operations the course already uses
  • the trade between space and time and the idea of paying a cost once and spreading it over many later uses

Chapter 9 — covered

The chapter the material is taken from.

The named search and sort algorithms and their code — deferred

  • Linear and binary search
  • bubble
  • selection and merge sort
  • together with the complexity of each and the lambda expression that merge sort uses for its ordering

Deferred to the next section, which the syllabus gives its own line and its own textbook chapter.

Recursion and the complexity of a recursive call — deferred

Counting the calls a recursive function makes and reading a class off that count.

Deferred with the search and sort material, where the course introduces recursion. Every example on this page is written with loops, so you can read the whole section without it.

Recall first
range with one argument

range(n) hands out the whole numbers from 0 up to but not including n, so for i in range(n): runs its body exactly n times and range(0) runs it not at all.

Every count on this page starts by asking how many times a loop body runs. For a for over a range that number is read straight off the call.

whole number division

a // b divides and throws away the fraction, so 7 // 2 is 3 and 1 // 2 is 0. It is the operator that makes a halving loop stop.

The logarithmic examples on this page are while loops of the form n = n // 2. With plain / the value would become a float and the loop would run a different number of times.

what len costs and what in costs

len(L) hands back a number the list already keeps, so it is one step whatever the length. item in L has no such record: it compares item against the entries of L one after another, so on a list of n items it costs up to n comparisons.

Half the wrong answers on this material come from pricing every method call as one step. The two calls above sit right next to each other in code and cost completely different amounts.

a dictionary reaches a key directly

d[key] does not walk the dictionary. Python computes a position from the key itself, so the cost of the lookup does not grow with the number of keys stored.

This is the one tool on the page for turning a repeated scan into a repeated direct reach, which is the whole space against time trade.

a list is passed by reference

Handing a list to a function does not copy it. The parameter is a second name for the same list, so L.append(x) inside the function is visible outside it, while L = L + [x] builds a new list and leaves the caller's list alone.

L = L + [x] inside a loop copies the whole list on every pass, which quietly turns a linear loop into a quadratic one. That trap appears in this section's mistake list.

powers of two

2 10 is 1024 and 2 20 is 1048576, so a thousand is about two to the tenth and a million is about two to the twentieth.

These two facts are how you convert between a halving count and a list length in your head, with no calculator on the desk.

Try it yourself first (2 questions)
1§09.0 — the cost of a method call inside a loop

A function keeps the items of one list that also appear in a second list. Both lists hold n items. The body of the loop is a single line, and that line is a method call rather than an arithmetic operation.

def keep_shared(first, second):
    """Assumes first and second are lists.
    Returns the items of first that also appear in second.
    """
    both = []
    for item in first:
        if item in second:
            both.append(item)
    return both
Find(a) How many item comparisons can this function make in the worst case?
Given
  • first holds n items and second also holds n items.

  • There is exactly one loop written in the function.

Hint 1/4

Do not count lines of code. Count comparisons between items, which is the work the machine actually does here.

Hint 2/4

item in second is not given to Python for free: on a list it compares item against the entries of second one after another until it finds a match or runs out.

Hint 3/4

Here the loop makes n passes, and inside each pass the test can walk all n items of second. Both lists hold n items.

Hint 4/4

The worst case is about n times n comparisons, so the one visible loop is hiding a second one.

Show solution

Replace the method call with the loop it stands for, then count.

Write out what the test does

$$\texttt{item in second}$$

on a list this walks the entries in order, so it is a loop and not a single step

$$\text{up to } n \text{ comparisons per test}$$

the walk only stops early on a match, and the worst case is the one where there is none

Multiply the two counts

$$n \text{ passes} \times n \text{ comparisons}$$

the inner walk restarts on every outer pass, which is the definition of nesting

$$= n^{2}$$

so the class is quadratic even though only one loop is written

Answer $$\boxed{\text{about } n^{2}}$$
Check

Two disjoint lists of four items give 16 counted comparisons, and of eight items 64: twice the size, four times the count.

Before you price a loop body, ask of every call in it whether that call walks something.

2§09.0 — a counter that is divided rather than reduced

This while loop does not take one off its counter. It halves it, using whole number division, and stops when the counter reaches one.

n = 40
passes = 0
while n > 1:
    n = n // 2
    passes = passes + 1
print(n, passes)
Find(a) Write the line this prints.
Given
  • n starts at 40.

  • // divides and throws away the fraction.

IPython console
Hint 1/4

You are asked for two things: the value the counter finished at, and how many passes it took to get there.

Hint 2/4

Each pass replaces the counter by its half, rounded down. The loop stops as soon as the counter is no longer greater than one.

Hint 3/4

Starting from 40 the values are 20, then 10, then 5, then 2, then 1. Count how many arrows that is.

Hint 4/4

It finishes at 1 after 5 passes.

Show solution

Write the chain of values out rather than reasoning about the logarithm.

Follow the counter

$$40 \to 20 \to 10 \to 5 \to 2 \to 1$$

each arrow is one pass of the loop body

$$5 // 2 = 2$$

whole number division throws the fraction away, which is what keeps the chain landing on integers

Read off both answers

$$\texttt{n} = 1$$

the loop condition fails the moment the counter is no longer greater than one

$$\texttt{passes} = 5$$

one increment per arrow, and there are five arrows

Answer $$\boxed{1\quad 5}$$
Check

Independent check against the powers of two: 2 to the fifth is 32 and 2 to the sixth is 64, and 40 lies between them, so the count has to be 5.

A counter that is divided gives a pass count that grows like the logarithm, and the quickest way to check such a count is to bracket the starting value between two powers of two.

Notation
symbolreads asmeanswatch out
$n$

the size of the input

The one number everything is measured against.

It is not always a length.

$O(g(n))$

order g of n, or big O of g of n

The set of step counts that grow no faster than g(n) once the input is large enough, allowing any constant factor.

It is an upper bound, so a linear function is honestly O(n^2) as well.

$\log_2 n$

log base two of n

How many times you can halve n before you reach 1. For 1000 that is 9, because 2 to the ninth is 512 and 2 to the tenth is 1024.

Inside a big O the base does not matter, because changing base only multiplies by a constant.

$5n+2$

five n plus two

An exact step count: five steps on each of n passes plus two steps outside the loop.

Do not report this as the answer to a complexity question, and do not report O(5n+2) either. The class is O(n).

Conventions used here
Every output block on this page was produced by running the program.

No block of output here was predicted by eye. Each program was written to a file, run, and the characters it wrote were copied back in.

In a programming course the printed answer is the whole claim, so the page cannot itself be guessing.

What counts as one step.

One step is one of: binding a name to a value, making one comparison, doing one arithmetic operation, or reaching one item of a list by its index.

Two people who count answer = answer * n as one step and as two steps get 4n+2 and 5n+2, and they still report the same class.

Which case a bare complexity question is asking about.

Unless the question names a case, the answer is the worst case, and the answer says so in words.

The worst case is the only one of the three that is a promise. The other two describe luck.

What this page is allowed to use.

Everything here is built from what the course has covered by this week: numbers, text, True and False, input, print with end, format, if, while, for, range, len, def and return, the string operations, files, tuples, lists, dictionaries and their methods, and classes.

The lab sheet says that only functionality covered in the course may be used, and a solution written with a shortcut you cannot use in the exam teaches the wrong habit.

How a class is written.

A capital O, round brackets, and inside them the simplest function of n with no constant factor and no added constant: O(1), O(log n), O(n), O(n log n), O(n^2), O(2^n).

Marks are given for the class, and a class with a constant left in it is the visible sign that the last rule was not applied.

The lab this section is preparing you for.

The lab that follows this material asks you to write searching and sorting code inside a class, so this page prepares the half of it that is about cost: how many passes your loop makes, why a method call inside a loop can be dearer than it looks, and how to justify the class you claim.

The lab mark is part of the 20 per cent, and the reason given for choosing one algorithm over another is part of what the lab asks for.

9.1Counting the steps a program takes, instead of timing it

Prices a program in steps, not seconds, so the answer survives a change of machine.

We can read both functions from the hook. What we cannot yet say is which one survives a longer list.

Solvable with what we have
  • Write a loop and know it runs once per item.

  • Run both functions and see that both answers are right.

  • Read a function and see which lines are inside the loop.

Not solvable yet
  • Say which function is better without running it.

  • Compare timings taken on two different machines.

  • Predict the cost at four hundred items from a run at four.

The obvious move is to time them: start a clock, run the function, stop the clock, prefer the smaller number. On a faster machine both numbers shrink, and the same laptop gives different numbers on battery. Worse, the ordering can flip. At four items the nested version was the cheaper one, so a stopwatch fed a short test list recommends the wrong function.

Why it fails

A timing measures four things and reports one number: the algorithm, how it was written, the machine, and the input. Change any of the last three and the number moves while the algorithm stands still. We want the algorithm alone.

MethodMethod 9.1: counting basic steps
Conditions
  • One step is one of: binding a name to a value, one comparison, one arithmetic operation, or reaching one list item by index.

  • The count is written as a formula in the input size, and you have to say what the input size counts.

  • Two people who disagree about whether one line is one step or two get counts that differ by a constant factor, and they still report the same class.

$$\boxed{\ \text{steps}(n) = \underbrace{a}_{\text{outside the loop}} + \underbrace{b \cdot n}_{b \text{ steps on each of } n \text{ passes}}\ }$$

Count what the function does once whatever the input: that is the fixed part. Count the steps in one pass of the body and multiply by the number of passes. Add the two. The answer is a formula and not a number, because the number depends on the input and the formula does not.

Looks like this, but is not

A count of the lines of code looks like the same measure and is quicker to make. The factorial has five lines and the nested function has six, so the nested one looks a touch dearer.

Counting lines prices a program by how much of it you typed, and what we want is how much of it runs. The six line function in the hook made one hundred and sixty thousand passes on a list of four hundred. A line inside two loops is paid once per pair of passes, and a line count cannot know that.

ncounted steps5ngapgap as a share

1

7

5

2

29 per cent

3

17

15

2

12 per cent

10

52

50

2

4 per cent

1000

5002

5000

2

0.04 per cent

The gap is 2 in every row, because it is the two steps outside the loop and those do not grow.

The exact step count of the factorial loop

Here is the factorial function with a counter added to it. The counter lines are not part of the function being measured: they are the measuring instrument, and each one records the price of the line above or below it.

def fact_steps(n):
    """Assumes n is an int and n >= 0.
    Returns n factorial and the number of steps the plain version uses.
    """
    answer = 1
    steps = 1
    while n >= 1:
        steps = steps + 1
        answer = answer * n
        steps = steps + 2
        n = n - 1
        steps = steps + 2
    steps = steps + 1
    return answer, steps


for size in [1, 3, 10, 1000]:
    value, used = fact_steps(size)
    print('n =', size, 'steps =', used, 'and 5n+2 =', 5 * size + 2)

Sample Run:

n = 1 steps = 7 and 5n+2 = 7
n = 3 steps = 17 and 5n+2 = 17
n = 10 steps = 52 and 5n+2 = 52
n = 1000 steps = 5002 and 5n+2 = 5002

Find the step count as a formula in n, and check it against the run.

FindA formula for the number of steps in terms of n, and a check that the formula agrees with the counted run.
Given
  • answer = 1 is one step: one binding.

  • while n >= 1: is one comparison, so one step, on each pass that succeeds.

  • answer = answer * n is one multiplication and one binding, so two steps.

  • n = n - 1 is one subtraction and one binding, so two steps.

  • return answer is one step.

Solution

Separate what is paid once from what is paid per pass

$$\textcolor{#6e7781}{a} = 1 + 1 = 2$$

the first binding and the return happen whatever n is, so they belong to the fixed part

$$\textcolor{#d1690a}{b} = 1 + 2 + 2 = 5$$

the test, the multiply line and the subtract line are the three lines inside the loop, and this is the price of one pass

Count the passes

$$n \text{ passes}$$

the counter starts at n and the loop stops when it drops below one, so the body runs once for each of n, n-1, down to 1

$$\text{steps}(n) = \textcolor{#6e7781}{2} + \textcolor{#d1690a}{5}n$$

the fixed part plus the per pass price times the number of passes

Check the formula against the machine

$$n = 3:\ 5 \cdot 3 + 2 = 17$$

the run printed 17 for n equal to 3, so the formula and the counter agree

$$n = 1000:\ 5 \cdot 1000 + 2 = 5002$$

the run printed 5002, which also rules out an error that only shows up at large n

Answer $$\boxed{\text{steps}(n) = 5n + 2}$$
Check

From n equal to 3 to n equal to 10 is seven more passes, so the count should rise by 35, and the run gives 52 minus 17.

Five steps per pass, and the loop makes n passes.

The shape a + b n is what a single loop always gives you.

Why the same algorithm gives two different counts, and why that is fine

A classmate counts the same factorial loop and gets 4n + 2 rather than 5n + 2. Her reason: she treats answer = answer * n as one step, because on the page it is one operation being written into one name.

Decide who is right, and say what the disagreement costs.

FindWhether the disagreement changes the answer to a complexity question.
Given
  • Our count: test 1, multiply line 2, subtract line 2, so 5 per pass.

  • Her count: test 1, multiply line 1, subtract line 2, so 4 per pass.

  • Both of us count the same two steps outside the loop.

Solution

Compare the two formulas at a size that matters

$$5n + 2 \text{ against } 4n + 2$$

both are honest counts under their own list of what a step is, so neither is a mistake

$$n = 1000:\ 5002 \text{ against } 4002$$

the two numbers differ by a quarter, which is not nothing

Compare how the two behave when n grows

$$n \to 2n:\ 5n+2 \to 10n+2$$

doubling the input roughly doubles our count

$$n \to 2n:\ 4n+2 \to 8n+2$$

and it roughly doubles hers too, by the same factor, which is the thing a complexity question asks about

Answer $$\boxed{\text{both are linear: } 5n+2 \text{ and } 4n+2 \text{ are } O(n)}$$
Check

Both counts predict the same ratio from n equal to 1000 to n equal to 2000: 10002 over 5002 and 8002 over 4002 are each 2.0.

The disagreement is a constant factor of five quarters.

Say which list of steps you used, produce the exact count, then simplify.

Checkpoint
§09.1 — pricing a loop body that has two lines in it

A loop with two lines in its body, each priced by the same list of steps the section uses. The counter records the price of the two lines it sits between.

total = 0
steps = 1
for i in range(6):
    steps = steps + 1
    total = total + i
    steps = steps + 2
    total = total * 2
    steps = steps + 2
steps = steps + 1
print(total, steps)
Find(a) Write the line this prints.
Given
  • total = 0 and the final print are the two lines outside the loop, and the counter charges one step for each.

  • One pass is charged 1 for the loop control, 2 for the addition line and 2 for the multiplication line.

  • range(6) gives six passes.

IPython console
Hint 1/4

Two separate jobs here: follow total through the six passes, and let the counter do its own arithmetic.

Hint 2/4

For the counter, the shape is the fixed part plus the price of one pass times the number of passes.

Hint 3/4

Here the fixed part is 2, one pass costs 1 plus 2 plus 2, and there are six passes because range(6) was given.

Hint 4/4

It prints 57 and 32.

Show solution

Two columns, one for total and one for steps, filled in pass by pass.

Follow total through the six passes

$$i=0:\ (0+0)\cdot 2 = 0$$

add the current i, then double, in that order

$$i=1:\ (0+1)\cdot 2 = 2$$

same two operations on the value left by the previous pass

$$i=2:\ (2+2)\cdot 2 = 8$$

the doubling is what makes this grow quickly

$$i=3:\ (8+3)\cdot 2 = 22$$

still the same two operations

$$i=4:\ (22+4)\cdot 2 = 52$$

one pass left

$$i=5:\ (52+5)\cdot 2 = 114$$

so the printed total is 114, and the arithmetic above is the only place it could have gone wrong

Let the counter use the formula

$$1 + 2 + 2 = 5 \text{ per pass}$$

the three counter lines in the body add to five, which is the price the section assigned to one pass

$$5 \cdot 6 + 2 = 32$$

six passes at five, plus the two steps outside

Answer $$\boxed{114\quad 32}$$
Check

The last thing done to total is a doubling, so it has to be even, and 114 is even while the halved answer 57 is not.

The counter obeys a + b n no matter what the body computes.

⚠ Counting lines of code rather than steps executed

The lines are on the page and the passes are not.

wrong$$\text{steps} = \text{number of lines} = 5$$
right$$\text{steps}(n) = 5n + 2$$
⚠ Giving the count as a number instead of a formula

A run produces one number, and writing it down feels like an answer.

wrong$$\text{steps} = 5002$$
right$$\text{steps}(n) = 5n + 2,\quad \text{so } 5002 \text{ at } n = 1000$$
⚠ Forgetting to say what n counts

In most examples n is a list length, so it stops getting mentioned.

wrong$$\text{steps} = 5n + 2$$
right$$\text{steps} = 5n + 2,\ n = \text{the value of the argument}$$

9.2Why 5n + 2 and 5n give the same answer

Cuts a count down to the one term that decides how it grows.

We have 5n + 2 and a table showing the 2 stops mattering. Now we make that a rule, with an order.

Solvable with what we have
  • Count the steps of a single loop exactly.

  • See that the added 2 stops mattering.

  • See that a classmate's 4n + 2 behaves as ours does.

Not solvable yet
  • Compare 1000 + x against 2x squared by eye.

  • Say which term of a three term count to keep.

  • Give an answer two different counters agree on.

The tempting move is to keep the whole count and compare the whole thing. Try it on a function whose count is 1000 plus x plus 2 x squared. At x equal to 10 the fixed 1000 is more than four fifths of the work. At x equal to 1000 it is one part in two thousand.

Why it fails

Comparing whole counts gives an answer that depends on which x you picked, which is the stopwatch problem in a new form. A class answers what happens as the input grows without limit, and at that end one term is left standing.

RuleRule 9.2: simplifying a count to a class
Conditions
  • Apply the two rules in this order.

  • Fastest growing means the term that wins for all large enough n, not the term that is biggest at the n you have in mind.

  • The order of growth from slowest to fastest is: constant, then log n, then n, then n log n, then n squared, then any higher power, then any constant raised to n.

$$\boxed{\ 1000 + x + 2x^{2}\ \xrightarrow{\text{keep the fastest}}\ 2x^{2}\ \xrightarrow{\text{drop the factor}}\ O(x^{2})\ }$$

Keep only the term that grows fastest and throw the others away entirely. Then, if what is left is a number times a function of n, throw the number away too. What remains goes inside the O. The first rule discards the terms that stop mattering, the second discards the part that depends on how you counted.

Looks like this, but is not

The rule looks like it says small terms never matter, so somebody reads it as advice and picks a program with a fixed start up cost of a million over one with a cost of ten, since both costs are constant.

The rule says what to report as a class, not what to prefer in practice. Two programs that are both O(n) can differ by a factor of a thousand at every size, and on a real list you feel it. The class answers what happens when the input grows, not which of two programs in one class to run today.

nlog nnn log nn squared

10

3

10

30

100

100

6

100

600

10000

1000

9

1000

9000

1000000

100000

16

100000

1600000

10000000000

Multiplying n by ten thousand, from 10 to 100000, multiplies the log column by about five and the n squared column by a hundred million. That is the whole reason these classes are worth naming.

Simplifying 1000 + x + 2 x squared, and seeing the terms take over

This function does three separate pieces of work: a loop of fixed length, a loop of length x, and a pair of nested loops over x with two additions in the body. It prints the running count after each piece, so the three terms are visible separately.

def f(x):
    """Assumes x is an int > 0. Returns the number of additions done."""
    ans = 0
    for i in range(1000):
        ans = ans + 1
    print('after the first loop', ans)
    for i in range(x):
        ans = ans + 1
    print('after the second loop', ans)
    for i in range(x):
        for j in range(x):
            ans = ans + 1
            ans = ans + 1
    print('after the nested loops', ans)
    return ans


f(10)
print()
f(1000)

Sample Run:

after the first loop 1000
after the second loop 1010
after the nested loops 1210

after the first loop 1000
after the second loop 2000
after the nested loops 2002000

Find the count as a formula in x, then the class.

FindThe exact count of additions, and the complexity class.
Given
  • The first loop runs 1000 times whatever x is.

  • The second loop runs x times.

  • The nested loops make x times x passes with two additions each.

  • The run above is for x equal to 10 and then x equal to 1000.

Solution

Read one term off each piece of work

$$\textcolor{#6e7781}{1000}$$

the range is written as a number, so this loop does not know x exists

$$\textcolor{#1f6feb}{x}$$

one addition on each of x passes

$$\textcolor{#d1690a}{2x^{2}}$$

x times x passes because the loops are nested, and two additions in each pass

Confirm each term against the printed run

$$x = 10:\ 1000,\ 1010,\ 1210$$

the jumps are 1000, then 10, then 200, which are the three terms at x equal to 10

$$x = 1000:\ 1000,\ 2000,\ 2002000$$

now the jumps are 1000, then 1000, then 2000000, so the same three terms with the last one grown by ten thousand

Apply the two rules in order

$$1000 + x + 2x^{2} \to 2x^{2}$$

first rule: the square grows faster than the linear term and faster than the constant, so the other two go

$$2x^{2} \to O(x^{2})$$

second rule: what is left is a number times x squared, so the number goes as well

Answer $$\boxed{1000 + x + 2x^{2} = O(x^{2})}$$
Check

The nested term alone goes from 200 to 2000000 when x goes from 10 to 1000, which is the ten thousandfold a quadratic predicts.

Three terms read off three pieces of code, then two rules applied once each.

A count with several terms is read off the code one piece at a time, and pieces that follow one another add.

Checkpoint
§09.2 — applying the two rules to a three term count

A function has been counted honestly and its step count comes to 4n^2 + 1000n + 50000. The question asks for its complexity class, and the student has thirty seconds.

Find(a) Which class should be reported?
Given
  • The exact count is 4n^2 + 1000n + 50000.

  • n is the length of a list.

  • The two rules are: keep the fastest growing term, then drop its constant factor.

Hint 1/4

You are not being asked which term is biggest right now. You are being asked which one is biggest once n is large.

Hint 2/4

Keep the fastest growing term and drop the others, then drop the constant factor in front of what is left.

Hint 3/4

Here the three terms are 4n^2, 1000n and 50000. At n equal to 1000 they are 4000000, 1000000 and 50000.

Hint 4/4

The class is O(n^2).

Show solution

Test the terms at a large n rather than arguing about the coefficients.

Rank the terms at a large n

$$n = 1000:\ 4n^{2} = 4\,000\,000$$

a size large enough that the ranking has settled

$$1000n = 1\,000\,000,\quad 50000$$

so the square is already four times the linear term and eighty times the constant

Apply the rules in order

$$4n^{2} + 1000n + 50000 \to 4n^{2}$$

first rule keeps one term and discards the rest

$$4n^{2} \to O(n^{2})$$

second rule drops the constant factor, which is the step most often left undone

Answer $$\boxed{O(n^{2})}$$
Check

At n equal to 1000 the count is 5050000 and at 2000 it is 18050000, a ratio of 3.57 heading for the 4 a quadratic gives.

Substituting one large n settles which term dominates faster than staring at coefficients, and it is a habit worth keeping for the questions where the coefficients are chosen to mislead.

⚠ Leaving the constant factor inside the O

The first rule feels like the hard one, so once the term is chosen the job feels done.

wrong$$4n^{2} + 1000n \Rightarrow O(4n^{2})$$
right$$4n^{2} + 1000n \Rightarrow O(n^{2})$$
⚠ Choosing the term that is biggest at the n in front of you

With 50000 in the count and n equal to 10 on the page, the constant really is the biggest term, and picking it feels like reading the situation.

wrong$$n = 10 \Rightarrow 50000 \text{ dominates} \Rightarrow O(1)$$
right$$n \to \infty \Rightarrow 4n^{2} \text{ dominates} \Rightarrow O(n^{2})$$
⚠ Keeping a sum of terms inside the O

Dropping a whole term feels like throwing away work you did, so it gets kept as a tidy sum instead.

wrong$$O(n^{2} + n + 1)$$
right$$O(n^{2})$$

9.3Big O: naming the upper bound on how a count grows

Says what the O promises, so dropping a term is justified rather than obeyed.

We have been dropping terms because a table said to. Now we write down what the notation actually claims.

Solvable with what we have
  • Apply the two rules and get from an exact count to a class.

  • See from a table that the added constant stops mattering.

  • Find the size at which two counts are level.

Not solvable yet
  • Say what the notation actually claims when a question asks.

  • Defend dropping a term to somebody who objects at small n.

  • Decide whether calling a linear function quadratic is wrong.

Reading the O as an equals sign is the natural first guess. It breaks at once. At x equal to 10 the count is 1210 and x squared is 100, so the two are neither equal nor close, and yet the class is right. Reading the O as equality forces you to call a correct claim false.

Why it fails

The O is a ceiling with two pieces of slack in it: you may multiply the ceiling by any constant, and you may ignore every input below a size of your choosing. With both in place, 1210 sitting above 100 is no objection.

DefinitionDefinition 9.3: f is O of g
Conditions
  • The constants c and the threshold size may be chosen freely, and they are chosen once and for all rather than per input.

  • Nothing has to hold below the threshold, which is why a large constant start up cost never changes the class.

  • Because it is an upper bound, a function that is O of n is also honestly O of n squared.

$$\boxed{\ f(n) = O(g(n))\ \text{means: there are } c > 0 \text{ and } n_0 \text{ with } f(n) \le c\,g(n)\ \text{for every } n \ge n_0\ }$$

Read it as a promise about the far end. There is a multiple of g, and a size beyond which the count never exceeds it again. Below that size the promise says nothing. So to show a claim, produce two numbers; to break one, show that no pair works.

Proof

Take the count from the previous block, 1000 plus x plus 2 x squared, and the claim that it is O of x squared. We have to produce a multiplier and a size.

Try the multiplier 3. The claim to check is that 1000 plus x plus 2 x squared is at most 3 x squared past some size.

Subtract 2 x squared from both sides: the claim becomes 1000 plus x at most x squared.

At x equal to 32 the left side is 1032 and the right side is 1024, so the claim is still false there. At x equal to 33 the left is 1033 and the right is 1089, so it holds.

Once x squared has overtaken 1000 plus x it stays ahead, because raising x by one adds 2x plus 1 to the right side and only 1 to the left, and 2x plus 1 is larger than 1 for every positive x.

So the multiplier 3 and the size 33 are a witnessing pair, and the claim is proved. Any larger multiplier would have given a smaller size, which is the trade the two constants let you make.

Looks like this, but is not

Since an upper bound can be generous, any large enough class looks like a correct answer. A linear function is O of n squared and also O of two to the n, and both are true.

True, and marked wrong, because the complexity of a function means the tightest bound you can justify. Calling a linear scan O of two to the n is like answering how tall somebody is with under four metres: it holds, and it answers nothing.

xcounted steps3 x squareddoes the bound hold

10

1210

300

no

20

1820

1200

no

32

3080

3072

no, by 8

33

3211

3267

yes

100

21100

30000

yes

The bound fails in the first three rows and holds in the last two, and it never fails again after x equal to 33.

Showing that 8n + 400 is O of n, with the two constants written down

A function has been counted and its step count is 8n plus 400. Show that this is O of n by naming a multiplier and a size, and then show that the same count is not O of the constant function 1.

FindA witnessing pair for the claim O of n, and an argument that no pair witnesses the claim O of 1.
Given
  • The exact count is 8n plus 400.

  • n is the length of a list, so n is a positive whole number.

  • The definition asks for a multiplier c and a size beyond which the bound holds.

Solution

Find a multiplier that works for O of n

$$8n + 400 \le c\,n$$

this is the claim with the definition written out, and c is ours to choose

$$c = 9:\ 8n + 400 \le 9n \iff 400 \le n$$

subtract 8n from both sides; choosing c one larger than the coefficient leaves exactly one n to absorb the 400

$$c = 9,\ n_0 = 400$$

so the pair is found, and at n equal to 400 both sides are 3600

Show that a smaller multiplier only moves the size

$$c = 12:\ 8n + 400 \le 12n \iff 100 \le n$$

a more generous multiplier buys a smaller threshold, which is the trade the definition allows

$$c = 8:\ 400 \le 0$$

and a multiplier equal to the coefficient fails for every n, so c has to be strictly larger than 8

Rule out the claim O of 1

$$8n + 400 \le c \cdot 1 \text{ for all } n \ge n_0$$

this is what O of 1 would claim, with c a fixed number chosen in advance

$$n = c:\ 8c + 400 > c$$

whatever c somebody picks, feed the count an n equal to that c and the claim fails, so no pair can work

Answer $$\boxed{8n + 400 = O(n)\ \text{with } c = 9, \ n_0 = 400;\ \text{and it is not } O(1)}$$
Check

At n equal to 500 the count is 4400 and nine times n is 4500, so the bound holds.

One inequality rearranged for the positive claim, one substitution for the negative one.

To prove a class, produce the pair.

Checkpoint
§09.3 — what the notation actually claims

A function has a counted step count of 3n + 5000. A student objects to calling it O(n), and gives a reason: at n equal to 10 the count is 5030, while n is only 10, so the count is five hundred times the bound. The objection is about a specific number, so it deserves a specific answer.

Find(a) Which answer settles the objection?
Given
  • The exact count is 3n + 5000.

  • The claim under dispute is that the count is O(n).

  • The definition allows a multiplier and a threshold size to be chosen.

Hint 1/4

The objection is about one particular n. Ask yourself whether the definition makes any claim at that n.

Hint 2/4

The definition asks for a multiplier and a threshold: the count has to stay at or below the multiplier times the bound, for every size at or above the threshold, and for nothing below it.

Hint 3/4

Here the count is 3n + 5000, and you are free to pick the multiplier. Picking 4 leaves one n to swallow the 5000, so the condition becomes 5000 at most n.

Hint 4/4

The answer is that the pair 4 and 5000 witnesses the bound.

Show solution

Answer by producing the pair rather than by arguing about the notation.

Write the claim as an inequality

$$3n + 5000 \le c\,n \text{ for } n \ge n_0$$

this is the definition with the given count substituted in

$$c = 4:\ 5000 \le n$$

subtracting 3n from both sides leaves one n to absorb the additive constant, which is why c is chosen one above the coefficient

Answer the student's specific number

$$n = 10 < 5000$$

the objection sits below the threshold, where the definition promises nothing

$$c = 503:\ 5000 \le 500n \iff n \ge 10$$

and if a threshold of 10 is wanted, a larger multiplier buys it, which shows the two constants trade against each other

Answer $$\boxed{c = 4,\ n_0 = 5000}$$
Check

At n equal to 5000 the count is 20000 and four times n is 20000, so the two are exactly level.

Every dispute about a class is settled by naming c and the threshold.

⚠ Reading the O as equality or as approximation

It is written with an equals sign, and in every other use of that sign the two sides are the same size.

wrong$$1000 + x + 2x^{2} \approx x^{2}$$
right$$1000 + x + 2x^{2} \le 3x^{2} \ \text{for } x \ge 33$$
⚠ Reporting a loose bound because it is technically true

The definition really is one sided, so a bigger class is never false, and false is the thing a student is watching out for.

wrong$$\text{linear scan} = O(2^{n})$$
right$$\text{linear scan} = O(n)$$
⚠ Letting the constants depend on n

The definition hands you two free numbers, so it feels as though you may keep choosing them as n changes.

wrong$$f(n) \le n \cdot g(n) \Rightarrow f = O(g)$$
right$$f(n) \le c\,g(n),\ c \text{ fixed before } n \text{ varies}$$

9.4Best, worst and average case, and which one to report

Separates the luck of the input from the promise about the algorithm.

Every count so far was the same for every input of a given size. A loop that stops early breaks that.

Solvable with what we have
  • Count a loop that runs to the end of the list.

  • Simplify that count to a class.

  • Name a multiplier and a threshold for it.

Not solvable yet
  • Count a loop that returns as soon as it finds something.

  • Answer when the same function costs 1 step and 5.

  • Say what a question with no named case means.

The first instinct is to count the run in front of you. Feed the function a list whose first item is negative, count one read, report a constant cost. The next list makes the same function read every item. Both counts are correct and they are not in the same class.

Why it fails

Once the code can stop early there is no single count, so there is nothing for one number to be. The fix is a better question: not what this function costs, but what it costs at most, at least, and on average over inputs of one size.

DefinitionDefinition 9.4: the three cases
Conditions
  • All three are taken over inputs of the same size, so the size is fixed first and the input is varied inside it.

  • The average case needs an assumption about how likely each input is, and on this material the assumption is that the wanted item is equally likely to be anywhere.

  • An input that is not in the list at all belongs to the worst case, because the loop cannot stop early on it.

$$\boxed{\ \text{best}(n) = \min_{|I| = n} \text{steps}(I),\quad \text{worst}(n) = \max_{|I| = n} \text{steps}(I)\ }$$

Fix the size, then look at what the function costs on every input of that size. The smallest of those costs is the best case and the largest is the worst case. The average is what you would pay per run if inputs turned up as often as you assumed. All three are functions of the size, not properties of one run.

Looks like this, but is not

The average case looks like the honest one to report, since it is neither the lucky answer nor the gloomy one, and real inputs are presumably somewhere in the middle.

The average is an answer about a population of inputs, and it is only as good as the assumption behind it. The average of about n over 2 assumes the wanted item is present and equally likely anywhere. Feed the function items that are mostly absent and every run costs n, so that average describes nothing. The worst case needs no assumption.

listposition of the negativereadscase

[-4, 7, 7, 7, 7]

0

1

best

[7, 7, -4, 7, 7]

2

3

the average, as it happens

[7, 7, 7, 7, -4]

4

5

worst

[7, 7, 7, 7, 7]

none

5

worst

One function, one size, four costs between 1 and 5. The last two rows tie, which is the observation worth keeping: the absent input is not a special case, it is one of the worst cases.

The three cases of a scan that stops at the first negative number

This function walks a list and returns as soon as it meets a negative number. The counter records how many positions it read, so the four runs below show four different costs for the same function on lists of the same length.

def first_negative(L):
    """Assumes L is a list of numbers.
    Returns the position of the first negative number, or -1 if there is
    none, together with the number of positions looked at.
    """
    looked = 0
    for i in range(len(L)):
        looked = looked + 1
        if L[i] < 0:
            return i, looked
    return -1, looked


print(first_negative([-4, 7, 7, 7, 7]))
print(first_negative([7, 7, -4, 7, 7]))
print(first_negative([7, 7, 7, 7, -4]))
print(first_negative([7, 7, 7, 7, 7]))

Sample Run:

(0, 1)
(2, 3)
(4, 5)
(-1, 5)

Give the best, worst and average case as formulas in n, and the class of each.

FindThe three cases as formulas in n, and the class of each, with the run used as the check.
Given
  • Every list in the run has five items, so n equals 5.

  • The counter is increased once per position read.

  • The fourth list has no negative number in it at all.

Solution

Read the best case off the first run

$$\textcolor{#1f6feb}{\text{best}} = 1$$

the loop returns inside its first pass when the first item is already negative, and the run printed a count of 1

$$O(1)$$

the count does not depend on n at all, since a longer list with a negative first item still costs one read

Read the worst case off the last two runs

$$\textcolor{#d1690a}{\text{worst}} = n$$

both the last item case and the absent case make the loop reach the end, and the run printed 5 for each

$$O(n)$$

the count is the length itself, so doubling the list doubles the reads

Build the average case from an assumption

$$\frac{1 + 2 + \cdots + n}{n} = \frac{n+1}{2}$$

assume the negative number is present and equally likely at each of the n positions, then average the n possible costs

$$n = 5:\ \frac{6}{2} = 3$$

which matches the middle run, where the negative number sat at position 2 and the count came to 3

$$O(n)$$

the one half is a constant factor, so the average case and the worst case are in the same class

Answer $$\boxed{\text{best } 1 = O(1),\ \text{average } \tfrac{n+1}{2} = O(n),\ \text{worst } n = O(n)}$$
Check

The three lists that do hold a negative number cost 1, 3 and 5 reads, and their mean is 3, which is what the formula gives at n equal to 5.

Three counts from one function.

The best case fell out of one run, the worst case out of two, and the average needed a sentence about the inputs.

A loop whose best case is not its first pass

This function is given a list that is already in increasing order, and it uses that to stop early when the wanted value cannot be ahead. The counter shows what it cost on three lists of the same length.

def in_sorted(L, e):
    """Assumes L is a list of ints in increasing order.
    Returns whether e is in L, and how many positions were
    read.
    """
    read = 0
    for i in range(len(L)):
        read = read + 1
        if L[i] == e:
            return True, read
        if L[i] > e:
            return False, read
    return False, read


L = [10, 20, 30, 40, 50]
print(in_sorted(L, 10))
print(in_sorted(L, 5))
print(in_sorted(L, 25))
print(in_sorted(L, 99))

Sample Run:

(True, 1)
(False, 1)
(False, 3)
(False, 5)

Say what the best case is, what the worst case is, and whether knowing the list is sorted changed the class.

FindThe best and worst case counts, and whether the class improved.
Given
  • L holds five items in increasing order.

  • The function stops when it finds the value and also when it passes the value.

  • The four runs ask for 10, 5, 25 and 99.

Solution

Notice that two different inputs give the best case

$$e = 10:\ \text{read} = 1$$

the value is the first item, so the first test succeeds

$$e = 5:\ \text{read} = 1$$

the value is below everything, so the second test fires on the first pass, and this is the case the extra line was written for

Find the worst case

$$e = 99:\ \text{read} = 5 = n$$

a value above everything is never passed, so the loop reaches the end and the sorted order bought nothing

$$\text{worst}(n) = n$$

so the worst case is unchanged from the unsorted scan

Say what actually improved

$$\text{absent value, average} \approx \tfrac n2$$

for a value that is absent but inside the range, the loop stops where the list passes it, which the run shows at 3 reads out of 5 for the value 25

$$O(n) \text{ either way}$$

the improvement is a constant factor on one family of inputs, and constant factors do not change the class

Answer $$\boxed{\text{best } 1,\ \text{worst } n,\ \text{still } O(n)}$$
Check

On a list of 50 items running 10, 20, 30 up to 500, asking for 25 still stops after 3 reads rather than 30.

One extra comparison per pass bought an earlier exit on absent values.

A real improvement that does not change the class is still a real improvement, and this section is not the place to pretend otherwise.

Checkpoint
§09.4 — which input makes this loop work hardest

A function reports how many positions it read before it stopped. It is run on three lists, all of length four, and one of the three asks for a value that is not there.

def looked_at(L, target):
    """Assumes L is a list. Returns how many positions were read
    before the loop stopped.
    """
    looked = 0
    for i in range(len(L)):
        looked = looked + 1
        if L[i] == target:
            break
    return looked


L = [4, 9, 16, 25]
print(looked_at(L, 4), looked_at(L, 25), looked_at(L, 99))
Find(a) Write the line this prints.
Given
  • L is [4, 9, 16, 25], so n equals 4.

  • break leaves the loop at once.

  • The three targets are 4, 25 and 99.

IPython console
Hint 1/4

Three separate runs of the same function. Ask for each one where the loop stopped and how many positions it had read by then.

Hint 2/4

The counter goes up before the test, so a position that is read and rejected still counts.

Hint 3/4

Here the list is [4, 9, 16, 25]. The first target sits at position 0, the second at position 3, and the third is not in the list at all.

Hint 4/4

It prints 1, 4 and 4.

Show solution

Take the three runs one at a time and say where each one stopped.

Trace the first two runs

$$\texttt{target} = 4$$

position 0 matches, so the counter has been raised once and the break fires: 1

$$\texttt{target} = 25$$

positions 0, 1 and 2 are read and rejected, position 3 matches: 4

Trace the run that finds nothing

$$\texttt{target} = 99$$

no position matches, so the break never fires and the range runs out

$$\text{reads} = \texttt{len(L)} = 4$$

which is the same count as the worst case that does find the value

Answer $$\boxed{1\quad 4\quad 4}$$
Check

No run can print more than 4, because the counter is raised once per pass and range(len(L)) offers four passes.

The worst case of an early exit loop is set by the inputs that never trigger the exit, and an absent value is the commonest of those.

⚠ Forgetting the input that is not in the list at all

Every example is written with the wanted item somewhere in the list, so the worst position gets found and the absent case never gets considered.

wrong$$\text{worst} = n - 1 \ \text{(the last position)}$$
right$$\text{worst} = n \ \text{(absent, or last)}$$
⚠ Reporting the best case as the complexity

The best case is the one the first test run happened to hit, and it is a real count of a real run.

wrong$$\text{scan} = O(1) \ \text{because it can stop at the first item}$$
right$$\text{scan} = O(n) \ \text{worst case, } O(1) \text{ best case}$$
⚠ Averaging without saying over what

The formula n plus one over two is memorable and gets written down without the sentence that makes it true.

wrong$$\text{average} = \tfrac{n+1}{2} \ \text{always}$$
right$$\text{average} = \tfrac{n+1}{2} \ \text{if present and uniformly placed}$$

9.5The six growth rates the course names, and the loop that makes each one

Gives each class a loop shape, so naming one becomes recognition rather than recall.

We can turn any count into a class. Which classes actually turn up, and what code lands in each?

Solvable with what we have
  • Simplify a count you are handed into a class.

  • Show a claimed class holds by naming two constants.

  • Separate the three cases of one function.

Not solvable yet
  • Say which classes are worth memorising.

  • Recognise a logarithmic loop in code.

  • Order two classes without substituting numbers.

The first guess is that one loop is linear, two nested loops are quadratic, and that is the whole list. Then a while loop turns up whose counter is divided, and the guess has nowhere to put it: 1000 takes 9 passes and a million takes 19, which is neither linear nor constant.

Why it fails

The guess counts how many loops are written, and that is not what decides the class. What decides it is how the pass count depends on n, and a divided counter grows far more slowly than the counter does. Six shapes cover everything the course asks.

RuleRule 9.5: the six classes and their loop shapes
Conditions
  • Constant does not mean loop free.

  • The classes are listed slowest growing first, and that order is the one to memorise.

  • Exponential means a constant raised to n, not n raised to a constant.

$$\boxed{\ O(1) \subset O(\log n) \subset O(n) \subset O(n \log n) \subset O(n^{k}) \subset O(c^{n})\ }$$

Constant, logarithmic, linear, log linear, polynomial, exponential. Each class is contained in the next, so a linear function is honestly quadratic too, and that is not what you report. The shapes, in order: no dependence on n; a counter divided each pass; one pass per item; a halving loop inside a pass over the items; nested passes; a pass count that is a power of two.

Looks like this, but is not

A while loop that changes its counter looks logarithmic, since the halving loop was a while loop that changed its counter. So while n > 1: n = n - 2 gets called logarithmic too.

Subtracting is not dividing. Taking two off a counter of a million needs five hundred thousand passes; halving it needs 19. Read the assignment: subtracting a fixed amount gives n over that amount, which is linear, and dividing by a fixed amount gives a logarithm.

nO(1)O(log n)O(n)O(n log n)O(n squared)O(2 to the n)

8

1

3

8

24

64

256

16

1

4

16

64

256

65536

100

1

6

100

600

10000

about 10 to the 30

1000

1

9

1000

9000

1000000

more than atoms in the earth

Read across the row for n equal to 100. The first four numbers are the sort of thing a computer does without noticing. The fifth is fine too.

The halving loop, counted, and why 1000 takes only 9 passes

This is the loop shape behind every logarithmic program in the course. It divides its counter instead of decreasing it, and the run shows how few passes that takes even on large values.

def halvings(n):
    """Assumes n is an int >= 1.
    Returns how many times n can be halved with // before it reaches 1.
    """
    count = 0
    while n > 1:
        n = n // 2
        count = count + 1
    return count


for size in [1, 8, 1000, 1000000]:
    print(size, 'needs', halvings(size), 'halvings')

Sample Run:

1 needs 0 halvings
8 needs 3 halvings
1000 needs 9 halvings
1000000 needs 19 halvings

Explain the four counts, and give the class of the loop in terms of the value of n.

FindWhy each count is what it is, and the class.
Given
  • n // 2 divides and throws away the fraction.

  • The loop stops as soon as n is no longer greater than 1.

  • The four values tried are 1, 8, 1000 and 1000000.

Solution

Check the two counts you can do in your head

$$1:\ \text{the test fails at once, } 0 \text{ passes}$$

the counter is not greater than 1, so the body never runs, which is the edge case worth checking first

$$8 \to 4 \to 2 \to 1:\ 3 \text{ passes}$$

and 8 is 2 to the third, so the count is the exponent

Turn that observation into the formula

$$n = 2^{k} \Rightarrow k \text{ passes}$$

each pass removes exactly one factor of two, and the loop stops when none is left

$$\textcolor{#d1690a}{k = \log_2 n}$$

which is what the logarithm means: the number of twos multiplied together to reach n

Check the two counts you cannot do in your head

$$2^{9} = 512 \le 1000 < 1024 = 2^{10}$$

1000 is not a power of two, so the count is the exponent of the power of two at or below it, which is 9

$$2^{19} = 524288 \le 10^{6} < 2^{20}$$

the same bracketing at a million gives 19, and the run printed 19

Answer $$\boxed{\text{passes} = \lfloor \log_2 n \rfloor = O(\log n)}$$
Check

A million is a thousand times a thousand, and a thousand is about 2 to the tenth, so going from 1000 to 1000000 should add about 10 passes.

One pass removes one factor of two.

When you meet a loop whose counter is divided, count in powers of two and bracket the value.

Building an n log n loop and an exponential loop from bare parts

Two of the six classes have no single loop that produces them, so here they are built. The first nests a halving loop inside a pass over the list. The second has one loop whose number of passes is itself a power of two.

def log_linear(L):
    """Assumes L is a list. Returns the number of inner passes."""
    passes = 0
    for i in range(len(L)):
        m = len(L)
        while m > 1:
            m = m // 2
            passes = passes + 1
    return passes


def exponential(n):
    """Assumes n is an int >= 0.
    Returns how many on and off patterns n switches have.
    """
    rows = 0
    for code in range(2 ** n):
        rows = rows + 1
    return rows


for size in [8, 16, 1024]:
    print(size, log_linear(list(range(size))))
for n in [3, 10, 20]:
    print('n =', n, 'rows =', exponential(n))

Sample Run:

8 24
16 64
1024 10240
n = 3 rows = 8
n = 10 rows = 1024
n = 20 rows = 1048576

Give the class of each function and check it against the run.

FindThe class of each, justified by the printed counts.
Given
  • log_linear makes one full halving loop per item of the list.

  • exponential has a single loop, but its range is 2 raised to n.

  • The list lengths tried are 8, 16 and 1024, and the values of n are 3, 10 and 20.

Solution

Price the log linear function

$$n \text{ outer passes}$$

one per item, read straight off the range

$$\log_2 n \text{ inner passes each}$$

the inner loop is the halving loop from the previous example, started afresh on every outer pass

$$n \log_2 n$$

nested, so the two counts multiply, and at n equal to 8 that is 8 times 3, which is 24

Check it at the two larger sizes

$$16 \cdot 4 = 64$$

16 is 2 to the fourth, so four inner passes each, and the run printed 64

$$1024 \cdot 10 = 10240$$

1024 is 2 to the tenth, and the run printed 10240, so doubling the list from 8 to 16 did not double the count but multiplied it by 2.67

Price the exponential function

$$2^{n} \text{ passes}$$

the loop body is one step and the range says how many times it runs

$$n = 20:\ 2^{20} = 1048576$$

so twenty switches already cost a million passes, and each extra switch doubles that

$$O(2^{n})$$

the input size sits in the exponent, which is what separates this class from any polynomial

Answer $$\boxed{\texttt{log\_linear} = O(n \log n),\quad \texttt{exponential} = O(2^{n})}$$
Check

From 8 to 16 the log linear count rose by 2.67, between the 2 a linear count gives and the 4 a quadratic one gives.

The log linear function does one halving loop per item, so on a list of 1024 it makes 10240 inner passes rather than the 1048576 a nested pass over the list would cost.

Log linear is what you get when a cheap logarithmic job is done once per item.

Checkpoint
§09.5 — telling a halving loop from a subtracting one

Two while loops, side by side, both counting their passes. One takes a fixed amount off its counter each pass and the other divides it. Both start from the same value, and the question is how their pass counts compare at a large starting value.

def by_subtraction(n):
    """Assumes n is an int >= 1. Returns the pass count."""
    passes = 0
    while n > 1:
        n = n - 2
        passes = passes + 1
    return passes


def by_division(n):
    """Assumes n is an int >= 1. Returns the pass count."""
    passes = 0
    while n > 1:
        n = n // 2
        passes = passes + 1
    return passes
Find(a) Which pair of classes is right?
Given
  • Both loops start from the same n and stop when the counter is no longer greater than 1.

  • The first takes 2 off the counter, the second halves it.

  • The question is about the classes, not about a single value of n.

Hint 1/4

Do not compare the two loops by looking at them.

Hint 2/4

A counter reduced by a fixed amount each pass gives a pass count proportional to n.

Hint 3/4

Here the first takes 2 off each pass, so from 1000 it needs about 500 passes. The second halves, so from 1000 it needs 9.

Hint 4/4

The classes are O(n) for the subtraction and O(log n) for the division.

Show solution

Pick one large starting value, count the passes of each loop, and read the classes off those two counts.

Count the subtracting loop

$$1000 \to 998 \to \cdots \to 2 \to 0$$

the counter drops by 2 each pass and stops once it is no longer above 1

$$\text{about } \tfrac{1000}{2} = 500 \text{ passes}$$

so the pass count is proportional to the starting value, which is the definition of linear here

Count the halving loop

$$1000 \to 500 \to \cdots \to 1$$

the counter is divided by 2 each pass, so each pass removes one factor of two

$$9 \text{ passes}$$

because 2 to the ninth is 512 and 2 to the tenth is 1024, so 1000 sits between them

Apply the second rule to both answers

$$\tfrac n2 \to O(n)$$

the divisor is a constant factor and constant factors are dropped

$$\log_2 n \to O(\log n)$$

changing the base multiplies by a constant, so the base is dropped for the same reason

Answer $$\boxed{O(n)\ \text{and}\ O(\log n)}$$
Check

From 1000 to 2000, the subtracting loop needs 500 more passes, twice as many as before. The halving loop needs exactly one more.

Read the assignment, not the loop. Whether the counter is reduced or divided is the whole difference between 500 passes and 9.

⚠ Calling any while loop with a changing counter logarithmic

The halving example is the first while loop the section counts, so the shape and the class get learned together.

wrong$$\texttt{n = n - 2} \Rightarrow O(\log n)$$
right$$\texttt{n = n - 2} \Rightarrow O(n),\quad \texttt{n = n // 2} \Rightarrow O(\log n)$$
⚠ Mixing up n to a power with a power raised to n

Both are written with a superscript, and the phrase used in speech, n squared and two to the n, sounds similarly shaped.

wrong$$O(n^{2}) \text{ and } O(2^{n}) \text{ are similar}$$
right$$n = 20:\ n^{2} = 400,\quad 2^{n} = 1\,048\,576$$
⚠ Assuming constant means loop free

sounds like no work, and a loop looks like work, so a loop in the body rules the class out.

wrong$$\texttt{for i in range(1000)} \Rightarrow O(n)$$
right$$\texttt{for i in range(1000)} \Rightarrow O(1)$$

9.6Reading the class straight off the loops

Turns the section into steps you can run on any function in a minute.

We have the classes and the shapes that make them. Now we put them together into the procedure an exam question actually asks you to carry out.

Solvable with what we have
  • Name the class of a single loop and of a halving loop.

  • Say what the six classes are and order them.

  • Count a nested loop whose inner range is the whole list.

Not solvable yet
  • Handle a function with three loops, one of them nested.

  • Price a loop whose inner range depends on the outer counter.

  • Spot a loop hidden inside a method call.

The rule of thumb most students arrive with is to count the loops: one linear, two quadratic, three cubic. It survives for a while. Then it meets two loops in a row, calls them quadratic, and is wrong; then one loop containing item in L, calls it linear, and is wrong the other way.

Why it fails

Counting loops ignores how they are arranged and what their bodies cost. Loops in a row add their passes; nested loops multiply them; and a method call in a body can carry a loop of its own that the page does not show.

MethodMethod 9.6: reading a class off a function
Conditions
  • Do the steps in order. Simplifying before you have all the terms is how a term goes missing.

  • Every call in a loop body has to be priced, not assumed. The list methods the course uses are priced in the table below.

  • The check at the end is not optional on this material: doubling n and predicting the count catches most errors in one line.

$$\boxed{\ \text{total} = \sum_{\text{pieces in a row}} \big(\text{passes} \times \text{cost of one pass}\big)\ }$$

Say what n counts. Then, piece by piece, work out how many times the body runs and what one run costs, including any call that hides a loop. Multiply those two per piece, add the pieces, and apply the two rules to the sum. Nesting multiplies and sequence adds.

Looks like this, but is not

A nested loop whose inner range is a fixed number looks like the quadratic shape, because it is two for lines with one indented under the other. The hook did exactly this: for j in range(3) under for i in range(len(L)).

The inner loop has to depend on n for the product to contain an n. Here the inner count is 3 whatever the length, so the product is 3 times n and the class is linear. The test is not whether one loop is inside another but whether the inner pass count mentions n.

operationcostwhy

len(L)

O(1)

the length is a number the list already keeps

L[i] and L[i] = v

O(1)

the position is computed, not searched for

L.append(x)

O(1)

the new item goes on the end, nothing moves

L.pop() with no argument

O(1)

removes from the end, nothing moves

d[key] and key in d

O(1)

a dictionary computes a position from the key

x in L and L.index(x)

O(n)

compares against the entries one after another

L.count(x)

O(n)

has to look at every entry, with no early exit

L.remove(x)

O(n)

finds the entry, then shifts everything after it down

L.insert(0, x)

O(n)

shifts every existing item up one place

L = L + [x]

O(n)

builds a new list and copies the old one into it

The two lines to be most careful about are the last two. L.append(x) inside a loop over n items is linear; L = L + [x] in the same place is quadratic, and the two lines do almost the same thing.

A function with a loop, then a nested loop, then a halving loop

Three pieces of work in one function, one after another. The middle piece is nested and the last piece divides its counter. Find the class.

def report(L):
    """Assumes L is a list of ints with at least one item.
    Returns the largest item, the number of equal pairs, and the
    number of halvings the length survives.
    """
    biggest = L[0]
    for x in L:
        if x > biggest:
            biggest = x
    pairs = 0
    for i in range(len(L)):
        for j in range(i + 1, len(L)):
            if L[i] == L[j]:
                pairs = pairs + 1
    steps = 0
    m = len(L)
    while m > 1:
        m = m // 2
        steps = steps + 1
    return biggest, pairs, steps


print(report([4, 9, 4, 2, 9, 9]))

Sample Run:

(9, 4, 2)

Give the class of the function in terms of the length of L.

FindThe complexity class of the whole function.
Given
  • n is the number of items in L.

  • The three pieces of work follow one another, so none is inside another.

  • The middle piece starts its inner range at i + 1.

Solution

Say what n counts, then price each piece

$$n = \texttt{len(L)}$$

the loops are all driven by the length, so the length is the size

$$\text{piece 1} = \textcolor{#1f6feb}{n}$$

one pass per item, with a constant body

$$\text{piece 2} = \textcolor{#d1690a}{\tfrac{n(n-1)}{2}}$$

nested, so multiply, and the inner range starting at i+1 keeps the triangle rather than the whole grid

$$\text{piece 3} = \textcolor{#6e7781}{\log_2 n}$$

the counter is divided, so this is the halving shape

Add the pieces because they are in a row

$$\text{total} = n + \tfrac{n(n-1)}{2} + \log_2 n$$

sequence means addition; none of the three is inside another

$$= \tfrac12 n^{2} + \tfrac12 n + \log_2 n$$

multiplied out, so the terms can be compared

Apply the two rules

$$\to \tfrac12 n^{2}$$

the square grows faster than the linear term and than the logarithm, so the other two go

$$\to O(n^{2})$$

and the one half is a constant factor

Answer $$\boxed{O(n^{2})}$$
Check

With six items the triangle has 6 times 5 over 2, that is 15 cells, and the run found 4 equal pairs among them, so the loop really did examine at most 15 pairs and not 36.

Three pieces priced, one addition, two rules.

A sum of terms in a row is decided entirely by its worst piece.

The triangular nested loop: 4950 passes on a hundred items

A nested loop whose inner range depends on the outer counter. It does strictly less work than a full pair of nested loops, and the question is whether that changes the class.

def count_inner(n):
    """Assumes n is an int >= 0.
    Returns how many times the inner line of the nested loop runs.
    """
    total = 0
    for i in range(n):
        for j in range(i):
            total = total + 1
    return total


for n in [4, 10, 100]:
    print('n =', n, 'inner line ran', count_inner(n), 'times, n*n is',
          n * n)

Sample Run:

n = 4 inner line ran 6 times, n*n is 16
n = 10 inner line ran 45 times, n*n is 100
n = 100 inner line ran 4950 times, n*n is 10000

Give the exact count and the class, and say how the count compares with n times n.

FindA formula for the number of inner passes, the class, and the ratio to n times n.
Given
  • The outer range is range(n) and the inner range is range(i).

  • range(0) gives no passes at all, so the first outer pass contributes nothing.

  • The three sizes tried are 4, 10 and 100.

Solution

Write out the inner counts and add them

$$i = 0, 1, 2, \ldots, n-1$$

the outer counter takes these values, and the inner range is range of that value

$$0 + 1 + 2 + \cdots + (n-1)$$

so the inner pass counts are the values themselves, and the total is their sum

$$= \tfrac{n(n-1)}{2}$$

the standard sum of the first n-1 whole numbers, which is why the shape is called triangular

Check the formula against all three runs

$$n = 4:\ \tfrac{4 \cdot 3}{2} = 6$$

the run printed 6, and the same formula is now worth trusting at sizes you cannot check by hand

$$n = 100:\ \tfrac{100 \cdot 99}{2} = 4950$$

which the run also printed

Compare with the full grid and name the class

$$\frac{4950}{10000} = 0.495$$

so the triangle is just under half of the full grid at n equal to 100

$$\tfrac{n(n-1)}{2} \to O(n^{2})$$

the fastest growing term is n squared over 2, and the one half is a constant factor, so the class is the same as the full grid's

Answer $$\boxed{\tfrac{n(n-1)}{2} \text{ passes} = O(n^{2})}$$
Check

From n equal to 10 to n equal to 20 the formula predicts 45 passes going to 190, a factor of 4.2.

Half the passes of the full nested version, and the same class.

Doing half the work is a constant factor.

Checkpoint
§09.6 — two loops in a row against two loops nested

Two functions over the same list. They contain the same two loops and differ only in the indentation of the second one.

def version_a(L):
    """Assumes L is a list of ints. Returns two counts."""
    first = 0
    for x in L:
        first = first + 1
    second = 0
    for y in L:
        second = second + 1
    return first, second


def version_b(L):
    """Assumes L is a list of ints. Returns two counts."""
    first = 0
    for x in L:
        first = first + 1
        second = 0
        for y in L:
            second = second + 1
    return first, second
Find(a) Which pair of classes is right?
Given
  • L holds n items.

  • In version a the second loop is at the same indentation as the first.

  • In version b the second loop is indented inside the first.

Hint 1/4

Count total passes of the counter line, not loops on the page.

Hint 2/4

Loops that follow one another add their pass counts. Loops that are nested multiply them.

Hint 3/4

Here both loops run over the same list of n items. In version a they follow one another and in version b the second is inside the first.

Hint 4/4

Version a is O(n) and version b is O(n^2).

Show solution

Count the passes at one concrete size before naming any class.

Count version a

$$n + n = 2n$$

the loops are in a row, so their pass counts add

$$\to O(n)$$

the 2 is a constant factor and goes

Count version b

$$n \times n = n^{2}$$

the inner loop restarts in full on each outer pass, so the counts multiply

$$\to O(n^{2})$$

nothing to drop here, the term is already bare

Answer $$\boxed{O(n)\ \text{and}\ O(n^{2})}$$
Check

Both versions return (n, n), so they give the same answer at costs that differ by a factor of n over 2.

Indentation is a complexity decision in Python, and this is the question shape that tests it.

⚠ Adding nested loops instead of multiplying them

Reading down the page, each loop looks like another item on a list of things the function does, and items on a list get added.

wrong$$\text{nested } n \text{ and } n \Rightarrow 2n$$
right$$\text{nested } n \text{ and } n \Rightarrow n^{2}$$
⚠ Pricing a list method as one step

On the page item in L is one short expression, and nothing in it looks like a loop.

wrong$$\texttt{for x in A: if x in B} \Rightarrow O(n)$$
right$$\texttt{for x in A: if x in B} \Rightarrow O(n^{2})$$
⚠ Expecting a triangular nested loop to be cheaper by a class

It genuinely does less than half the work, and half feels like the sort of saving that ought to show up in the answer.

wrong$$\tfrac{n(n-1)}{2} \Rightarrow O(n)$$
right$$\tfrac{n(n-1)}{2} \Rightarrow O(n^{2})$$

9.7Space against time, and paying a cost once to spread it over many uses

Shows when spending memory up front pays, and from how many uses onward.

So far the question has been time. Memory is the other resource, and you can buy one with the other.

Solvable with what we have
  • Read the class of a scan off its loop, which is linear.

  • See that repeating that scan k times costs k times n.

  • Price a dictionary lookup as constant.

Not solvable yet
  • Say whether building the dictionary is worth the trouble.

  • Give the number of lookups at which the trade pays for itself.

  • Defend spending memory when the class does not improve.

The reflex by now is to compare the classes and prefer the smaller. A scan is linear per search, a dictionary lookup is constant, so the dictionary wins. It fails in the obvious case: for a single search, building the dictionary costs a full pass and then you still do the lookup.

Why it fails

Comparing the per search classes answers a question nobody asked. What matters is the whole job: the one off build plus the number of searches times the cost of each. Until you fix that number there is no answer, and it is the number, not the class, that decides.

RuleRule 9.7: when a one off cost pays for itself
Conditions
  • Fix the number of uses first. Without it the comparison has no answer, and that is not a failing of the method.

  • The memory spent is part of the price. A dictionary over n names holds n entries, so the trade costs O(n) extra space.

  • Spreading a one off cost over many later uses is called amortizing it, and the per use figure is only meaningful if the uses actually happen.

$$\boxed{\ \text{total}(k) = \underbrace{B}_{\text{build once}} + k \cdot \underbrace{U}_{\text{each use}}\ }$$

The whole job costs the build plus the number of uses times the cost of one use. Write that sum for each design and set the two equal: the solution is the number of uses at which they are level. Below it the design with no build wins, above it the other does.

Looks like this, but is not

The rule looks like an argument for building an index whenever you can, since the crossing point is usually low. So a program that reads one name from a file of a million and reports its position gets an index built for it first.

The crossing point is where the two designs are level, not where the index becomes free. For a single lookup the index costs a full pass plus memory for a million entries, and the scan costs at most a full pass and no memory. The rule says count the uses first, and at one use it says do not build.

searches kscan each timebuild once, then look upwhich wins

1

100

101

scan, by one step

2

200

102

index

10

1000

110

index, by nine times

100

10000

200

index, by fifty times

The crossing is between one search and two, and after that the index design pulls away without limit.

Five names, one scan against one dictionary

Two ways to answer where is this name in the list. The first walks the list and counts the names it read. The second builds a dictionary from name to position once, and then reaches the answer directly.

def build_index(names):
    """Assumes names is a list of strings.
    Returns a dictionary that maps each name to its position in the list.
    """
    index = {}
    for i in range(len(names)):
        index[names[i]] = i
    return index


def position_by_scan(names, wanted):
    """Assumes names is a list of strings.
    Returns the position of wanted and the number of names looked at.
    """
    looked = 0
    for i in range(len(names)):
        looked = looked + 1
        if names[i] == wanted:
            return i, looked
    return -1, looked


names = ['ada', 'boran', 'ceren', 'deniz', 'eren']
print(position_by_scan(names, 'eren'))
index = build_index(names)
print(index)
print(index['eren'])

Sample Run:

(4, 5)
{'ada': 0, 'boran': 1, 'ceren': 2, 'deniz': 3, 'eren': 4}
4

Give the cost of each design for k searches over a list of n names, and find the k at which they are level.

FindA cost formula in n and k for each design, and the crossing point.
Given
  • names holds 5 strings, so n equals 5.

  • The scan read 5 names to find the last one.

  • Building the dictionary makes one pass over the list.

  • A dictionary lookup does not grow with the number of keys.

Solution

Write the cost of each design

$$\text{scan: } \textcolor{#d1690a}{k \cdot n}$$

no build, and each search reads up to the whole list, so the worst case per search is n

$$\text{index: } \textcolor{#1f6feb}{n + k}$$

one pass to build, then a constant per search, and we take that constant as 1

Set them level and solve for k

$$k n = n + k$$

level means equal total cost for the same job

$$k(n-1) = n \Rightarrow k = \frac{n}{n-1}$$

collect the k terms; this is legitimate since n is at least 2 for the question to mean anything

$$n = 5:\ k = 1.25$$

so from two searches onward the index is already cheaper, even on a list of five

Check the numbers against the run

$$k = 1:\ 5 \text{ against } 6$$

the run shows the scan reading 5 names, and the index costing 5 to build plus 1 to look up, so one search really is cheaper without the index

$$k = 10:\ 50 \text{ against } 15$$

and at ten searches the index has more than paid for itself

Answer $$\boxed{\text{scan } kn,\ \text{index } n+k,\ \text{level at } k = \tfrac{n}{n-1} \approx 1}$$
Check

At n equal to 100 the scan costs 200 against the index's 102 for two searches, and 100 against 101 for one.

The index also costs memory: n entries for a list of n names.

Write both totals as build plus uses times cost, then set them equal.

Checkpoint
§09.7 — counting the reads of a rebuilt index

A function builds a small dictionary and reports how many list positions it read while building. The build sits inside a loop, so it happens more than once.

def lookup_all(names, wanted):
    """Assumes names and wanted are lists of str.
    Returns the positions found and the number of positions read.
    """
    found = []
    read = 0
    for name in wanted:
        index = {}
        for i in range(len(names)):
            read = read + 1
            index[names[i]] = i
        found.append(index[name])
    return found, read


names = ['a', 'b', 'c', 'd']
print(lookup_all(names, ['d', 'b']))
print(lookup_all(names, ['a']))
Find(a) Write the two lines this prints.
Given
  • names holds 4 items in both calls.

  • The first call asks for 2 names and the second for 1.

  • The counter is raised once per list position visited during a build.

IPython console
Hint 1/4

Two things to produce per call: the list of positions found, and the total number of reads.

Hint 2/4

The build is inside the loop over wanted, so it is paid once per wanted name.

Hint 3/4

Here names has 4 items. The first call wants 2 names and the second wants 1, and the positions in the dictionary are a 0, b 1, c 2, d 3.

Hint 4/4

The first line is ([3, 1], 8) and the second is ([0], 4).

Show solution

Find the positions first and the reads second.

Read the positions out of the dictionary

$$\texttt{\{a: 0, b: 1, c: 2, d: 3\}}$$

the build writes each name against its index, and the same dictionary is rebuilt identically each pass

$$\texttt{['d', 'b']} \to [3, 1]$$

in the order asked for, since append adds to the end

Count the reads

$$2 \times 4 = 8$$

the build is inside the loop over wanted, so it is paid once per wanted name

$$1 \times 4 = 4$$

the second call wants one name, so one build

Answer $$\boxed{([3, 1],\ 8)\quad\text{and}\quad ([0],\ 4)}$$
Check

The first item of each tuple must be as long as the wanted list passed in, since append is called once per pass, and the counts 2 and 1 match.

A cost that does not depend on the loop variable should be outside the loop.

⚠ Building the lookup structure inside the loop that uses it

The build and the use belong to the same idea, so they get written together, and the loop that needs the use ends up containing both.

wrong$$k \cdot (n + 1) = O(kn)$$
right$$n + k = O(n + k)$$
⚠ Comparing the per use costs and ignoring the build

The per use classes are the two numbers the section taught you to produce, so comparing them feels like the method being applied.

wrong$$O(1) < O(n) \Rightarrow \text{always index}$$
right$$n + k \ \text{against} \ kn:\ \text{index wins for } k \ge 2$$
⚠ Growing a list with the plus operator inside a loop

out = out + [x] reads like out.append(x) and both lines end with one item more in the list.

wrong$$\texttt{out = out + [x]} \Rightarrow O(n)$$
right$$\texttt{out = out + [x]} \Rightarrow O(n^{2}),\quad \texttt{out.append(x)} \Rightarrow O(n)$$
Reading a complexity class off a function

Any question that prints a function and asks for its complexity, which is the commonest shape this material takes in an exam.

  1. Name n

    Write one line saying what n counts: the length of the list, the number of characters, the value of the argument.

  2. Cut the function into pieces

    A piece is one loop with its body, or a stretch with no loop in it.

  3. Price each piece

    For each piece: how many passes, in terms of n, and what one pass costs.

  4. Combine: nested multiplies, sequence adds

    Multiply the passes by the cost of a pass within each piece, then add the pieces that follow one another.

  5. Simplify with the two rules

    Keep the fastest growing term and throw the rest away. Then drop the constant factor from what is left. In that order.

  6. Check and label

    Double n and predict: a linear count doubles, a quadratic one quadruples, a logarithmic one gains a constant.

Where it goes wrong
  • Adding two nested loops, which turns a quadratic answer into a linear one.

  • Pricing item in L or L.index(item) as one step, which turns a quadratic answer into a linear one from the other direction.

  • Simplifying each piece before adding them, which loses the term that was going to dominate.

  • Leaving a constant in the class, as O(3n) or O(n/2).

  • Answering with the best case because that was the run in your head.

Producing an exact step count when the question asks for one

When the wording is how many steps, or how many times does this line run, rather than what is the complexity.

  1. State your price list

    Write one line: a binding, a comparison, an arithmetic operation and a list index each cost one.

  2. Price the body of each loop once

    Go line by line through the body and add the prices.

  3. Count the passes exactly

    range(n) gives n. range(i) inside range(n) gives the , which is n times n minus 1 over 2.

  4. Assemble and test at one small size

    Write the formula, then substitute a size you can check by hand, such as n equal to 3, and count the passes on paper.

Where it goes wrong
  • Forgetting the final failing test of a while, which changes the count by one and is worth saying out loud rather than hiding.

  • Counting answer = answer * n as one step in one place and two in another inside the same answer.

  • Giving a number rather than a formula, which answers the question for one input only.

Deciding whether to build a lookup structure

When a program searches the same collection more than once, or when a lab asks you to justify a design rather than only to write it.

  1. Count the uses

    Find k, the number of times the collection will be searched.

  2. Write both totals as build plus uses

    Scanning: no build, and k times the cost of one scan. Indexing: one build, and k times the cost of one lookup.

  3. Set them level and solve for k

    The k that makes the two totals equal is the crossing point.

  4. Add the memory to the account

    Say what the structure costs in space, normally O(n) extra.

Where it goes wrong
  • Comparing the per use classes and ignoring the build, which always recommends the index.

  • Building the structure inside the loop that uses it, which pays the build k times.

  • Reporting a crossing point without saying what n was, since the crossing point normally depends on it.

Inner range is the whole list: quadratic

Two loops, the inner one running over the same list as the outer.

def pair_count(L):
    """Assumes L is a list. Returns how many times the inner line runs."""
    runs = 0
    for i in range(len(L)):
        for j in range(len(L)):
            runs = runs + 1
    return runs
FindThe pass count and the class.
Given
  • The outer range is range(len(L)).

  • The inner range is also range(len(L)).

Solution

Price the two loops

$$n \text{ outer passes}$$

one per position of the list

$$n \text{ inner passes each}$$

the inner range mentions the list length, so it restarts in full on every outer pass

$$n \times n = n^{2}$$

nested, so multiply

Answer $$\boxed{n^{2} \text{ passes} = O(n^{2})}$$
Check

At 400 items the counted passes were 160000, which is 400 squared exactly, so the formula is not an estimate.

Inner range is a fixed number: linear

The same two loops, with one character changed in the inner range.

def triple_count(L):
    """Assumes L is a list. Returns how many times the inner line runs."""
    runs = 0
    for i in range(len(L)):
        for j in range(3):
            runs = runs + 1
    return runs
FindThe pass count and the class.
Given
  • The outer range is range(len(L)).

  • The inner range is range(3), which does not mention the list.

Solution

Price the two loops

$$n \text{ outer passes}$$

unchanged from the other version

$$3 \text{ inner passes each}$$

the inner range is a fixed number, so it does not grow with the list

$$3n \to O(n)$$

multiply, then drop the constant factor

Answer $$\boxed{3n \text{ passes} = O(n)}$$
Check

At 400 items the counted passes were 1200, which is 3 times 400, so the count really is proportional to the length and not to its square.

Both are two for lines with one indented under the other, and one is quadratic while the other is linear, because only one of them has a list length in the inner range.

How to tell them apart

Look at the inner range and ask whether n appears in it. If it does, multiply two things that both grow.

Searching a list inside a loop: quadratic

One visible loop, and a membership test in its body over a list.

def shared_with_list(first, second):
    """Assumes first and second are lists.
    Returns the items of first that also appear in second.
    """
    both = []
    for item in first:
        if item in second:
            both.append(item)
    return both
FindThe class in the worst case.
Given
  • Both lists hold n items.

  • second is a list, so in walks it.

Solution

Price the body honestly

$$n \text{ passes}$$

one per item of the first list

$$\text{up to } n \text{ comparisons per pass}$$

in on a list compares entry by entry, and the worst case is the one with no match

$$n \times n = O(n^{2})$$

the hidden walk multiplies with the visible loop

Answer $$\boxed{O(n^{2})}$$
Check

Two disjoint lists of eight items gave 64 counted comparisons, and of four items gave 16. Twice the size, four times the count.

Searching a dictionary inside the same loop: linear

The same loop, with the second collection turned into a dictionary first.

def shared_with_dict(first, second):
    """Assumes first and second are lists.
    Returns the items of first that also appear in second.
    """
    table = {}
    for item in second:
        table[item] = True
    both = []
    for item in first:
        if item in table:
            both.append(item)
    return both
FindThe class in the worst case.
Given
  • Both lists hold n items.

  • table is a dictionary, so in on it does not walk.

Solution

Price the two pieces in a row

$$n \text{ to build the table}$$

one pass per item of the second list, with a constant body

$$n \text{ passes at constant cost each}$$

the membership test on a dictionary does not grow with the number of keys

$$n + n = 2n \to O(n)$$

the two pieces follow one another, so add, then drop the constant factor

Answer $$\boxed{O(n)}$$
Check

The function returns the same list as the other version on the same inputs, so the change is purely a cost change.

The two functions return the same answer and differ by a class, and the whole difference is which kind of collection the word in is applied to.

How to tell them apart

in on a list is O(n) and in on a dictionary is O(1).

Scaffolding comes off
The common skeleton
  1. Say what n counts, in words, before writing anything else.

  2. Take the function one piece at a time, where a piece is a loop or a loop free stretch.

  3. For each piece: how many times does its body run, in terms of n, and what does one run of that body cost including any call inside it.

  4. Multiply those two per piece, and add the pieces, because pieces in a row add and pieces nested inside one another multiply.

  5. Apply the two rules to the sum: keep the fastest growing term, then drop its constant factor.

  6. Check it by doubling n and predicting the count, then say which case you reported.

1 · fully worked

Full working: a summary function with three pieces

Give the complexity class of this function in terms of the length of L, showing the working.

def tally(L, limit):
    """Assumes L is a list of ints and limit an int.
    Returns how many items are below limit, how many pairs are equal,
    and how many halvings the length survives.
    """
    below = 0
    for x in L:
        if x < limit:
            below = below + 1
    equal = 0
    for i in range(len(L)):
        for j in range(i + 1, len(L)):
            if L[i] == L[j]:
                equal = equal + 1
    size = len(L)
    rounds = 0
    while size > 1:
        size = size // 2
        rounds = rounds + 1
    return below, equal, rounds


print(tally([3, 8, 3, 1, 8, 8, 2, 9], 5))

Sample Run:

(4, 4, 3)
FindThe class of the whole function, with working.
Given
  • n is the length of L.

  • The three pieces of work follow one another.

  • The run uses a list of eight items and a limit of 5.

Solution

Step 1: name n

$$n = \texttt{len(L)}$$

all three pieces are driven by the length, and limit does not affect how many passes anything makes

Steps 2 and 3: price each piece

$$\text{piece 1: } n \text{ passes}$$

one per item, and the body is a comparison and sometimes an addition, so constant

$$\text{piece 2: } \tfrac{n(n-1)}{2} \text{ passes}$$

nested with the inner range starting at i+1, so the triangular sum rather than the full grid

$$\text{piece 3: } \log_2 n \text{ passes}$$

the counter is divided rather than reduced, so this is the halving shape

Step 4: combine

$$n + \tfrac{n(n-1)}{2} + \log_2 n$$

the three pieces are in a row, so their counts add

$$= \tfrac12 n^{2} + \tfrac12 n + \log_2 n$$

multiplied out so the terms are comparable

Step 5: simplify

$$\to \tfrac12 n^{2}$$

n squared grows faster than n and than log n, so the other two terms go

$$\to O(n^{2})$$

the one half is a constant factor

Step 6: check by doubling, and say the case

$$n = 8:\ \tfrac{8 \cdot 7}{2} = 28 \text{ inner passes}$$

the dominant piece at the size in the run

$$n = 16:\ \tfrac{16 \cdot 15}{2} = 120$$

roughly four times 28, which is the signature of a quadratic, and this is the worst case since no piece can stop early

Answer $$\boxed{O(n^{2})}$$
Check

At n equal to 8, piece 1 makes 8 passes, piece 2 makes 28 and piece 3 makes 3, so the middle piece already dominates.

Three pieces priced, one addition, two rules, one doubling check. About a minute of work on paper.

This is the whole skeleton on one function.

2 · you write the reasoning

Here is a shorter function with one loop, and the working is already written out. Your job is the reason column: for each step, say why it is true. The arithmetic is done, so this rung is easier than the last one on purpose. Writing the reasons is the part that carries over to a function you have not seen.

def total_with_rate(prices, rate):
    """Assumes prices is a list of floats and rate a float.
    Returns the total after the rate has been added to each price.
    """
    total = 0.0
    for p in prices:
        total = total + p * (1 + rate)
    return total


print(format(total_with_rate([10.0, 20.0, 30.0], 0.2), '.2f'))

Sample Run:

72.00
  1. reasoning

    n is the number of prices, because the only loop runs over the list and nothing else in the function depends on a size.

  2. reasoning

    There is one loop and nothing nested inside it, so there is one piece to price and no multiplication to do between pieces.

  3. reasoning

    The body is one addition, one multiplication, one addition inside the brackets, and one binding to total.

  4. reasoning

    Four steps on each of n passes, plus the initial binding of total and the return, which are the two steps paid once whatever the list length.

  5. reasoning

    The fastest growing term is 4n, so the 2 goes; then the constant factor 4 goes. Both rules applied, in order.

  6. reasoning

    Doubling the list doubles the number of passes and leaves the cost of a pass alone, so the count roughly doubles.

3 · find the buried error

Now a harder one, and somebody else has already answered it. This function has two pieces, and the first of them is nested. The working below reaches a class, and the class is wrong. There are exactly two faulty steps.

def count_ordered_pairs(L):
    """Assumes L is a list of ints.
    Returns how many position pairs have L[i] < L[j], and the
    largest item.
    """
    pairs = 0
    for i in range(len(L)):
        for j in range(len(L)):
            if L[i] < L[j]:
                pairs = pairs + 1
    top = L[0]
    for x in L:
        if x > top:
            top = x
    return pairs, top


print(count_ordered_pairs([5, 1, 9, 3]))

Sample Run:

(6, 9)

Find the two faulty steps.

the two buried errors (2)
⚠ step 2

The two nested loops are added rather than multiplied. The inner loop restarts in full on every outer pass, so the inner line runs n times n, that is n squared, not 2n.

Reading down the page, each loop looks like another job the function does, and jobs on a list get added. The indentation is the only thing saying otherwise and it is easy to skim past.

right

Replace 2n with n squared. At the size in the run, n equal to 4, the inner line runs 16 times and not 8, and the printed 6 is the number of pairs that passed the test rather than the number examined.

⚠ step 4

The class is written with the constant factor still in it. Even if the count really had been 3n, the class would be O(n): the second rule drops the constant factor and it was not applied.

The first rule feels like the work, so once a term has been chosen the answer feels finished. The second rule is one clause long and gets skipped.

right

Write O of the bare function. With step 2 corrected the count is n squared plus n, the fastest growing term is n squared, and the class is O(n squared).

4 · the bare problem
§09.6 — three pieces, one of them nested and triangular

No working given this time. Here is a function with three pieces of work, one of which is a nested loop with a triangular inner range and a method call in its body.

def audit(L):
    """Assumes L is a list of ints with at least one item.
    Returns the number of items, the number of items that repeat an
    earlier item, and the number of halvings the length survives.
    """
    seen = []
    repeats = 0
    for x in L:
        if x in seen:
            repeats = repeats + 1
        else:
            seen.append(x)
    size = len(L)
    rounds = 0
    while size > 1:
        size = size // 2
        rounds = rounds + 1
    return len(L), repeats, rounds


print(audit([7, 2, 7, 4, 2, 7, 1, 9]))
Find
  1. (a) Give the worst case class of audit in terms of n, with the working set out in the six steps.

  2. (b) Say what the run prints, and say which of the three returned numbers tells you nothing about the cost.

Given
  • n is the length of L.

  • seen starts empty and grows by at most one item per pass.

  • x in seen walks seen from the front.

  • The run uses the list [7, 2, 7, 4, 2, 7, 1, 9].

Hint 1/4

There is only one for line here, so the trap is not the indentation.

Hint 2/4

A membership test on a list walks it, so its cost is the current length of that list.

Hint 3/4

Here seen has at most 0, 1, 2 and so on up to n minus 1 items when the tests happen, and the list in the run is [7, 2, 7, 4, 2, 7, 1, 9], in which 7 appears three times and 2 twice.

Hint 4/4

The worst case is O(n^2), and the run prints (8, 3, 3).

Show solution

The one move that matters here is refusing to multiply.

Steps 1 to 3: name n and price the body pass by pass

$$n = \texttt{len(L)}$$

the loop and the halving loop are both driven by the length

$$\text{pass } i:\ \texttt{len(seen)} \le i$$

seen gains at most one item per pass, so on pass i the walk has at most i items to get through

$$\text{worst case: every item new}$$

then seen gains exactly one per pass and the bound above is reached on every pass

Step 4: add the passes rather than multiplying them

$$0 + 1 + 2 + \cdots + (n-1)$$

the cost of pass i is i comparisons, so the total is the sum of those costs

$$= \tfrac{n(n-1)}{2}$$

the triangular sum, the same one the nested triangular loop produced

$$+ \log_2 n$$

the halving piece follows in a row, so it adds

Step 5: simplify

$$\tfrac12 n^{2} - \tfrac12 n + \log_2 n \to \tfrac12 n^{2}$$

n squared over 2 is the fastest growing of the three terms

$$\to O(n^{2})$$

the one half is a constant factor and goes

Step 6: check, name the case, and read the run

$$n = 8:\ \tfrac{8 \cdot 7}{2} = 28$$

the predicted worst case walk count at the size in the run

$$\text{best case} = O(n)$$

a list of identical items keeps seen at length one, so this is the worst case and it is worth saying so

$$(8, 3, 3)$$

8 items, three of them repeats of an earlier item, and 8 halves to 1 in three steps

Answer $$\boxed{O(n^{2}) \text{ worst, } O(n) \text{ best};\ (8, 3, 3)}$$
Check

The actual walks on that list cost 0, 1, 1, 2, 2, 1, 3 and 4, which is 14, comfortably under the worst case bound of 28.

When a loop body walks something that is growing, sum the pass costs instead of multiplying.

Full exam-style question

Exam shape: trace the output, then give the class with reasonsexam format

This is the shape the material takes in a paper: one program, one part asking for the exact printed output and one part asking for the complexity with a reason. Both parts are marked, and the second part is marked on the reason and not only on the class.

def merge_counts(a, b):
    """Assumes a and b are lists of ints.
    Returns a list of the items of a that are not in b, the number of
    comparisons made, and the number of halvings len(a) survives.
    """
    only_a = []
    compares = 0
    for x in a:
        found = False
        for y in b:
            compares = compares + 1
            if x == y:
                found = True
        if not found:
            only_a.append(x)
    size = len(a)
    rounds = 0
    while size > 1:
        size = size // 2
        rounds = rounds + 1
    return only_a, compares, rounds


print(merge_counts([4, 7, 9, 2], [7, 2]))
print(merge_counts([4, 7, 9, 2, 5, 5, 1, 8], [7]))

Sample Run:

([4, 9], 8, 2)
([4, 9, 2, 5, 5, 1, 8], 8, 3)

Part (a): write the two lines the program prints. Part (b): give the complexity in terms of the lengths of a and b, and say whether the inner loop has a best case.

FindThe two printed lines, the class in terms of n and m, and whether the inner loop can stop early.
Given
  • a has n items and b has m items.

  • The inner loop has no break in it, so it always runs to the end of b.

  • The first call passes [4, 7, 9, 2] and [7, 2].

  • The second call passes [4, 7, 9, 2, 5, 5, 1, 8] and [7].

Solution

Part (a): trace the first call

$$a = [4, 7, 9, 2],\ b = [7, 2]$$

four outer passes, each making a full pass over the two items of b

$$\text{kept: } 4, 9$$

7 and 2 are both in b, so only 4 and 9 are appended, in the order they appear in a

$$\text{compares} = 4 \times 2 = 8$$

the inner loop has no early exit, so every outer pass costs exactly the length of b

$$\text{rounds}: 4 \to 2 \to 1 = 2$$

the halving loop is driven by the length of a, which is 4

Part (a): trace the second call

$$a \text{ has 8 items},\ b = [7]$$

eight outer passes, each making one comparison

$$\text{kept: everything except } 7$$

so the list keeps 4, 9, 2, 5, 5, 1, 8 including both fives, since duplicates in a are not removed

$$\text{compares} = 8 \times 1 = 8$$

the same total as the first call, by a coincidence of the sizes, which is a useful reminder that the count is a product

$$\text{rounds}: 8 \to 4 \to 2 \to 1 = 3$$

three halvings for a list of eight

Part (b): price the pieces

$$\text{piece 1} = n \times m$$

nested, and the inner count is the length of b, so the two sizes multiply

$$\text{piece 2} = \log_2 n$$

the halving loop is driven by the length of a only

$$n m + \log_2 n \to O(nm)$$

the product grows faster than the logarithm of one of its factors, and there is no constant factor to drop

Part (b): answer the question about the best case

$$\text{no } \texttt{break} \Rightarrow \text{cost} = m \text{ always}$$

the inner loop sets a flag instead of returning, so it cannot stop early and every pass costs m

$$\text{best} = \text{worst} = O(nm)$$

which means this function has no best case worth reporting, and adding a break after the flag is set would give it one

Answer $$\boxed{([4, 9], 8, 2)\ \text{and}\ ([4, 9, 2, 5, 5, 1, 8], 8, 3);\ O(nm)}$$
Check

The count has to be exactly the product of the two lengths, because the inner loop has no exit: 4 times 2 is 8 and 8 times 1 is 8.

Two nested loops over different lists give a product of two different sizes, which is why the answer names both.

When two loops run over two different collections, the class carries two letters.

Practice

A · concept 3 questions
1§09.3 — a true statement that loses the mark

A function has been counted honestly and its step count is 5n plus 2. A student writes on the paper that this function is O of n squared.

The claim: that statement is mathematically true.

Find(a) True or false, and say what should have been written.
Given
  • The exact count is 5n plus 2.

  • The definition says a count is O of g when it stays at or below some multiple of g past some size.

  • The question on the paper asked for the complexity of the function.

Hint 1/4

Two different questions are hiding here: is the statement true, and is it the right answer. Deal with the first one only.

Hint 2/4

The definition asks for a multiplier and a threshold such that the count stays under the multiplier times the bound from the threshold on.

Hint 3/4

Here the count is 5n plus 2 and the proposed bound is n squared. Try the multiplier 1 and the threshold 6: at n equal to 6 the count is 32 and n squared is 36.

Hint 4/4

True, and the answer that earns the mark is O of n.

Show solution

Settle the truth of the statement by producing a witnessing pair, then answer the separate question of what should have been written.

Show the statement is true

$$5n + 2 \le 1 \cdot n^{2} \text{ for } n \ge 6$$

at n equal to 6 the two sides are 32 and 36, and the right side grows faster from there

$$c = 1,\ n_0 = 6$$

a witnessing pair exists, so the claim satisfies the definition

Say why it is not the answer

$$5n + 2 = O(n)$$

the tightest bound, found by the two rules

$$O(n) \subset O(n^{2})$$

so the student's answer is implied by the right one and does not identify it

Answer $$\boxed{\text{True, but the answer is } O(n)}$$
Check

At n equal to 1000 the count is 5002 and n squared is a million, so the claimed ceiling is two hundred times the count.

True and useful are different. When a question asks for the complexity, produce the tightest class you can justify and nothing looser.

2§09.5 — which of these four loops is logarithmic

Four while loops, all counting their own passes, all starting from the same value of n. Only one of them has a pass count that grows like a logarithm.

# loop 1
while n > 1:
    n = n - 1

# loop 2
while n > 1:
    n = n // 3

# loop 3
while n > 1:
    n = n - n // 2

# loop 4
while n > 1:
    n = n * 2
    if n > 1000000:
        n = 0
Find(a) Which loop has a logarithmic pass count?
Given
  • All four start from the same n, a whole number above 1.

  • // is whole number division.

  • Loop 3 replaces n by n minus half of n.

Hint 1/4

Take n equal to 1000 and count the passes of each loop. Do not reason from the shape of the code.

Hint 2/4

A counter reduced by a fixed amount gives a linear pass count.

Hint 3/4

From 1000: loop 1 takes 999 passes; loop 2 goes 333, 111, 37, 12, 4, 1; loop 3 goes 500, 250, 125, 63, 32, 16, 8, 4, 2, 1; loop 4 climbs to a million and is then set to 0.

Hint 4/4

Loop 2 is the logarithmic one.

Show solution

Count the passes from one concrete starting value.

Count loops 1 and 2 from 1000

$$1000 \to 999 \to \cdots \to 1:\ 999 \text{ passes}$$

a fixed amount removed per pass gives a linear count

$$1000 \to 333 \to 111 \to 37 \to 12 \to 4 \to 1$$

six passes, and each one divides by three, which is the logarithmic shape

Look closely at loops 3 and 4

$$n - n//2 = \lceil n/2 \rceil$$

so loop 3 also divides, by two, with the rounding going the other way

$$\text{loop 4 stops at } 10^{6} \text{ regardless of } n$$

its pass count is set by the ceiling in the body, not by the starting value

Answer $$\boxed{\text{loop 2}}$$
Check

From 2000 loop 2 takes seven passes rather than six, one more; loop 1 takes 1999 rather than 999, twice as many.

Read the assignment to the counter, not the keyword in front of the loop.

3§09.6 — growing a list two ways

Two loops, each adding one item per pass to a list that starts empty. One uses the append method and the other uses the plus operator.

def with_append(n):
    """Assumes n is an int >= 0. Returns a list of n zeros."""
    out = []
    for i in range(n):
        out.append(0)
    return out


def with_plus(n):
    """Assumes n is an int >= 0. Returns a list of n zeros."""
    out = []
    for i in range(n):
        out = out + [0]
    return out

The claim: the two functions are in the same complexity class, because both have one loop over n and both add one item per pass.

Find(a) True or false, and give the class of each.
Given
  • Both functions return a list of n zeros.

  • out.append(0) puts an item on the end of the existing list.

  • out + [0] builds a new list holding everything out had, plus one.

Hint 1/4

Both loops make n passes, so the passes are not where the difference is. Price one pass of each body.

Hint 2/4

Appending puts an item on the end of the list that is already there.

Hint 3/4

Here on pass i the list holds i items, so the copy costs i. Add those costs over all n passes rather than multiplying.

Hint 4/4

False: append gives O(n) and the plus operator gives O(n squared).

Show solution

Price the body rather than counting the loops, because the loops are identical.

Price the append version

$$n \text{ passes} \times \text{constant}$$

appending does not touch the items already in the list

$$O(n)$$

one multiplication, nothing to add up

Price the plus version pass by pass

$$\text{pass } i \text{ costs } i$$

the new list has to receive copies of the i items already in out

$$0 + 1 + \cdots + (n-1) = \tfrac{n(n-1)}{2}$$

the costs differ per pass, so they are summed rather than multiplied

$$O(n^{2})$$

fastest growing term is n squared over 2, and the one half is a constant factor

Answer $$\boxed{\text{False: } O(n) \text{ and } O(n^{2})}$$
Check

At n equal to 4 the plus version copies 0, then 1, then 2, then 3 items, which is 6 copies, and 4 times 3 over 2 is 6.

Any line that rebuilds a structure inside a loop over that structure is a hidden second loop.

B · computation 5 questions
1§09.5 — a counter divided, and what is left at the end

A while loop that halves its counter with whole number division, keeping a tally of its passes. Both the final counter and the tally are printed.

n = 100
steps = 0
while n > 1:
    n = n // 2
    steps = steps + 1
print(n, steps)
Find(a) Write the line this prints.
Given
  • n starts at 100.

  • // divides and throws the fraction away.

  • The loop stops when n is no longer greater than 1.

IPython console
Hint 1/4

Two answers wanted: what the counter finished at, and how many passes it took.

Hint 2/4

Each pass replaces the counter by its half with the fraction thrown away, and the loop stops as soon as the counter is not above 1.

Hint 3/4

From 100 the values are 50, 25, 12, 6, 3, 1. Note the 25 halving to 12 rather than to 12.5.

Hint 4/4

It prints 1 and 6.

Show solution

Write the chain of values out.

Follow the counter

$$100 \to 50 \to 25 \to 12 \to 6 \to 3 \to 1$$

each arrow is one pass, and the divisions that do not come out exactly round down

$$25 // 2 = 12,\quad 3 // 2 = 1$$

the two places the rounding matters, and the second is what ends the loop

Read both printed values

$$\texttt{n} = 1$$

the condition fails once the counter is not above 1

$$\texttt{steps} = 6$$

one increment per arrow

Answer $$\boxed{1\quad 6}$$
Check

2 to the sixth is 64 and 2 to the seventh is 128, so 100 lies between them and the halving count must be 6.

Bracketing the value between two powers of two gives the halving count without tracing, and it is the check to use when the starting value is large.

2§09.6 — a nested loop whose outer counter is reassigned

A triangular nested loop, with one extra line: the outer loop variable is changed at the end of each outer pass. Both the total and the final value of the outer variable are printed.

total = 0
for i in range(4):
    for j in range(i):
        total = total + 1
    i = i + 10
print(total, i)
Find(a) Write the line this prints.
Given
  • range(4) hands out 0, 1, 2 and 3 in that order.

  • The inner range is range(i), using the value of i at that moment.

  • The last line of the outer body adds 10 to i.

IPython console
Hint 1/4

Two separate things to settle: how many times the inner line ran, and what i holds after the loop has finished.

Hint 2/4

A for over a range binds its variable afresh at the start of every pass, so an assignment to that variable inside the body is thrown away when the next pass begins.

Hint 3/4

Here i takes the values 0, 1, 2, 3 at the top of the four passes, so the inner ranges are range(0), range(1), range(2) and range(3). On the final pass i is 3 and then has 10 added.

Hint 4/4

It prints 6 and 13.

Show solution

Tabulate the value of i at the top of each pass and at the bottom.

Find i at the top of each pass

$$0, 1, 2, 3$$

the range was fixed before the loop began, so the assignment in the body cannot change these

$$\text{inner ranges: } 0, 1, 2, 3 \text{ passes}$$

each inner range is built from the value of i at that moment

Add the inner counts

$$0 + 1 + 2 + 3 = 6$$

the triangular sum for n equal to 4, which is also 4 times 3 over 2

Find i after the loop

$$\text{last pass: } i = 3 \to 13$$

the plus 10 runs on the last pass as on every other

$$\text{no further rebinding}$$

the range is exhausted, so nothing overwrites the 13 and that is what the print sees

Answer $$\boxed{6\quad 13}$$
Check

The inner line runs once for each pair of positions with j strictly below i, and among four positions there are 6 such pairs.

Assigning to a for variable inside the body changes nothing about the loop and everything about the value left behind afterwards.

3§09.1 — a step log kept in a default parameter

A function that records the name of each measured piece of work in a log, so the log can be printed at the end. The log is a parameter with a default value.

def record(name, log=[]):
    """Assumes name is a str. Adds name to log and returns log."""
    log.append(name)
    return log


print(record('scan'))
print(record('sort'))
print(record('scan', []))
print(record('merge'))
Find(a) Write the four lines this prints.
Given
  • The default value of log is written as an empty list in the definition.

  • append changes the list it is called on.

  • The third call passes a list of its own.

IPython console
Hint 1/4

Ask how many list objects exist in this program, and which call sees which one. That is the whole question.

Hint 2/4

A default value is built once, when the definition is executed, not once per call.

Hint 3/4

Here the first, second and fourth calls supply no list, so all three share the default. The third call passes its own empty list.

Hint 4/4

The four lines are ['scan'], ['scan', 'sort'], ['scan'] and ['scan', 'sort', 'merge'].

Show solution

Count the list objects before tracing any calls.

Count the lists

$$\text{one default list, built at } \texttt{def}$$

the default expression is evaluated when the definition runs, and the resulting object is kept with the function

$$\text{one more, passed by call 3}$$

so two list objects exist in the whole program

Route each call to its list

$$\text{calls 1, 2, 4} \to \text{the default}$$

no argument supplied, so the shared object is used and grows

$$\text{call 3} \to \text{its own}$$

an argument was supplied, so the default is untouched by this call

Read the four lines off

$$[\texttt{scan}],\ [\texttt{scan}, \texttt{sort}]$$

the shared list after the first two appends

$$[\texttt{scan}],\ [\texttt{scan}, \texttt{sort}, \texttt{merge}]$$

the fresh list, then the shared list again with a third item

Answer $$\boxed{[\texttt{scan}];\ [\texttt{scan}, \texttt{sort}];\ [\texttt{scan}];\ [\texttt{scan}, \texttt{sort}, \texttt{merge}]}$$
Check

The shared list receives exactly three appends across the program, so its final length must be 3, and the fourth line has three items.

A mutable default is built once and shared for the life of the program.

4§09.6 — removing items from the list being walked

A loop that walks a list and removes the even items as it goes, keeping a count of the passes it made. Both the list and the count are printed at the end.

L = [1, 2, 3, 4]
passes = 0
for x in L:
    if x % 2 == 0:
        L.remove(x)
    passes = passes + 1
print(L, passes)
Find(a) Write the line this prints.
Given
  • L starts as [1, 2, 3, 4].

  • L.remove(x) takes the first entry equal to x out and shifts everything after it down one place.

  • A for over a list walks it by position, and asks for the next position each time round.

IPython console
Hint 1/4

The loop does not hold a copy of the list.

Hint 2/4

A for over a list asks for position 0, then 1, then 2, and stops when the position it wants is past the end of the list as it is at that moment.

Hint 3/4

Here the list starts [1, 2, 3, 4]. Position 0 gives 1, position 1 gives 2 which is removed, and now the list is [1, 3, 4], so position 2 gives 4 rather than 3.

Hint 4/4

It prints [1, 3] 3.

Show solution

Keep two columns: the position the loop is about to visit, and the list as it stands.

Pass 1

$$\text{position } 0,\ L = [1, 2, 3, 4]$$

the loop asks for position 0 and gets 1, which is odd, so nothing is removed

Pass 2

$$\text{position } 1 \Rightarrow 2$$

even, so it is removed

$$L = [1, 3, 4]$$

the 3 and the 4 have each slid down one place, and the loop has no idea

Pass 3

$$\text{position } 2 \Rightarrow 4$$

the 3 now sits at position 1, which the loop has already passed, so it is skipped

$$L = [1, 3]$$

4 is even and is removed

Pass 4 does not happen

$$\text{position } 3 > \texttt{len(L)} - 1$$

the list now has two items, so the loop stops

$$\texttt{passes} = 3$$

three bodies ran, and the counter was raised once in each

Answer $$\boxed{[1, 3]\quad 3}$$
Check

Two items were removed from a list of four, so the final length must be 2, and [1, 3] has two items.

Never remove from the list you are walking.

5§09.6 — three pass counts at two sizes

One function, three separate pieces of work, and a printed tuple of the three pass counts. It is called at two sizes so that the growth of each piece is visible.

def count_calls(n):
    """Assumes n is an int >= 1. Returns the pass counts of three loops."""
    a = 0
    for i in range(n):
        for j in range(4):
            a = a + 1
    b = 0
    for i in range(n):
        for j in range(n):
            b = b + 1
    c = 0
    m = n
    while m > 1:
        m = m // 2
        c = c + 1
    return a, b, c


print(count_calls(4))
print(count_calls(16))
Find(a) Write the two lines this prints.
Given
  • The first piece has an inner range of range(4), a fixed number.

  • The second piece has an inner range of range(n).

  • The third piece halves its counter.

  • The two sizes are 4 and 16.

IPython console
Hint 1/4

Six numbers wanted. Price each of the three pieces as a formula in n first, then substitute the two sizes.

Hint 2/4

A nested pair multiplies its two pass counts. When the inner range is a fixed number, one of the factors does not grow.

Hint 3/4

Here the three formulas are 4n, n times n, and the halvings of n. The sizes are 4 and 16, and 4 is 2 squared while 16 is 2 to the fourth.

Hint 4/4

The lines are (16, 16, 2) and (64, 256, 4).

Show solution

Get the three formulas first and substitute second.

Price the three pieces

$$a = n \times 4 = 4n$$

nested, so multiply, and the inner count is a fixed number so it becomes a constant factor

$$b = n \times n = n^{2}$$

nested, and this time the inner count grows with n

$$c = \lfloor \log_2 n \rfloor$$

the halving shape, driven by the value of n

Substitute n equal to 4

$$4 \cdot 4 = 16,\quad 4^{2} = 16$$

the two pieces happen to tie at this size, which is the trap the question is built around

$$4 \to 2 \to 1:\ c = 2$$

two halvings, since 4 is 2 squared

Substitute n equal to 16

$$4 \cdot 16 = 64,\quad 16^{2} = 256$$

now the two are four times apart, and the gap grows with every further increase

$$16 \to 8 \to 4 \to 2 \to 1:\ c = 4$$

four halvings, since 16 is 2 to the fourth

Answer $$\boxed{(16, 16, 2)\quad\text{and}\quad (64, 256, 4)}$$
Check

The size rose by a factor of 4: the linear count rose by 4, the quadratic one by 16, and the halving count gained 2.

Two counts that agree at one size tell you nothing.

C · exam level 4 questions
1§09.5 — ordering four functions at a large input

Four functions have been counted exactly. The counts are given below, and the question is which one costs most when the input is large.

Function P costs 100n steps. Function Q costs n^2 / 4 steps. Function R costs 50 n log n steps. Function S costs 2^n / 1000 steps.

Find(a) Which ordering from cheapest to dearest is right for large n?
Given
  • P costs 100n.

  • Q costs n^2 / 4.

  • R costs 50 n log n, with the logarithm in base two.

  • S costs 2^n / 1000.

  • n is the length of the same list in all four.

Hint 1/4

The constants are all chosen to mislead, so do not compare them. Strip each count to its class first and order the classes.

Hint 2/4

The order of the classes, cheapest first, is constant, then log n, then n, then n log n, then any power of n, then any constant raised to n.

Hint 3/4

Here P is linear, Q is quadratic, R is log linear, and S is exponential, whatever the 100, the quarter, the 50 and the thousand suggest.

Hint 4/4

The order is P, then R, then Q, then S.

Show solution

Classify first and compare second.

Strip each count to its class

$$100n \to O(n)$$

one term, constant factor dropped

$$\tfrac{n^{2}}{4} \to O(n^{2})$$

dividing by 4 is multiplying by a quarter, which is a constant factor

$$50 n \log n \to O(n \log n)$$

same again

$$\tfrac{2^{n}}{1000} \to O(2^{n})$$

the thousand is a constant factor; the n is in the exponent and that is what matters

Order the classes

$$O(n) \subset O(n \log n) \subset O(n^{2}) \subset O(2^{n})$$

the standard order, and every inclusion here is strict

$$P, R, Q, S$$

reading the four functions off that chain

Check where the constants stop mattering

$$n = 10:\ 1000,\ 25,\ 1660,\ 1$$

at this size the order is nearly reversed, which is what the constants buy

$$n = 1000:\ 10^{5},\ 2.5 \times 10^{5},\ 5 \times 10^{5}$$

P is settled into first place but Q and R are still the wrong way round, so 1000 is not yet large enough

$$n = 10^{4}:\ 10^{6},\ 2.5 \times 10^{7},\ 6.6 \times 10^{6}$$

and by here every pair has crossed, so the class order has taken over completely

Answer $$\boxed{P,\ R,\ Q,\ S}$$
Check

Divide both by n: 50 log n against n over 4 is 498 against 250 at n equal to 1000, and 664 against 2500 at n equal to 10000.

Classify, then order. And when a question says large n, it is telling you the constants are there to be ignored.

2§09.6 — a class with a hidden walk in one of its methods

A class that keeps a shelf of titles and refuses duplicates. The specification is the kind a lab sheet gives, and the question is about the cost of filling the shelf rather than about whether the code works.

class Shelf(object):
    """A shelf that keeps a list of titles, with no duplicates."""

    def __init__(self, name):
        """Assumes name is a str."""
        self.__name = name
        self.__titles = []

    def add(self, title):
        """Adds title if it is not already on the shelf.
        Returns whether it was added.
        """
        if title in self.__titles:
            return False
        self.__titles.append(title)
        return True

    def size(self):
        """Returns how many titles the shelf holds."""
        return len(self.__titles)


s = Shelf('fiction')
print(s.add('Ada'), s.add('Zeno'), s.add('Ada'))
print(s.size())
Find
  1. (a) Write the two lines the script prints.

  2. (b) Give the worst case class of add on a shelf that already holds n titles, and of size.

  3. (c) Give the worst case class of a loop that calls add n times on a shelf that starts empty, and name the change to the class that would make it linear.

Given
  • self.__titles starts empty and grows by at most one per successful add.

  • title in self.__titles walks the list from the front.

  • len on a list does not walk it.

  • The script makes three add calls, one of them a duplicate.

IPython console
Hint 1/4

Three separate jobs. Part (a) is a trace, part (b) prices two methods, and part (c) puts part (b) inside a loop.

Hint 2/4

Price every call in a method body. Membership on a list is O(n) and len is O(1).

Hint 3/4

Here the shelf starts empty and the script adds Ada, then Zeno, then Ada again, which is already there. On the k th call the list holds at most k minus one titles.

Hint 4/4

It prints True True False and 2; add is O(n) and size is O(1); n calls cost O(n squared), and keeping the titles in a dictionary makes it linear.

Show solution

Price the methods before pricing the loop.

Part (a): trace the three calls

$$\texttt{add('Ada')} \to \texttt{True}$$

the list is empty, so the membership test fails and the title is appended

$$\texttt{add('Zeno')} \to \texttt{True}$$

one comparison, no match, appended

$$\texttt{add('Ada')} \to \texttt{False}$$

the first comparison matches, so the method returns before appending

$$\texttt{size()} = 2$$

two successful appends out of three calls

Part (b): price the two methods

$$\texttt{add}: \text{up to } n \text{ comparisons}$$

the worst case is a title that is not present, which makes the test walk the whole list

$$\texttt{add} = O(n),\quad \texttt{size} = O(1)$$

append and len are both constant, so the membership test is the only term that grows

Part (c): put the method in a loop and sum

$$\text{call } k \text{ costs at most } k - 1$$

the shelf holds at most one title per earlier call, so the cost rises as the loop proceeds

$$\sum_{k=1}^{n} (k-1) = \tfrac{n(n-1)}{2} = O(n^{2})$$

a sum because the per call cost changes, not a product

$$\text{dictionary} \Rightarrow O(1) \text{ per call} \Rightarrow O(n)$$

membership on a dictionary does not grow with the number of keys, so the n calls become n constant amounts

Answer $$\boxed{\texttt{True True False};\ 2;\ O(n),\ O(1);\ O(n^{2}) \to O(n)}$$
Check

Drop the membership test entirely and n calls to add cost n constant appends, which is O(n).

A method whose cost grows with the object's own size turns a loop of n calls into a sum.

3§09.7 — a program that reports what four loop shapes cost

A lab style exercise. You are to write a program, in a file called cost_report.py, that asks for one list size and reports what four shapes of loop would cost at that size.

Write four functions, each taking one int parameter n that is at least 1, each returning an int, and each with a docstring:

  • constant_cost(n) returns the number of passes of a piece of code with no loop in it, which is 1.
  • halving_cost(n) returns how many times n can be halved with // before it reaches 1.
  • linear_cost(n) returns the number of passes of one loop over n items, counted by the loop itself and not computed.
  • square_cost(n) returns the number of passes of two loops nested over n items, again counted by the loops.

Then a script that reads one size with input, prints the four costs with the labels shown, and finishes with one line saying how many times dearer the squared shape is than the linear one at that size, using whole number division.

Find(a) Write the file, then say what class each of the four functions is in.
Given
  • Only what the course has covered may be used: def, return, for, while, range, len, input, int and print.

  • The two loop counting functions must count with a loop, not return n or n * n directly.

  • The labels and spacing in the Sample Run are part of the specification.

Hint 1/4

Start from the four function headers and their docstrings.

Hint 2/4

A count of passes is kept in a variable that starts at 0 and is raised by one inside the loop.

Hint 3/4

Here the Sample Run shows a size of 64, and 64 is 2 to the sixth, so halving_cost must return 6, linear_cost 64 and square_cost 4096.

Hint 4/4

The four classes are O(1), O(log n), O(n) and O(n^2), and the last line of the run prints 64.

Show solution

Make the two loop counting functions actually count rather than return a formula.

Write the four headers with their docstrings

$$\texttt{def constant\_cost(n):}$$

no loop, so nothing to count and the body is one return

$$\texttt{def halving\_cost(n):}$$

a while with n = n // 2, since the specification asks for halvings and only division gives them

Count with a loop, not with arithmetic

$$\texttt{for i in range(n): passes = passes + 1}$$

the specification says counted by the loop, and this is what makes the reported number a measurement

$$\text{nested version: the same, one level deeper}$$

so that the 4096 in the run is produced by 4096 passes rather than by a multiplication

Write the script and the closing line

$$\texttt{size = int(input('List size: '))}$$

input hands back a string, so it has to be converted before the loops can use it

$$\texttt{square\_cost(size) // linear\_cost(size)}$$

whole number division, as the specification asks, and the result is n because the two counts are n squared and n

Name the four classes

$$O(1),\ O(\log n),\ O(n),\ O(n^{2})$$

read off the four shapes: no dependence, a divided counter, one pass per item, and nested passes

Answer $$\boxed{O(1),\ O(\log n),\ O(n),\ O(n^{2})}$$
Check

At a size of 8 the four costs come out 1, 3, 8 and 64, and the closing line prints 8.

Writing the counters as loops rather than as formulas is worth the extra lines: the program then measures the thing the section claims, and you can check the claim by running it.

4§09.7 — a claimed improvement that made things worse

A student has been told that dictionaries make lookups cheap, and has rewritten a function accordingly. The new version is slower than the old one on every input, and it returns the same answers, so the tests all pass.

def first_shared(first, second):
    """Assumes first and second are lists of ints.
    Returns the first item of first that also appears in second,
    or -1 if there is none.
    """
    for item in first:
        table = {}
        for other in second:
            table[other] = True
        if item in table:
            return item
    return -1


print(first_shared([4, 7, 9], [9, 7]))
print(first_shared([1, 2], [8]))
Find
  1. (a) Write the two lines the script prints.

  2. (b) Give the worst case class of this version and of the old one, and say which line is in the wrong place.

Given
  • first holds n items and second holds m items.

  • Building the dictionary costs one pass over second.

  • Membership on a dictionary does not grow with the number of keys.

  • The old version tested item in second directly on the list.

Hint 1/4

Part (a) is a trace and part (b) is a pricing.

Hint 2/4

A cost that does not depend on the loop variable should be outside the loop.

Hint 3/4

Here the build is inside the loop over first, so it happens n times, and each build costs m. The old version's test cost m per pass as well.

Hint 4/4

It prints 7 then -1; both versions are O(nm), and the two lines that build the dictionary belong above the loop over first.

Show solution

Ask of every line inside a loop whether it depends on the loop variable.

Part (a): trace the two calls

$$\texttt{first} = [4, 7, 9],\ \texttt{second} = [9, 7]$$

the table is built as {9: True, 7: True} on the first pass, and 4 is not a key

$$\text{second pass: } 7 \in \texttt{table}$$

so the function returns 7 and never reaches 9

$$[1, 2] \text{ against } [8]$$

neither item is a key of {8: True}, so the loop runs out and -1 is returned

Part (b): price the new version

$$n \text{ passes} \times m \text{ build steps}$$

the build is inside the loop over first, so it is paid once per item of first

$$= O(nm)$$

and the constant time test adds nothing to that

Part (b): price the old version and the fix

$$n \times m$$

n passes, each walking up to m list entries, so the same product

$$\text{build moved out: } m + n = O(n + m)$$

one build, then n constant tests, which is the design the student wanted

Answer $$\boxed{7,\ -1;\ O(nm) \text{ both};\ \text{move the build above the outer loop}}$$
Check

Count the dictionary writes on the first call: second has two items and the outer loop makes two passes before returning, so four writes happen for a table that only ever holds two entries.

A good data structure in the wrong place is not an improvement.

D · interleaved 4 questions
1§09.6 — totals over a grid of rows

A function that adds up each row of a rectangular grid of numbers, counting the individual numbers it read on the way.

def row_totals(grid):
    """Assumes grid is a list of lists of ints.
    Returns a list with the total of each row, and the number of
    items read.
    """
    totals = []
    read = 0
    for row in grid:
        total = 0
        for value in row:
            read = read + 1
            total = total + value
        totals.append(total)
    return totals, read


grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
print(row_totals(grid))
Find
  1. (a) Write the line this prints.

  2. (b) Give the class in terms of r rows and c columns, and say whether it is quadratic.

Given
  • grid has 4 rows of 3 numbers each.

  • totals collects one number per row.

  • read is raised once per individual number visited.

IPython console
Hint 1/4

Two jobs: the four row totals and the read count for part (a), then a class for part (b).

Hint 2/4

Nested loops multiply their pass counts. When the two loops run over different things, the product carries two different letters.

Hint 3/4

Here the grid has 4 rows of 3, so the inner line runs 4 times 3 times, and the rows add up to 6, 15, 24 and 33.

Hint 4/4

It prints ([6, 15, 24, 33], 12), and the class is O(rc), which is not quadratic unless the grid is square.

Show solution

Count the cells rather than the passes of either loop.

Part (a): add the rows

$$1+2+3 = 6,\ 4+5+6 = 15$$

the inner loop resets total at the top of each row, so the rows do not accumulate into each other

$$7+8+9 = 24,\ 10+11+12 = 33$$

four rows, so four totals in the returned list

$$\texttt{read} = 4 \times 3 = 12$$

one read per cell, and the grid has twelve cells

Part (b): price the nesting with two letters

$$r \text{ outer passes} \times c \text{ inner passes}$$

the outer loop runs once per row and the inner once per number in that row

$$O(rc)$$

a product of two independent sizes, which is why one letter would not do

$$rc = n \Rightarrow O(n)$$

measured in cells rather than rows, the function is linear, and nothing about it is quadratic unless r equals c

Answer $$\boxed{([6, 15, 24, 33],\ 12);\ O(rc),\ \text{not quadratic in general}}$$
Check

The four totals add to 78, which is the sum of the numbers 1 to 12, so every cell was visited exactly once.

When two nested loops run over two different things, the class needs two letters, and the answer has to say what each one counts.

2§09.6 — building text one piece at a time

A function joining a list of words into one string, counting the characters it copies as it goes. The counter is the interesting part.

def joined(words):
    """Assumes words is a list of str.
    Returns the words joined with a space, and the number of
    characters that ended up copied.
    """
    out = ''
    copies = 0
    for w in words:
        out = out + w + ' '
        copies = copies + len(out)
    return out.strip(), copies


print(joined(['ab', 'cd', 'ef']))
Find(a) Write the line this prints.
Given
  • words is ['ab', 'cd', 'ef'], three words of two letters each.

  • A string cannot be changed in place, so out + w + ' ' builds a new string.

  • copies is raised by the length of the new string on each pass.

  • strip() removes the space at the end and returns a new string.

IPython console
Hint 1/4

Two answers in one tuple. The string is straightforward; the number is the sum of three lengths and is where the question is.

Hint 2/4

Each pass builds a whole new string containing everything so far plus one more word and a space, so the number of characters copied on a pass is the length of the result of that pass.

Hint 3/4

Here the three words are two letters each, so after the passes out is 'ab ', then 'ab cd ', then 'ab cd ef ', with lengths 3, 6 and 9.

Hint 4/4

It prints ('ab cd ef', 18).

Show solution

Tabulate out and its length at the end of each pass.

Build the table of passes

$$\texttt{'ab '},\ \text{length } 3$$

the empty string plus the word plus a space

$$\texttt{'ab cd '},\ \text{length } 6$$

the previous string was copied in full, then extended

$$\texttt{'ab cd ef '},\ \text{length } 9$$

copied in full again, which is the cost that is piling up

Add the copies and strip

$$3 + 6 + 9 = 18$$

the counter adds the length after each concatenation, so one row per pass

$$\texttt{'ab cd ef '.strip()} = \texttt{'ab cd ef'}$$

the trailing space goes, and this happens once, after the loop

Name the class in the number of words

$$L + 2L + \cdots + kL = L\tfrac{k(k+1)}{2}$$

with k words of length L, since pass i has copied i words and i spaces

$$O(k^{2})$$

the triangular sum again, so joining text this way is quadratic in the number of pieces

Answer $$\boxed{(\texttt{'ab cd ef'},\ 18)}$$
Check

Here k is 3 and each word contributes 3 characters to the result counting its space, so the total is 3 times 3 times 4 over 2, that is 18.

Anything of the form x = x + something inside a loop, where x is a string or a list, copies x on every pass.

3§09.7 — answering many questions about one list of words

A lab style exercise that puts the space against time trade to work. Write a program in a file called word_index.py that answers repeated position questions about one fixed list of words.

Write two functions with docstrings:

  • build_index(words) takes a list of strings and returns a dictionary from each word to its first position in the list. A word that appears twice keeps its first position.
  • position_by_scan(words, wanted) takes the list and one word, and returns two things: the first position of wanted or -1 if it is absent, and the number of words it read on the way.

Then a script that builds the index once, asks for words in a loop until the user types end, prints the position of each or says it is not in the list, and at the end prints two lines: how many words a scanning version would have read in total, and how many the index version read.

Find(a) Write the file, then say what the two designs cost for k questions over n words.
Given
  • The fixed list is ['ada', 'boran', 'ceren', 'ada', 'deniz', 'eren'], six words with one repeat.

  • Only what the course has covered may be used.

  • build_index must be called once, before the loop over the user's words.

  • The scanning total is the sum of the read counts, which is why position_by_scan returns two things.

Hint 1/4

Two functions and a script. Decide first where build_index is called, because that single decision is what the exercise is testing.

Hint 2/4

A cost that does not depend on the user's word belongs outside the loop over the user's words.

Hint 3/4

Here the list is ['ada', 'boran', 'ceren', 'ada', 'deniz', 'eren']. The repeat means build_index has to keep the first position of ada, which is 0, and ignore the later one.

Hint 4/4

Scanning k words costs up to kn; the index costs n once plus about k, that is O(n + k).

Show solution

Put the build above the loop and keep the scan inside it.

Write build_index so the repeat is handled

$$\texttt{if words[i] not in index:}$$

without this guard the later ada would overwrite the earlier one and the dictionary would report position 3

$$\texttt{index[words[i]] = i}$$

so each word is stored against the first position it was seen at

Write position_by_scan so it reports its own cost

$$\texttt{read = read + 1} \text{ before the test}$$

a position that is read and rejected still cost a read, so the counter has to rise first

$$\texttt{return i, read}$$

returning both means the script can total the reads without knowing how the scan works

Place the build once, outside the loop

$$\texttt{index = build\_index(words)} \text{ before the loop}$$

the index does not depend on the user's word, so paying for it once is the whole point of the exercise

$$\text{inside the loop} \Rightarrow kn \text{ instead of } n$$

which is the mistake the error hunt in this set is built on

Give the two cost formulas

$$\text{scan: } O(kn)$$

k questions, each walking up to n words

$$\text{index: } O(n + k) \text{ time},\ O(n) \text{ space}$$

one build and k constant lookups, and the memory is part of the price

Answer $$\boxed{\text{scan } O(kn),\ \text{index } O(n+k)}$$
Check

With n equal to 6 and k equal to 3, the scan bound is 18 and the run reported 13, which is under the bound as it must be.

The exercise is one decision: where the build goes.

4§09.6 — the cost of a method call on an object in a loop

A class keeps a list of marks and offers a method that reports the highest one. A script then asks the same question inside a loop over the marks.

class Marks(object):
    """A set of marks for one course."""

    def __init__(self, values):
        """Assumes values is a list of ints."""
        self.__values = values

    def highest(self):
        """Returns the largest mark."""
        top = self.__values[0]
        for v in self.__values:
            if v > top:
                top = v
        return top

    def count(self):
        """Returns how many marks there are."""
        return len(self.__values)


def gaps(m):
    """Assumes m is a Marks object. Returns the gaps to the top."""
    out = []
    for i in range(m.count()):
        out.append(m.highest() - i)
    return out
Find(a) What is the class of gaps on an object holding n marks?
Given
  • The object holds n marks.

  • highest walks the whole list every time it is called.

  • count uses len, which does not walk.

  • gaps calls highest inside its loop.

Hint 1/4

There is one loop in gaps, so the question is not about indentation. Price one pass of its body.

Hint 2/4

Every call in a loop body has to be priced.

Hint 3/4

Here gaps makes n passes, because count returns n, and each pass calls highest, which walks all n marks.

Hint 4/4

The class is O(n^2).

Show solution

Price each of the two method calls in the body separately.

Price the two calls

$$\texttt{m.count()} = O(1)$$

its body is len on a list, which reads a stored number

$$\texttt{m.highest()} = O(n)$$

its body is a loop over all the marks, with no early exit

Count the passes and multiply

$$n \text{ passes of } \texttt{gaps}$$

the range is built from count, which is n, and it is evaluated once

$$n \times O(n) = O(n^{2})$$

the walk inside highest restarts on every pass, which is nesting even though the inner loop is in another file's worth of code

Name the fix

$$\texttt{top = m.highest()} \text{ above the loop}$$

the marks do not change during gaps, so the answer cannot change either

$$O(n) + O(n) = O(n)$$

one walk to find the top, then n constant passes

Answer $$\boxed{O(n^{2}),\ \text{fixable to } O(n)}$$
Check

With 4 marks, highest makes 4 comparisons per call and gaps calls it 4 times, so 16 comparisons. With 8 marks it is 64.

A method call is not one step.

Mistake ledger (23 entries)
⚠ Counting lines of code rather than steps executed

The lines are on the page and the passes are not.

wrong$$\text{steps} = \text{number of lines} = 5$$
right$$\text{steps}(n) = 5n + 2$$
⚠ Giving the count as a number instead of a formula

A run produces one number, and writing it down feels like an answer.

wrong$$\text{steps} = 5002$$
right$$\text{steps}(n) = 5n + 2,\quad \text{so } 5002 \text{ at } n = 1000$$
⚠ Forgetting to say what n counts

In most examples n is a list length, so it stops getting mentioned.

wrong$$\text{steps} = 5n + 2$$
right$$\text{steps} = 5n + 2,\ n = \text{the value of the argument}$$
⚠ Leaving the constant factor inside the O

The first rule feels like the hard one, so once the term is chosen the job feels done.

wrong$$4n^{2} + 1000n \Rightarrow O(4n^{2})$$
right$$4n^{2} + 1000n \Rightarrow O(n^{2})$$
⚠ Choosing the term that is biggest at the n in front of you

With 50000 in the count and n equal to 10 on the page, the constant really is the biggest term, and picking it feels like reading the situation.

wrong$$n = 10 \Rightarrow 50000 \text{ dominates} \Rightarrow O(1)$$
right$$n \to \infty \Rightarrow 4n^{2} \text{ dominates} \Rightarrow O(n^{2})$$
⚠ Keeping a sum of terms inside the O

Dropping a whole term feels like throwing away work you did, so it gets kept as a tidy sum instead.

wrong$$O(n^{2} + n + 1)$$
right$$O(n^{2})$$
⚠ Reading the O as equality or as approximation

It is written with an equals sign, and in every other use of that sign the two sides are the same size.

wrong$$1000 + x + 2x^{2} \approx x^{2}$$
right$$1000 + x + 2x^{2} \le 3x^{2} \ \text{for } x \ge 33$$
⚠ Reporting a loose bound because it is technically true

The definition really is one sided, so a bigger class is never false, and false is the thing a student is watching out for.

wrong$$\text{linear scan} = O(2^{n})$$
right$$\text{linear scan} = O(n)$$
⚠ Letting the constants depend on n

The definition hands you two free numbers, so it feels as though you may keep choosing them as n changes.

wrong$$f(n) \le n \cdot g(n) \Rightarrow f = O(g)$$
right$$f(n) \le c\,g(n),\ c \text{ fixed before } n \text{ varies}$$
⚠ Forgetting the input that is not in the list at all

Every example is written with the wanted item somewhere in the list, so the worst position gets found and the absent case never gets considered.

wrong$$\text{worst} = n - 1 \ \text{(the last position)}$$
right$$\text{worst} = n \ \text{(absent, or last)}$$
⚠ Reporting the best case as the complexity

The best case is the one the first test run happened to hit, and it is a real count of a real run.

wrong$$\text{scan} = O(1) \ \text{because it can stop at the first item}$$
right$$\text{scan} = O(n) \ \text{worst case, } O(1) \text{ best case}$$
⚠ Averaging without saying over what

The formula n plus one over two is memorable and gets written down without the sentence that makes it true.

wrong$$\text{average} = \tfrac{n+1}{2} \ \text{always}$$
right$$\text{average} = \tfrac{n+1}{2} \ \text{if present and uniformly placed}$$
⚠ Calling any while loop with a changing counter logarithmic

The halving example is the first while loop the section counts, so the shape and the class get learned together.

wrong$$\texttt{n = n - 2} \Rightarrow O(\log n)$$
right$$\texttt{n = n - 2} \Rightarrow O(n),\quad \texttt{n = n // 2} \Rightarrow O(\log n)$$
⚠ Mixing up n to a power with a power raised to n

Both are written with a superscript, and the phrase used in speech, n squared and two to the n, sounds similarly shaped.

wrong$$O(n^{2}) \text{ and } O(2^{n}) \text{ are similar}$$
right$$n = 20:\ n^{2} = 400,\quad 2^{n} = 1\,048\,576$$
⚠ Assuming constant means loop free

Constant time sounds like no work, and a loop looks like work, so a loop in the body rules the class out.

wrong$$\texttt{for i in range(1000)} \Rightarrow O(n)$$
right$$\texttt{for i in range(1000)} \Rightarrow O(1)$$
⚠ Adding nested loops instead of multiplying them

Reading down the page, each loop looks like another item on a list of things the function does, and items on a list get added.

wrong$$\text{nested } n \text{ and } n \Rightarrow 2n$$
right$$\text{nested } n \text{ and } n \Rightarrow n^{2}$$
⚠ Pricing a list method as one step

On the page item in L is one short expression, and nothing in it looks like a loop.

wrong$$\texttt{for x in A: if x in B} \Rightarrow O(n)$$
right$$\texttt{for x in A: if x in B} \Rightarrow O(n^{2})$$
⚠ Expecting a triangular nested loop to be cheaper by a class

It genuinely does less than half the work, and half feels like the sort of saving that ought to show up in the answer.

wrong$$\tfrac{n(n-1)}{2} \Rightarrow O(n)$$
right$$\tfrac{n(n-1)}{2} \Rightarrow O(n^{2})$$
⚠ Building the lookup structure inside the loop that uses it

The build and the use belong to the same idea, so they get written together, and the loop that needs the use ends up containing both.

wrong$$k \cdot (n + 1) = O(kn)$$
right$$n + k = O(n + k)$$
⚠ Comparing the per use costs and ignoring the build

The per use classes are the two numbers the section taught you to produce, so comparing them feels like the method being applied.

wrong$$O(1) < O(n) \Rightarrow \text{always index}$$
right$$n + k \ \text{against} \ kn:\ \text{index wins for } k \ge 2$$
⚠ Growing a list with the plus operator inside a loop

out = out + [x] reads like out.append(x) and both lines end with one item more in the list.

wrong$$\texttt{out = out + [x]} \Rightarrow O(n)$$
right$$\texttt{out = out + [x]} \Rightarrow O(n^{2}),\quad \texttt{out.append(x)} \Rightarrow O(n)$$
⚠ Reporting a class without saying what n counts

The letter n is in every example, so it stops feeling like something that has to be defined.

wrong$$O(n^{2})$$
right$$O(n^{2}),\ n = \texttt{len(L)}$$
⚠ Turning a sum of differing pass costs into a product

Multiplying is the rule for nesting and it gets applied even when the inner cost changes from pass to pass.

wrong$$n \text{ passes} \times n = n^{2}$$
right$$0 + 1 + \cdots + (n-1) = \tfrac{n(n-1)}{2}$$
Formula card
Method 9.1: counting basic steps
$$\boxed{\ \text{steps}(n) = \underbrace{a}_{\text{outside the loop}} + \underbrace{b \cdot n}_{b \text{ steps on each of } n \text{ passes}}\ }$$

Say which operations you are counting as one step. Different price lists differ by a constant factor and give the same class.

Rule 9.2: simplifying a count to a class
$$\boxed{\ 1000 + x + 2x^{2}\ \xrightarrow{\text{keep the fastest}}\ 2x^{2}\ \xrightarrow{\text{drop the factor}}\ O(x^{2})\ }$$

Fastest growing means for all large enough n, not biggest at the n in front of you. Apply the rules in the order given.

Definition 9.3: f is O of g
$$\boxed{\ f(n) = O(g(n))\ \text{means: there are } c > 0 \text{ and } n_0 \text{ with } f(n) \le c\,g(n)\ \text{for every } n \ge n_0\ }$$

The multiplier and the threshold are fixed once, before n varies. Nothing is claimed below the threshold.

Definition 9.4: the three cases
$$\boxed{\ \text{best}(n) = \min_{|I| = n} \text{steps}(I),\quad \text{worst}(n) = \max_{|I| = n} \text{steps}(I)\ }$$

All three are taken over inputs of one fixed size. The average case also needs a stated assumption about how likely each input is.

Rule 9.5: the six classes and their loop shapes
$$\boxed{\ O(1) \subset O(\log n) \subset O(n) \subset O(n \log n) \subset O(n^{k}) \subset O(c^{n})\ }$$

Every class in the chain is contained in the next, so a looser answer is true and still wrong. Report the tightest you can justify.

Method 9.6: reading a class off a function
$$\boxed{\ \text{total} = \sum_{\text{pieces in a row}} \big(\text{passes} \times \text{cost of one pass}\big)\ }$$

Do not simplify a piece before adding the pieces together, or the term that was going to dominate can go missing.

Rule 9.7: when a one off cost pays for itself
$$\boxed{\ \text{total}(k) = \underbrace{B}_{\text{build once}} + k \cdot \underbrace{U}_{\text{each use}}\ }$$

Fix the number of uses first. The memory the structure costs is part of the price and does not appear in the time comparison.

The six classes, cheapest first
$$O(1),\ O(\log n),\ O(n),\ O(n \log n),\ O(n^{k}),\ O(c^{n})$$

n raised to a constant is polynomial; a constant raised to n is exponential. They sit at opposite ends of the chain.

The triangular sum
$$0 + 1 + 2 + \cdots + (n-1) = \frac{n(n-1)}{2} = O(n^{2})$$

Use it when the inner pass count depends on the outer counter, or when a loop body walks a structure that is growing inside the loop.

Halving count against powers of two
$$2^{k} \le n < 2^{k+1} \iff \lfloor \log_2 n \rfloor = k,\quad 2^{10} \approx 10^{3},\ 2^{20} \approx 10^{6}$$

Base two, since that is what a halving loop does. Inside a big O the base is dropped, because changing it only multiplies by a constant.

Cost of the operations the course already uses
$$O(1):\ \texttt{len},\ \texttt{L[i]},\ \texttt{append},\ \texttt{d[k]}\qquad O(n):\ \texttt{in L},\ \texttt{index},\ \texttt{remove},\ \texttt{insert(0,x)}$$

The O of n ones walk or shift the whole structure. Put any of them inside a loop over the same structure and the function is quadratic with one loop written down.

Check yourself

Close the page and write, from memory: the two simplification rules and the order they go in; the six classes in order from cheapest to dearest; the loop shape that produces each of the six; what the big O statement promises and the two numbers it lets you choose; the difference between nesting and sequence; and four list operations that cost O of n rather than O of 1.

  • Take a loop, price its body in steps, count its passes, and write the count as a formula in n rather than as a number?

    c-step-count

  • Turn 4n squared plus 1000n plus 50000 into a class, and say why the constant does not decide it even though it is the largest term at n equal to 10?

    c-drop-terms

  • Name a multiplier and a threshold that witness a claimed bound, and say why an objection at small n is not a counterexample?

    c-big-o

  • Give the best, worst and average case of a scan that can stop early, and say which input sets the worst case?

    c-cases

  • Look at a while loop and say from the assignment to its counter whether the pass count is linear or logarithmic?

    c-classes

  • Price a function with three pieces, one of them nested and one of them containing a membership test on a list, and get the class in under a minute?

    c-off-code

  • Write both totals for k searches over n items, solve for the crossing point, and say what the memory costs?

    c-space-amortize

Glossary (26 terms)
algoritmik karmaşıklık

How the number of basic steps a program takes grows as the size of its input grows, reported as a class rather than as a count of seconds.

basic steptemel işlem

One operation taken to cost a fixed amount: binding a name, one comparison, one arithmetic operation, or reaching one list item by index.

input sizegirdi boyutu

The one number a complexity is measured against, written n.

big O notation

A way of writing an upper bound on growth.

The general name for notation that describes behaviour as the input grows without limit.

upper boundüst sınır

A ceiling that a quantity is promised not to pass.

The smallest class you can justify for a given count.

additive constant

A term in a count that does not depend on the input size, such as the 2 in 5n plus 2.

constant factorsabit çarpan

A fixed multiplier in front of a term, such as the 5 in 5n.

baskın terim

The term of a sum that grows fastest, and therefore the only one that survives the first simplification rule.

constant timesabit zaman

A cost that does not grow with the input at all, written O of 1.

A cost that grows like the number of times the input can be halved, written O of log n.

doğrusal zaman

A cost proportional to the input size, written O of n.

A cost of the shape n times log n, produced by doing a logarithmic job once for each item of the input.

A cost of the shape n raised to a fixed power.

A cost that grows like the square of the input size.

A cost of the shape c raised to n, where the input size sits in the exponent.

worst caseen kötü durum

The largest cost over all inputs of a given size.

best caseen iyi durum

The smallest cost over all inputs of a given size.

average caseortalama durum

The mean cost over all inputs of a given size, under a stated assumption about how likely each input is.

Spreading a one off cost over the many later operations it makes cheaper, so that the per operation figure counts the build once rather than every time.

The same kind of measure applied to memory rather than to steps.

A structure built once so that later questions can be answered directly instead of by searching.

hidden cost

Work done by a call that looks like a single step.

triangular sum

The total 0 plus 1 up to n minus 1, which equals n times n minus 1 over 2.

kaba kuvvet

An approach that tries every possibility rather than using structure in the data.

What comes next
§10 · Simple Algorithms and Data Structures. Search and Sort Algorithms (Chapter 10)

The next section spends its whole length on five algorithms whose only interesting property is the class you now know how to read: a scan that is linear, a divide and conquer search that is logarithmic, two sorts that are quadratic, and one sort that is log linear.

Sources
  • kitapJohn Guttag, Introduction to Computation and Programming Using Python, with Application to Understanding Data, Second Edition, Chapter 9 The chapter the syllabus line names.
  • ders malzemesiCourse lecture slides on complexity, searching and sorting The step count of the factorial loop, the three cases, the function whose count is 1000 plus x plus 2 x squared with its two sample runs, the list of six classes, and the idea of spreading a one off cost over many uses all follow the order these slides use.
  • ders malzemesiCourse information sheet and weekly outline The weighting used on this page comes from here: labs 20 per cent, midterm 40, final 40, with the lowest of ten labs dropped and no makeup lab in the term recorded.
  • ders malzemesiPast papers with solutions The tracing question that asks for the exact printed output of a short program, and the reference list of functions printed on the cover sheet.
  • sabitPython language reference on list and dictionary operations Used for the cost table only: which operations touch a stored value and which walk or shift the whole structure.

Spotted something missing or wrong? tell us · share your own notes or an old exam.

Last updated .