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 = 0for x in L:
first = first + 1# line A
second = 0for i inrange(len(L)):
for j inrange(len(L)):
second = second + 1# line B
third = 0for i inrange(len(L)):
for j inrange(2):
third = third + 1# line Cprint(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
In [1]: %run untitled0.py
Loop A runs once per item, which is 5. Loop B multiplies five by five, which is 25. Loop C multiplies five by two, which is 10. Notice that B and C have exactly the same shape on the page and differ by a factor of two and a half at this size.
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 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.
defpair_count(L):
"""Assumes L is a list. Returns how many times the inner line runs."""
runs = 0for i inrange(len(L)):
for j inrange(len(L)):
runs = runs + 1return runs
deftriple_count(L):
"""Assumes L is a list. Returns how many times the inner line runs."""
runs = 0for i inrange(len(L)):
for j inrange(3):
runs = runs + 1return runs
for size in [4, 40, 400]:
L = list(range(size))
print(size, pair_count(L), triple_count(L))
Sample Run:
416124016001204001600001200
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.
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
Adding nested loops instead of multiplying them.
Writing the constant inside the name, as O(3n) or O(n/2).
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
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.
Simplify an exact step count to a complexity class by keeping the fastest growing term and dropping its , in that order.
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.
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.
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 .
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.
Trade memory for time by building a lookup structure once and say from how many uses onward that trade pays for itself.
Syllabus coverage
covered
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
covered
Chapter 9 — covered
The chapter the material is taken from.
deferred
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.
deferred
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.
defkeep_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 = 0while n > 1:
n = n // 2
passes = passes + 1print(n, passes)
Find(a) Write the line this prints.
Given
n starts at 40.
// divides and throws away the fraction.
IPython console
In [1]: %run untitled0.py
Forty halves down through 20, 10, 5, 2 and 1, which is five passes. Note the last step: 5 halves to 2 because the fraction is thrown away, and 2 halves to 1. A loop like this is the reason the logarithm turns up in this subject at all. Doubling the starting value adds one single pass, not twice as many, and you can see that here: 80 would take six.
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
symbol
reads as
means
watch 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.
The factorial loop with every line priced. The two lines outside the loop are paid once each.
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.
n
counted steps
5n
gap
gap 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.
deffact_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 = 1while n >= 1:
steps = steps + 1
answer = answer * n
steps = steps + 2
n = n - 1
steps = steps + 2
steps = steps + 1return 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 = 7and5n+2 = 7
n = 3 steps = 17and5n+2 = 17
n = 10 steps = 52and5n+2 = 52
n = 1000 steps = 5002and5n+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
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 = 1for i inrange(6):
steps = steps + 1
total = total + i
steps = steps + 2
total = total * 2
steps = steps + 2
steps = steps + 1print(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
In [1]: %run untitled0.py
The counter is the easy half: 5 steps on each of six passes is 30, plus 2 outside, which is 32. That is the formula 5n + 2 again with n equal to 6, and the loop body is not the same one as in the factorial, which is the point.
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.
The two parts of 1000 + x + 2 x squared, drawn against each other. The flat looking blue line is the constant and the linear part together, which barely moves at this scale.
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.
n
log n
n
n log n
n 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.
deff(x):
"""Assumes x is an int > 0. Returns the number of additions done."""
ans = 0for i inrange(1000):
ans = ans + 1print('after the first loop', ans)
for i inrange(x):
ans = ans + 1print('after the second loop', ans)
for i inrange(x):
for j inrange(x):
ans = ans + 1
ans = ans + 1print('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.
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.
The proof in the box, drawn. The blue curve is the counted steps, 1000 plus x plus 2 x squared. The dashed orange curve is three times x squared, the ceiling we chose.
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.
x
counted steps
3 x squared
does 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.
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.
One function, one list length, three costs. The shaded boxes are the positions the loop actually read before it stopped.
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.
list
position of the negative
reads
case
[-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.
deffirst_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 = 0for i inrange(len(L)):
looked = looked + 1if 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
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.
defin_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 = 0for i inrange(len(L)):
read = read + 1if L[i] == e:
returnTrue, read
if L[i] > e:
returnFalse, read
returnFalse, 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
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.
deflooked_at(L, target):
"""Assumes L is a list. Returns how many positions were read before the loop stopped. """
looked = 0for i inrange(len(L)):
looked = looked + 1if L[i] == target:
breakreturn 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
In [1]: %run untitled0.py
The first run finds the target immediately, so one read. The second finds it in the last position, so four reads. The third never finds it, so the loop runs out and that is also four reads.
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{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.
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.
Five of the six classes drawn on one pair of axes, with the same input size along the bottom.
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.
n
O(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.
defhalvings(n):
"""Assumes n is an int >= 1. Returns how many times n can be halved with // before it reaches 1. """
count = 0while n > 1:
n = n // 2
count = count + 1return count
for size in [1, 8, 1000, 1000000]:
print(size, 'needs', halvings(size), '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.
deflog_linear(L):
"""Assumes L is a list. Returns the number of inner passes."""
passes = 0for i inrange(len(L)):
m = len(L)
while m > 1:
m = m // 2
passes = passes + 1return passes
defexponential(n):
"""Assumes n is an int >= 0. Returns how many on and off patterns n switches have. """
rows = 0for code inrange(2 ** n):
rows = rows + 1return 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:
8241664102410240
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
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.
defby_subtraction(n):
"""Assumes n is an int >= 1. Returns the pass count."""
passes = 0while n > 1:
n = n - 2
passes = passes + 1return passes
defby_division(n):
"""Assumes n is an int >= 1. Returns the pass count."""
passes = 0while n > 1:
n = n // 2
passes = passes + 1return 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
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.
Why nesting multiplies. Each cell of the grid is one pass of the inner line, with the outer counter down the side and the inner counter along the top.
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.
operation
cost
why
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.
defreport(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 = 0for i inrange(len(L)):
for j inrange(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 + 1return 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
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.
defcount_inner(n):
"""Assumes n is an int >= 0. Returns how many times the inner line of the nested loop runs. """
total = 0for i inrange(n):
for j inrange(i):
total = total + 1return 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 is16
n = 10 inner line ran 45 times, n*n is100
n = 100 inner line ran 4950 times, n*n is10000
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
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.
defversion_a(L):
"""Assumes L is a list of ints. Returns two counts."""
first = 0for x in L:
first = first + 1
second = 0for y in L:
second = second + 1return first, second
defversion_b(L):
"""Assumes L is a list of ints. Returns two counts."""
first = 0for x in L:
first = first + 1
second = 0for y in L:
second = second + 1return 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.
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.
The two designs over a hundred names, drawn against the number of searches. The orange line is scanning: a hundred reads every single time, so it climbs steeply from the origin.
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 k
scan each time
build once, then look up
which 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.
defbuild_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 inrange(len(names)):
index[names[i]] = i
return index
defposition_by_scan(names, wanted):
"""Assumes names is a list of strings. Returns the position of wanted and the number of names looked at. """
looked = 0for i inrange(len(names)):
looked = looked + 1if 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'])
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.
deflookup_all(names, wanted):
"""Assumes names and wanted are lists of str. Returns the positions found and the number of positions read. """
found = []
read = 0for name in wanted:
index = {}
for i inrange(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
In [1]: %run untitled0.py
The positions are the easy half: d is at 3 and b is at 1, and a is at 0. The reads are the point. In the first call the build runs twice over four names, which is 8, and in the second it runs once, which is 4. Written properly, with the build above the loop, both calls would have read 4.
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
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.
Name n
Write one line saying what n counts: the length of the list, the number of characters, the value of the argument.
Cut the function into pieces
A piece is one loop with its body, or a stretch with no loop in it.
Price each piece
For each piece: how many passes, in terms of n, and what one pass costs.
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.
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.
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.
State your price list
Write one line: a binding, a comparison, an arithmetic operation and a list index each cost one.
Price the body of each loop once
Go line by line through the body and add the prices.
Count the passes exactly
range(n) gives n. range(i) inside range(n) gives the , which is n times n minus 1 over 2.
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.
Count the uses
Find k, the number of times the collection will be searched.
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.
Set them level and solve for k
The k that makes the two totals equal is the crossing point.
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.
defpair_count(L):
"""Assumes L is a list. Returns how many times the inner line runs."""
runs = 0for i inrange(len(L)):
for j inrange(len(L)):
runs = runs + 1return 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
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.
deftriple_count(L):
"""Assumes L is a list. Returns how many times the inner line runs."""
runs = 0for i inrange(len(L)):
for j inrange(3):
runs = runs + 1return 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.
defshared_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.
defshared_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
Say what n counts, in words, before writing anything else.
Take the function one piece at a time, where a piece is a loop or a loop free stretch.
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.
Multiply those two per piece, and add the pieces, because pieces in a row add and pieces nested inside one another multiply.
Apply the two rules to the sum: keep the fastest growing term, then drop its constant factor.
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.
deftally(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 = 0for x in L:
if x < limit:
below = below + 1
equal = 0for i inrange(len(L)):
for j inrange(i + 1, len(L)):
if L[i] == L[j]:
equal = equal + 1
size = len(L)
rounds = 0while size > 1:
size = size // 2
rounds = rounds + 1return 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
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.
deftotal_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.0for 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
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.
reasoning
There is one loop and nothing nested inside it, so there is one piece to price and no multiplication to do between pieces.
reasoning
The body is one addition, one multiplication, one addition inside the brackets, and one binding to total.
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.
reasoning
The fastest growing term is 4n, so the 2 goes; then the constant factor 4 goes. Both rules applied, in order.
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.
defcount_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 = 0for i inrange(len(L)):
for j inrange(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.
defaudit(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 = 0for x in L:
if x in seen:
repeats = repeats + 1else:
seen.append(x)
size = len(L)
rounds = 0while size > 1:
size = size // 2
rounds = rounds + 1returnlen(L), repeats, rounds
print(audit([7, 2, 7, 4, 2, 7, 1, 9]))
Find
(a) Give the worst case class of audit in terms of n, with the working set out in the six steps.
(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
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.
defmerge_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 = 0for x in a:
found = Falsefor y in b:
compares = compares + 1if x == y:
found = Trueifnot found:
only_a.append(x)
size = len(a)
rounds = 0while size > 1:
size = size // 2
rounds = rounds + 1return 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
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 1while n > 1:
n = n - 1# loop 2while n > 1:
n = n // 3# loop 3while n > 1:
n = n - n // 2# loop 4while n > 1:
n = n * 2if 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.
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.
defwith_append(n):
"""Assumes n is an int >= 0. Returns a list of n zeros."""
out = []
for i inrange(n):
out.append(0)
return out
defwith_plus(n):
"""Assumes n is an int >= 0. Returns a list of n zeros."""
out = []
for i inrange(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 = 0while n > 1:
n = n // 2
steps = steps + 1print(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
In [1]: %run untitled0.py
The chain is 100, 50, 25, 12, 6, 3, 1, which is six arrows. Two of them round down: 25 becomes 12 and 3 becomes 1. A quick check against the powers of two: 2 to the sixth is 64 and 2 to the seventh is 128, and 100 sits between them, so the count has to be 6.
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 = 0for i inrange(4):
for j inrange(i):
total = total + 1
i = i + 10print(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
In [1]: %run untitled0.py
The inner line ran 0 plus 1 plus 2 plus 3 times, which is 6, the triangular count for n equal to 4. The final i is 13, because the rebinding at the top of each pass wipes out the plus 10 from the previous pass, but after the last pass there is no next pass to wipe it, so the 3 becomes 13 and stays.
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
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.
defrecord(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
In [1]: %run untitled0.py
There are two lists here, not four. The default list is built once when the def runs, and the first, second and fourth calls all append to that same object, so it grows across calls. The third call supplies a fresh list, prints it, and then that list is thrown away, which is why the fourth line carries on from the second and not from the third.
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
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 = 0for x in L:
if x % 2 == 0:
L.remove(x)
passes = passes + 1print(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
In [1]: %run untitled0.py
Position 0 sees 1 and keeps it. Position 1 sees 2, removes it, and the list becomes [1, 3, 4]. Position 2 now sees 4, not 3, because everything slid down one place, so 3 is skipped entirely. 4 is removed and the list becomes [1, 3]. Position 3 does not exist any more, so the loop stops after three passes.
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.
defcount_calls(n):
"""Assumes n is an int >= 1. Returns the pass counts of three loops."""
a = 0for i inrange(n):
for j inrange(4):
a = a + 1
b = 0for i inrange(n):
for j inrange(n):
b = b + 1
c = 0
m = n
while m > 1:
m = m // 2
c = c + 1return 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
In [1]: %run untitled0.py
At n equal to 4 the first two pieces tie at 16, which is exactly the coincidence that makes 4n and n^2 look interchangeable. At n equal to 16 they are 64 and 256. Quadrupling the size quadrupled the first count and multiplied the second by sixteen, and the third count merely went from 2 to 4.
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
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.
classShelf(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 = []
defadd(self, title):
"""Adds title if it is not already on the shelf. Returns whether it was added. """if title inself.__titles:
returnFalseself.__titles.append(title)
returnTruedefsize(self):
"""Returns how many titles the shelf holds."""returnlen(self.__titles)
s = Shelf('fiction')
print(s.add('Ada'), s.add('Zeno'), s.add('Ada'))
print(s.size())
Find
(a) Write the two lines the script prints.
(b) Give the worst case class of add on a shelf that already holds n titles, and of size.
(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
In [1]: %run untitled0.py
Ada and Zeno are new so both return True; the second Ada is already there so it returns False and nothing is appended, leaving a size of 2. (b)add is O(n) in the worst case, where n is the number of titles already on the shelf: the membership test walks the whole list when the title is not there, and append is constant.
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
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
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.
deffirst_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] = Trueif item in table:
return item
return -1print(first_shared([4, 7, 9], [9, 7]))
print(first_shared([1, 2], [8]))
Find
(a) Write the two lines the script prints.
(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.
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.
defrow_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 = 0for row in grid:
total = 0for 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
(a) Write the line this prints.
(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
In [1]: %run untitled0.py
The four row totals are 1 plus 2 plus 3 and so on, and the read count is 4 times 3, which is 12. For the class: the inner line runs once per cell, so the cost is O(rc) where r is the number of rows and c the number of columns. It is not quadratic in general.
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
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.
defjoined(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 = 0for 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
In [1]: %run untitled0.py
The string is 'ab cd ef' after strip takes the trailing space off. The 18 is 3 plus 6 plus 9: each pass copies everything built so far, so the copies pile up instead of staying level. With k words of length L the total is L plus 2L plus up to kL, which is L k(k+1)/2, so the cost is quadratic in the number of words.
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
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.
classMarks(object):
"""A set of marks for one course."""def__init__(self, values):
"""Assumes values is a list of ints."""self.__values = values
defhighest(self):
"""Returns the largest mark."""
top = self.__values[0]
for v inself.__values:
if v > top:
top = v
return top
defcount(self):
"""Returns how many marks there are."""returnlen(self.__values)
defgaps(m):
"""Assumes m is a Marks object. Returns the gaps to the top."""
out = []
for i inrange(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.
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.