7 concepts17 worked examples27 exercises3 exam-level7 figures
What are you here for?
10 Searching and sorting: linear search, bisection search, bubble, selection and merge sort, and the recursion they are written with
Start with this
One question before you read anything. Getting it wrong is the point: it shows you what this section is for.
§10.0 — what sort hands back
Before the searches and sorts, three things from earlier weeks decide whether your code here works at all. First one: a student wants a sorted list and writes the line the way you would write it for sorted.
nums = [5, 2, 9]
nums = nums.sort()
print(nums)
Find(a) Write exactly what this prints.
Given
nums starts as the list [5, 2, 9].
sort is the list method, called on nums itself.
IPython console
In [1]: %run untitled0.py
The sort did happen. [2, 5, 9] was built inside the list object, and then the assignment threw away the only name pointing at it and put None there instead. Had the line been nums.sort() on its own, print(nums) would show [2, 5, 9].
Hint 1/4
The question is not what the sorted list is. It is what the expression on the right of the equals sign evaluates to.
Hint 2/4
A method that changes the object it was called on and has nothing new to report hands back None. sort is one of those.
Hint 3/4
So nums.sort() rearranges the list and evaluates to None, and then that None is assigned to the name nums. The starting list was [5, 2, 9].
Hint 4/4
The sorted list exists for a moment and then loses its only name. The program prints None.
Show solution
Evaluate the right hand side first
$$\texttt{nums.sort()}$$
Python evaluates the right of an assignment before it binds the name, so the sort runs now.
$$\texttt{nums} \to [2, 5, 9]$$
The list object itself is rearranged. Nothing is copied.
$$\texttt{nums.sort()} \to \texttt{None}$$
The method has no new object to report, and a function with nothing to return hands back None.
Then bind the name
$$\texttt{nums = None}$$
The assignment overwrites the only name that pointed at the sorted list.
$$\texttt{print(nums)} \to \texttt{None}$$
What is printed is whatever the name holds now, which is None.
Answer $$\boxed{\texttt{None}}$$
Check
Independent check: print(type(nums)) after the same two lines prints <class 'NoneType'>, which a list could never print.
Rule to carry: if a line both sorts and assigns, one of the two is wrong. Use L.sort() alone, or copy = sorted(L).
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.
A till program holds 20000 product codes in a list and looks up four of them. For each code it walks the list from the front until it finds a match. The codes it is asked for happen to sit near the end.
# One full scan of the price list for every code we look up.
codes = list(range(0, 40000, 2))
wanted = [39998, 39996, 39994, 39992]
comparisons = 0for w in wanted:
for c in codes:
comparisons += 1if c == w:
breakprint('codes in the list:', len(codes))
print('comparisons for 4 lookups:', comparisons)
Sample Run:
codes in the list: 20000
comparisons for4 lookups: 79994
Eighty thousand comparisons for four lookups. The list was already in ascending order the whole time, and nothing in the program used that. The same four lookups on the same list can be answered in fewer than sixty comparisons.
By the end of this section you can look at a list and a lookup and say which of the two searches applies and how many comparisons it will take at worst; write , , and both searches from a specification, iteratively or recursively; and take a short program built from any of them and write down exactly what it prints.
In 60 seconds
Searching a list costs one pass unless the list is sorted, in which case it costs the number of times you can halve it; sorting costs a lot once and makes every later search cheap; and every here can be written either as a loop or as a function that calls itself on a smaller piece.
Linear search, any list
$$n \text{ comparisons at worst},\quad O(n)$$
The list is in no particular order, or you will search it only a few times. Works on anything you can compare with the equals sign.
Bisection search, sorted list only
$$\log_{2} n + 1 \text{ comparisons at worst},\quad O(\log n)$$
The list is sorted ascending. On an unsorted list it gives wrong answers silently, so check the order first.
A lab sheet names one of them, or the list is short. Both rearrange the list you handed in and return nothing.
Merge sort
$$n\log_{2} n \text{ comparisons},\quad O(n\log n),\quad \text{new list}$$
The list is long. It needs room for copies, and it hands back a sorted list instead of changing yours.
Three most common mistakes
Running a bisection search on a list nobody sorted. It answers -1 for a value that is sitting in the list, and no error is raised.
Writing numbers = numbers.sort(). The method sorts in place and hands back None, so the name now holds None and the sorted list is gone.
A recursive function whose recursive call has no return in front of it. Every deeper call runs, the answer is computed, and then it is thrown away: the function returns None.
The weights for this course are labs 20, midterm 40 and final 40. Two real papers from this course were checked while this page was written: on the midterm the question that only asked what four short programs print was 30 of 100, and on the final the same kind of question was 20 of 100. This week's material comes after the midterm, so it is final material, and the trace questions on this page are the exercise for that block of marks.
How much time do you have?
10 minutes
The two searches, the one condition that separates them, and the cost of each. This is what a multiple choice question or a one line lab step asks for.
The 60-second card · Linear search, and the one extra line a sorted list buys you · Bisection search · Formula card
45 minutes
Enough to write any of the five algorithms from a written specification, to write a recursive function over a list, and to trace a search or a sort loop on paper without running it.
The 60-second card · A function that calls itself · Linear search, and the one extra line a sorted list buys you · Bisection search · Bubble sort and selection sort · Merge sort · Scaffolding comes off · B · computation
full read
Everything, including why sorting first is worth paying for only after about fifteen lookups, what each algorithm costs with the counts measured rather than claimed, and the eight places students lose marks on this material.
The 60-second card · Recall first · Conventions · A function that calls itself · Linear search, and the one extra line a sorted list buys you · Bisection search · What each search costs · Bubble sort and selection sort · Merge sort · Paying for a sort once · Method boxes · Look-alike pairs · Scaffolding comes off · Full exam-style question · Practice set · Mistake ledger · Glossary
By the end of this section
Write a function that solves a list problem by calling itself on a shorter piece, naming the base case first and making sure every path ends in a return.
Implement linear search on a list in no particular order and on a list sorted ascending, and say what the second version may do on the first kind of list.
Implement bisection search both as a while loop and as a recursive function, and trace the values of first, last and mid step by step.
Count the comparisons each search makes in the best, average and , and name the complexity class of each.
Write bubble sort and selection sort from a specification, trace one pass of each on paper, and say which one has finished after a given number of passes.
Write the merge step and the recursive merge sort around it, and say what merge sort hands back and what it leaves untouched.
Decide from the number of lookups whether sorting the list first pays for itself, and pick between sort, sorted and a dictionary for a given task.
Syllabus coverage
covered
Simple Algorithms and Data Structures — covered
The two shapes every algorithm here uses: a loop over a list, or a function calling itself on a shorter piece of it. Base and recursive case, what a call costs in memory, how to turn one shape into the other. The data structure throughout is the list from the structured types week, plus the dictionary as a search-free lookup.
covered
Search and Sort Algorithms — covered
Linear search on a list in no particular order and the early stop that a sorted list allows
bisection search as a loop and as a
bubble sort with the already sorted flag
selection sort
merge sort with its merge step and the comparison function passed in as a parameter, which is where the course introduces the one line
the built in sort and sorted
covered
Chapter 10 — covered
The chapter's own argument, which is about cost rather than code: what each of these algorithms costs in comparisons, why the same problem has several algorithms with different costs, and the closing idea of , that the price of one sort is worth paying when many searches follow.
off syllabus
Hash tables — off syllabus
The last part of the chapter, where the dictionary is opened up and the hash function that gives it constant time lookup is written out.
The result is used on this page, because deciding whether to sort at all needs it: a dictionary lookup does not search. How the hash function itself is built is not developed here.
Recall first
Big O and the worst case
The cost of a program is counted in basic steps, not seconds, and only the fastest growing term is kept: keep the largest term of a sum and drop any constant multiplying it. So 1000 + x + 2x squared is O(x squared). The worst case is the largest number of steps over all inputs of a given size, and it is the case reported unless a block says best or average. The classes this page uses are O(1), O(log n), O(n), O(n log n) and O(n squared), in that order of growth.
Every claim on this page about one algorithm being better than another is a claim in these terms. Without the worst case convention the sentence linear search takes n steps is simply false, because sometimes it takes one.
List indexing, slicing and the list methods
len(L) is the number of items, and the valid indices run from 0 to len(L) - 1. L[i:j] is a NEW list holding items i up to but not including j; L[:] is a new list holding all of them. L.append(x) adds one item at the end and returns nothing. L.pop(i) removes the item at index i and returns it. x in L walks the list and gives True or False.
Every algorithm here is written with these and nothing else. The difference between a slice and the list itself is the difference between merge sort and bubble sort in memory.
A function, its parameters and its return
def f(a, b=0): gives b the value 0 when the caller leaves it out. A function that reaches the end of its body without a return hands back None. A parameter is a new name; assigning to it inside the function does not touch the caller's name, but changing the object it points at does.
The default parameter is how the recursive functions here hide their index from the first caller, and the missing return is the most common fault in a recursive function.
The less than sign on your own objects
Writing def __lt__(self, other): in a class decides what a < b means for two objects of that class. Once it is there, L.sort(), sorted(L), min and max all work on a list of those objects, because all of them ask the same question.
The lab this week sorts a list of objects. The sort code does not change at all; the only new line is in the class.
Try it yourself first (2 questions)
1§10.0 — a list handed to a function that sorts it
Second one. A function is given a list and sorts it. The caller prints its own name afterwards.
deftidy(values):
"""Sorts the list it was given."""
values.sort()
returnlen(values)
data = [4, 1, 3]
how_many = tidy(data)
print(how_many)
print(data)
Find(a) Write both lines of output.
Given
data starts as [4, 1, 3].
tidy calls sort on its parameter and returns the length.
IPython console
In [1]: %run untitled0.py
This is the behaviour every in place sort on this page relies on. Bubble sort and selection sort return nothing at all; the only way the caller sees their work is exactly this, through the shared object. It is also why you must never sort a list a caller still wants in its original order without saying so.
Hint 1/4
Two separate questions: what the function returns, and what the caller's list looks like after the call.
Hint 2/4
A parameter is a new name for the SAME object. Sorting through that name sorts the one list that both names point at.
Hint 3/4
So values and data are two names for [4, 1, 3]. The sort rearranges that one object, and len of it is 3.
Hint 4/4
The function returns 3, and data is now sorted.
Show solution
Bind the parameter
$$\texttt{tidy(data)}$$
The call gives the parameter values the same object data points at, not a copy.
$$\texttt{values.sort()}$$
Chosen by the author over sorted precisely because the caller is meant to see the change.
Read both names afterwards
$$\texttt{len(values)} = 3$$
Sorting never changes the length, so the return value is the starting length.
$$\texttt{data} \to [1, 3, 4]$$
The same object was rearranged, so the caller's name shows the sorted order.
Answer $$\boxed{3 \;/\; [1, 3, 4]}$$
Check
Independent check: replacing values.sort() with values = sorted(values) leaves data as [4, 1, 3], because that line rebinds the local name instead of touching the object.
2§10.0 — the midpoint of two indices
Third one, and the one that breaks a hand written bisection search. Two indices, and the two ways of halving the distance between them.
first = 3
last = 8print((first + last) / 2)
print((first + last) // 2)
print(type((first + last) // 2))
Find(a) Write all three lines.
Given
first is 3 and last is 8.
One slash is division, two slashes are whole division.
IPython console
In [1]: %run untitled0.py
Only the second of these can be used as an index. A list subscript must be an int, so L[5.5] raises TypeError, and a bisection search written with one slash fails on its very first comparison.
Hint 1/4
Three separate questions: the value with one slash, the value with two, and the type of the second one.
Hint 2/4
One slash always gives a float, even when the division comes out even. Two slashes divide and throw the fraction away, giving an int.
Hint 3/4
With first 3 and last 8 the sum is 11. Eleven over two is 5.5; eleven whole divided by two is 5.
Hint 4/4
5.5, then 5, then the type of 5.
Show solution
Divide both ways
$$(3 + 8) / 2 = 5.5$$
A single slash is true division, and its result is a float whatever the operands.
$$(3 + 8) // 2 = 5$$
Whole division drops the fraction. The value 5 is chosen over rounding to 6 because Python truncates towards minus infinity, and for positive numbers that is just dropping the tail.
Ask which one an index accepts
$$\texttt{type(5)} \to \texttt{int}$$
Whole division of two ints gives an int, and only an int can index a list.
Independent check on the truncation rather than rounding: (3 + 10) // 2 gives 6 and not 7, although 13 over 2 is 6.5.
Notation
symbol
reads as
means
watch out
$n$
n
The number of items in the list an algorithm is working on.
It is the length of the list, never the value of an item in it. A list of three numbers has n equal to 3 even if one of the numbers is 900000.
$O(n)$
order n, or big O of n
An upper bound on how the number of steps grows as the list gets longer. O(n) says the steps grow in step with the list length.
O throws away constants and smaller terms, so 3n + 40 and n are both O(n). Two algorithms in the same class can still differ by a factor of ten on your machine.
$first, last, mid$
first, last, mid
The three index names in the bisection loop: the left end of the part still worth searching, the right end of it, and the index halfway between them.
last is an index, so it starts at len(L) - 1 and not at len(L). One wrong character here gives an IndexError on the second call rather than the first.
$//$
whole division, or floor division
Divides and throws the fraction away, giving an int. The midpoint is computed with it because a list index has to be a whole number.
A single slash gives a float, and a float cannot be used as an index. This is the single most common reason a hand written bisection search raises TypeError.
$L[i:j]$
L from i up to j
A new list holding the items of L from index i up to but not including index j. Merge sort splits a list with two of these.
It is a new list, so slicing inside a recursion copies. That is why merge sort uses more memory than the sorts that swap in place.
$L[:]$
all of L
A new list holding everything L holds. Merge sort returns this in its base case so that the caller never gets a second name for the caller's own list.
L[:] and L are different objects. Writing return L in the base case instead would hand back the very list that was passed in, and a later change would show up in both places.
$log_2 n$
log base two of n
How many times n can be halved before one item is left. For 20000 items that is 14.
Complexity classes are written O(log n) with no base, because changing the base only multiplies by a constant and O drops constants. The count of halvings is still base two.
Conventions used here
Every output block on this page is a real run.
No output here was predicted by eye. Every program was written to a file, run, and the characters it printed were copied back in. That is why one block below ends in a blank line: the program really printed an empty string there.
In a programming course the printed answer is the whole claim. A page that guesses output teaches the guess.
The tools this page keeps inside.
Numbers, text, True and False, if, while, for, range, len, def, return, default parameter values, lists with append, pop, remove, index, sort and sorted, slicing, dictionaries with keys and pop, files, tuples, two dimensional lists, and classes with __init__, private attributes, __lt__ and __repr__. Nothing else. No comprehension, no f string, no enumerate, no zip, no key= keyword on sort, no module beyond what the course has imported.
The lab sheets say to use only what the course has covered, and the exam is closed book with a fixed list of functions printed on the cover. A shortcut you cannot use in the exam is a shortcut that costs marks.
The one shortcut this week does add.
The one line lambda appears once, because the course's own merge sort carries compare = lambda x, y: x < y as a default parameter. Every solution on this page also shows the same thing written as a named def, and the named version is the one to copy in an exam answer unless the question asks for a lambda.
Leaving the lambda out would leave the course's own merge sort unreadable, and putting it everywhere would teach a form the rest of the course does not use.
Where the comparison counts come from.
Every count of comparisons on this page was produced by running the algorithm with a counter added to it, not by evaluating a formula. Where a formula and a count disagree the count is printed and the formula is called approximate.
A formula like n log n is a growth rate, not a promise about a particular list. The measured number is what the machine did.
Ascending order, and what happens to ties.
Sorted means ascending unless a block says otherwise: smallest first, and for text the order Python's less than sign gives, which puts every capital letter before every small one. Two items that compare equal keep the order they were in for merge sort and for the built in sort, and may not for the two swapping sorts, so no claim about ties is made for those.
A lab sheet that says sort by count, then by name for ties, is asking about exactly this, and a wrong tie rule is a wrong answer even when the list looks sorted.
In place against hands back a new list.
For every operation here two separate questions are answered: does it change the list you gave it, and what does it hand back. Bubble sort and selection sort change the list and hand back nothing. Merge sort changes nothing and hands back a new list. sort changes and hands back None; sorted changes nothing and hands back a list.
Most lost marks on this material are not about the algorithm at all, they are about which of these four a line of code did.
The lab this section is written for.
This page is the eighth lab: a class given an ordering, a container class whose data is private, a bubble sort over the objects in it that uses that ordering, and a recursive search over the same list that prints a report. The worked example near the end of the page is that whole shape with different data.
The lab sheet asks for a sort and a recursive search inside a class, not for either one on its own, and the joint is where the marks are.
The docstring every function here carries.
Every function and method on this page starts with a docstring in the course's form: what the parameters are assumed to be, then what it returns or what it changes. For a recursive function the docstring also says what the index parameter means, because that is the part a reader cannot guess.
The lab sheets ask for docstrings, and for a recursive function the docstring is what makes the recursive call readable.
10.1A function that calls itself: the base case and the recursive case
A function that hands the rest of the list back to itself, so the code only has to say what to do with one item.
The complexity week gave us a way to price a program. This week is about the programs, and three of the five need a shape we have not written.
Solvable with what we have
Walk a list with for i in range(len(L)).
Build an answer up in a variable across the passes of a loop.
Give a parameter a default value.
Split a list with L[:middle] and L[middle:].
Not solvable yet
Write the report the lab asks for, whose specification says the search must be recursive.
Sort by splitting a list in half and sorting each half the same way. No loop shape fits the middle step.
Write bisection search without a while loop.
The first worked example below is the honest first attempt: a function that adds up prices by taking the first one and asking itself for the rest. It raises IndexError: list index out of range.
Why it fails
The idea is right and the stopping is missing. Nothing in it says what the answer is when no items are left, so the calls run down to the empty list and then ask for prices[0] of it.
RuleRule 10.1: the two parts of a recursive function
Conditions
There is a smallest input for which the answer needs no work at all. For a list that is usually the empty list, or the single item at the end.
Every recursive call is made on something strictly smaller, so the smallest input is always reached.
Every branch of the function ends in a return, including the one that contains the recursive call.
Say what the answer is when there is nothing left to do, and then say how to build the answer for the whole thing out of one item and the answer for everything except that item. Those two sentences are the whole function. You never write the middle.
Four calls of factorial live at once, each with its own n. Nothing is multiplied on the way down; the multiplications all happen on the way back up, which is why the deepest call has to return before the first one can.
Looks like this, but is not
This has a base case and a call to itself.
deftotal(prices, i=0):
"""Meant to add up prices from index i on."""if i == len(prices):
return0return prices[i] + total(prices, i)
The call passes i again instead of i + 1, so every call is handed the problem it was given and the base case can never be reached. Having a base case is not what makes a recursion finish; getting closer to it on every call is.
The total of a basket of prices, first without a base case and then with one
Add up a list of prices by taking the first one and asking the same function for the rest. Here is the attempt with the recursive line and nothing else.
deftotal(prices):
"""Adds up a list of prices. There is no base case."""return prices[0] + total(prices[1:])
basket = [2.5, 3.0, 1.5]
print(total(basket))
Run:
Traceback (most recent call last):
File "basket.py", line 6, in <module>
print(total(basket))
^^^^^^^^^^^^^
File "basket.py", line 3, in total
return prices[0] + total(prices[1:])
^^^^^^^^^^^^^^^^^
File "basket.py", line 3, in total
return prices[0] + total(prices[1:])
^^^^^^^^^^^^^^^^^
File "basket.py", line 3, in total
return prices[0] + total(prices[1:])
^^^^^^^^^^^^^^^^^
[Previous line repeated 1 more time]
IndexError: list index out of range
Now the same function with two lines added, and a third call on the empty list to prove the new lines work.
deftotal(prices):
"""Assumes prices is a list of numbers. Returns their sum."""iflen(prices) == 0:
return0return prices[0] + total(prices[1:])
basket = [2.5, 3.0, 1.5]
print('items:', len(basket))
print('total:', total(basket))
print('empty basket:', total([]))
Sample Run:
items: 3
total: 7.0
empty basket: 0
FindWhy the first version stops with an error, and what the second one prints.
Given
basket is [2.5, 3.0, 1.5].
The recursive line is prices[0] + total(prices[1:]).
Each call slices off the front, so the list really is getting shorter and the recursion is not circular.
$$\texttt{prices[0]}\ \text{on}\ []$$
There is no item 0 of an empty list, so the read raises IndexError. The fault is not in the recursive line; it is that the recursive line is the only line.
Add the line that answers for nothing
$$\texttt{if len(prices) == 0: return 0}$$
0 is the sum of no numbers, chosen over any other value because adding it to the rest changes nothing. It must come before the read, not after.
$$2.5 + (3.0 + (1.5 + 0)) = 7.0$$
The additions all happen on the way back up, once the base case has handed its 0 to the innermost waiting call.
$$\texttt{total([])} = 0$$
The call that exercises the new line on its own. A recursive function should always be tested on the input its base case describes.
Independent check of the total without the recursion: 2.5 plus 3.0 is 5.5 and plus 1.5 is 7.0, which is what printed.
Four calls for three prices, and each one slices the tail, so the copying costs about n squared over 2 item moves. An index parameter avoids that and is what the next example uses.
This is the loop the hook of the section does not close, so close it here: the missing piece was never the recursive call, it was the sentence about what to answer when there is nothing left.
factorial of 5, written as a base case and one multiplication
Write factorial(n) without a loop. The mathematics already has the two sentences in it: 1 factorial is 1, and n factorial is n times (n minus 1) factorial.
deffactorial(n):
"""Assumes n is an int and n >= 0. Returns n!."""if n == 0or n == 1:
return1else:
return n * factorial(n - 1)
value = 5print('factorial of', value, 'is', factorial(value))
Sample Run:
factorial of 5is120
To see the four calls that answer actually took, the same function with a print on the way in and a print on the way out.
deffactorial(n):
"""Assumes n is an int and n >= 0. Returns n!."""print('call factorial(' + str(n) + ')')
if n == 0or n == 1:
print('base factorial(1) gives 1')
return1
answer = n * factorial(n - 1)
print('return factorial(' + str(n) + ') gives', answer)
return answer
result = factorial(4)
print('result:', result)
FindThe value of factorial(5), and the order the calls finish in.
Given
n is an int and not negative.
0 factorial and 1 factorial are both 1.
Solution
Name the input that needs no work
$$\texttt{if n == 0 or n == 1: return 1}$$
Both are base cases, and both are needed: 0 is chosen as well as 1 because factorial(0) is a legal call and with only the n == 1 test it would fall through to n times factorial(-1).
Say how the whole answer is built from one step
$$\texttt{return n * factorial(n - 1)}$$
The one item is the factor n; the rest is the same problem one size smaller. Written with n - 1 rather than n // 2 because the base case is one step away from every value, so shrinking by one reaches it.
The first call cannot finish until the deeper one does.
$$= 5 \cdot 4 \cdot 3 \cdot 2 \cdot 1 = 120$$
Once the base case returns 1, each waiting call multiplies and returns, innermost first.
Answer $$\boxed{\texttt{factorial of 5 is 120}}$$
Check
Independent check without recursion: 5 factorial is the product of 1 through 5, which a loop gives as 1, 2, 6, 24, 120.
Five calls for n equal to 5, and every one of them is alive at the same time. The number of calls is n, so this is O(n) in time and, unlike the loop version, O(n) in memory too.
The order of the two prints in the traced version is the thing to carry: every call goes all the way down before any multiplication happens. A recursive function that prints before its call prints top down, and one that prints after its call prints bottom up.
A recursive report over a stock list, the shape the lab asks for
A shop keeps two lists of the same length: the product names and their counts. Print one line for every product whose count is below a limit, saying how many to order. The specification says the search must be recursive, so there is no for anywhere.
deffind_low(counts, names, limit, i):
"""Assumes counts and names are lists of the same length, limit an int, i an index. Prints every name whose count is below limit, from index i on."""if i == len(counts):
returnif counts[i] < limit:
print(limit - counts[i], 'of', names[i], 'must be ordered')
find_low(counts, names, limit, i + 1)
names = ['gofret', 'benimo', 'simit', 'metro']
counts = [4, 5, 5, 7]
find_low(counts, names, 6, 0)
Sample Run:
2 of gofret must be ordered
1 of benimo must be ordered
1 of simit must be ordered
FindThe lines printed, and why the function needs an index parameter.
Given
names is ['gofret', 'benimo', 'simit', 'metro'].
counts is [4, 5, 5, 7], in the same order.
The limit is 6, and the call starts at index 0.
Solution
Turn the index into the shrinking thing
$$\texttt{find\_low(counts, names, limit, i)}$$
The list itself is not sliced. An index is added as a parameter and that is what shrinks, chosen over counts[1:] because slicing copies the list on every call and here nothing is returned anyway.
$$\texttt{if i == len(counts): return}$$
The base case is reaching the end. A bare return is right because the function reports by printing and has no value to hand back.
Do the work for one index, then pass the rest on
$$\texttt{if counts[i] < limit:}$$
The test is on one item only. Whether any other item is low is not this call's business.
The shortfall is limit minus count, so for gofret it is 6 - 4 = 2. Both lists are read at the same index, which is what keeps the name attached to the right count.
$$\texttt{find\_low(counts, names, limit, i + 1)}$$
Last line, and no return in front of it on purpose: there is nothing to return. In a function that computes a value this missing return would be the bug in the next box down.
Answer $$\boxed{\texttt{2 of gofret}\ /\ \texttt{1 of benimo}\ /\ \texttt{1 of simit}}$$
Check
Independent check by eye against the data rather than the code: the counts below 6 are 4, 5 and 5, sitting at indices 0, 1 and 2, and metro's 7 is not below 6.
One call per item, so four calls for four products. Same cost as the loop; the recursion buys nothing here except that the specification asked for it.
This is the pattern for every recursive pass over a list in this course: an index parameter, a base case that compares it with len, the work for one index, then the same call with i + 1.
Checkpoint
§10.1 — where a recursive print lands
Thirty seconds. The function prints before it calls itself, and the base case prints as well.
defshrink(word):
"""Prints the word, then the word with its first letter removed, and so on."""iflen(word) == 0:
print('done')
returnprint(word)
shrink(word[1:])
shrink('sort')
Find(a) Write every line it prints, in order.
Given
The call is shrink('sort').
word[1:] is the word without its first letter.
'sort'[1:] is 'ort', and ''[1:] is ''.
IPython console
In [1]: %run untitled0.py
Five calls and five lines. The base case is not a silent stop here; it prints too, and its line is last because it is the deepest call and the print in it comes after all four calls on the way down.
Hint 1/4
You need the sequence of words the calls are made with, and then which of them reach a print.
Hint 2/4
The print comes before the recursive call, so a word is printed on the way down. The base case has its own print, so it appears too.
Hint 3/4
Starting from 'sort', the calls are made with 'sort', 'ort', 'rt', 't', ''. The first four have a positive length.
Hint 4/4
Four words on four lines, then the word done from the base case.
The fifth call takes the base case, prints and returns without calling again.
Answer $$\boxed{\texttt{sort, ort, rt, t, done}\ \text{on five lines}}$$
Check
Independent check on the count: the word has four letters, one line per letter is four, plus one line from the base case is five.
Print before the call and you read the input shrinking. Print after the call and you would read the same words in the opposite order.
⚠ No base case at all
The recursive line is the interesting one and gets written first. The stopping line has nothing to do with the idea, so it feels like boilerplate and gets left out.
right$$\texttt{if len(prices) == 0: return 0}\;\text{first, then the recursive line}$$
⚠ The recursive call with no return in front of it
A recursive call looks like a statement, because in a printing function like the stock report it is one. In a function that computes a value it is part of an expression that has to be returned.
wrong$$\texttt{count\_down(n - 1)}$$
right$$\texttt{return count\_down(n - 1)}$$
⚠ A call that does not get closer to the base case
The index or the slice is copied from the parameter list without the change. The function then looks complete: it has both parts, and it still never stops.
wrong$$\texttt{return f(L, i)}$$
right$$\texttt{return f(L, i + 1)}$$
10.2Linear search, and the one extra line a sorted list buys you
Walk the list from the front and compare; only a sorted list lets you stop before the end and still be right.
With the recursive shape in hand, here is the first of the two searches, the one that asks nothing at all of the list it is given.
RuleRule 10.2: linear search, and the early stop
Conditions
The items can be compared with ==. Nothing else is required, so this works on a list in any order and on items of any type.
The early stop line is legal only when the list is sorted ascending. On any other list it may return a wrong answer, and it will not raise an error while doing so.
$$\boxed{\begin{aligned}&\textbf{any list:}&&\texttt{for i in range(len(L)):}\\& &&\quad\texttt{if L[i] == e: return True}\\& &&\texttt{return False}\\[2pt]&\textbf{sorted only:}&&\quad\texttt{if L[i] > e: return False}\end{aligned}}$$
Look at each item in turn. The moment one of them equals what you want, answer yes and leave. If you get to the end without a match, answer no. If the list happens to be sorted, you may also answer no as soon as an item is bigger than what you want, because everything after it is bigger still.
The same three comparisons, doing two different amounts of work. On the unsorted row three comparisons settle one question, whether 16 is there. On the sorted row three comparisons settle the question for 7 and for everything bigger than 16 as well, which is what the early stop is buying.
Looks like this, but is not
The sorted version is shorter and faster, and it looks like a strict improvement. Here it is on a list nobody sorted, looking for a value that is sitting in it.
defsearch(L, e):
"""The sorted-list version, with the early stop."""for i inrange(len(L)):
if L[i] == e:
returnTrueif L[i] > e:
returnFalsereturnFalse
unsorted = [34, 6, 16, 35, 1]
print('16 is in the list:', 16in unsorted)
print('search says: ', search(unsorted, 16))
Run:
16isin the list: True
search says: False
in says 16 is there and the function says it is not. The first item is 34, which is bigger than 16, so the early stop fires on comparison one and the other four items are never looked at. The line if L[i] > e: return False is not an optimisation of linear search, it is a different function with a precondition.
looking for
in the list
no early exit
stops at the first match
34, at index 0
yes
5
1
7
no
5
5
Stopping at the first match helps a lot when the item is near the front and not at all when the item is absent, because absence can only be established by looking at everything. That is why both versions are called O(n): the worst case is the same, and only the lucky cases differ.
linear_search on a list in no particular order, with and without the early exit
The lecture's version sets a flag and keeps going to the end. Write it, then write the version that leaves the moment it finds the item, and count the comparisons each one makes.
deflinear_search(L, e):
"""Assumes L is a list, e any value. Returns True if e is in L."""
found = Falsefor i inrange(len(L)):
if e == L[i]:
found = Truereturn found
plates = [34, 6, 16, 35, 1]
print(linear_search(plates, 16))
print(linear_search(plates, 7))
Sample Run:
TrueFalse
Now both, with a counter, so the difference is a number rather than an opinion.
deflinear_search(L, e):
"""Returns (found, comparisons) with no early exit."""
found = False
comparisons = 0for i inrange(len(L)):
comparisons += 1if e == L[i]:
found = Truereturn found, comparisons
deflinear_search_early(L, e):
"""Returns (found, comparisons), stopping at the first match."""
comparisons = 0for i inrange(len(L)):
comparisons += 1if e == L[i]:
returnTrue, comparisons
returnFalse, comparisons
plates = [34, 6, 16, 35, 1]
print('no early exit, looking for 34:', linear_search(plates, 34))
print('early exit, looking for 34:', linear_search_early(plates, 34))
print('no early exit, looking for 7:', linear_search(plates, 7))
print('early exit, looking for 7:', linear_search_early(plates, 7))
Sample Run:
no early exit, looking for34: (True, 5)
early exit, looking for34: (True, 1)
no early exit, looking for7: (False, 5)
early exit, looking for7: (False, 5)
FindWhat each version returns, and how many comparisons each makes.
Given
The list is [34, 6, 16, 35, 1], in no particular order.
Two targets are tried: 34, which sits at index 0, and 7, which is absent.
Solution
Write the version that cannot be wrong about anything
$$\texttt{found = False}$$
The answer starts as no, so that a list with no match needs no special case at the end.
$$\texttt{for i in range(len(L)):}$$
Indices rather than items, chosen because the lab specifications in this course are written in terms of positions and because the sorted version below needs the index too.
$$\texttt{if e == L[i]: found = True}$$
Recording the match rather than leaving. This is the lecture's form and it is correct, just not thrifty.
Replace the flag with an immediate answer
$$\texttt{if L[i] == e: return True}$$
Returning from inside the loop ends the function, so no later item can change the answer. Safe because a match is final: nothing found later could make the answer no.
$$\texttt{return False}$$
Reached only when the loop ran out, which is exactly the case where the answer is no. It must be outside the loop; inside, it would answer after the first item.
Read the counts
$$34 \Rightarrow 5 \text{ against } 1$$
The target is first, so the thrifty version stops after one comparison and the flag version still walks all five.
$$7 \Rightarrow 5 \text{ against } 5$$
An absent value cannot be ruled out early on an unsorted list, so both versions pay the full price.
Answer $$\boxed{\texttt{True}\ /\ \texttt{False};\ 5\ \text{against}\ 1\ \text{comparisons for a hit at the front}}$$
Check
Independent check on the counter itself: for the absent value 7 both versions report 5, and the list has 5 items, so the counter is counting comparisons and not something else.
Worst case n comparisons for both, 1 for the thrifty one and n for the flag one. Both are O(n), because O reports the worst case.
Returning early changes the best case and not the class. When a question asks for the complexity of linear search the answer is O(n) either way; when it asks how many comparisons for this list and this target, the early exit matters.
Checkpoint
§10.2 — how many items a search looked at
Thirty seconds. The function returns the number of items it looked at before it could answer.
deflinear_search(L, e):
"""Returns how many items it looked at before it answered."""
looked = 0for i inrange(len(L)):
looked += 1if L[i] == e:
return looked
return looked
rooms = [204, 111, 307, 204, 118]
print(linear_search(rooms, 204))
print(linear_search(rooms, 118))
print(linear_search(rooms, 500))
Find(a) Write the three numbers it prints.
Given
The list is [204, 111, 307, 204, 118], in no particular order.
204 appears twice, at index 0 and at index 3.
The three targets are 204, 118 and 500.
IPython console
In [1]: %run untitled0.py
The second 204 at index 3 is never reached, because the function returned at index 0. That is the difference between this function and one that counts occurrences: this one stops, so it can never tell you there were two.
Hint 1/4
For each target, the question is at which index the function first gets to return, or whether it runs off the end.
Hint 2/4
The counter goes up once per item examined, and the return inside the loop fires on the first match, so a match at index i costs i + 1 looks.
Hint 3/4
The list is [204, 111, 307, 204, 118]. 204 matches at index 0. 118 matches at index 4. 500 matches nowhere.
Hint 4/4
One look for 204, five for 118, five for 500.
Show solution
Locate each target
$$204 \to \text{index } 0$$
First item, so the loop returns on its first turn.
$$118 \to \text{index } 4$$
Last item, so every earlier item was looked at first.
$$500 \to \text{absent}$$
No match, so the loop runs out and the count after the loop is the full length.
Turn an index into a count
$$\text{count} = \text{index} + 1$$
The counter is increased before the test, so a match at index i has already counted i + 1 looks.
Answer $$\boxed{1 \;/\; 5 \;/\; 5}$$
Check
Independent check on the last two: a present item at the end and an absent item cost the same, 5, which is the list length.
An absent value always costs the full list on an unsorted list. That single fact is the whole argument for sorting.
⚠ Answering no from inside the loop
The two branches look symmetric, so an else: return False next to the return True reads as tidy. It is not symmetric: one match settles the question, one mismatch settles nothing.
⚠ Using the early stop on a list that is not sorted
The line comes from the same box as the rest of the function and looks like part of it. The precondition lives in the docstring, and the docstring is the first thing dropped when code is copied.
right$$\text{sort first, or drop the line and pay the full }n$$
⚠ Setting the flag and carrying on when the answer is already known
The lecture's own first version does this, and it is correct, so it gets copied into places where the cost matters. It only becomes a real fault inside a loop that searches many times.
right$$\texttt{return True}\;\text{at the match}$$
10.3Bisection search: throw away half the list at every comparison
Compare with the middle item of what is left and drop the half that cannot contain the target.
Sortedness bought linear search half the average and nothing in the worst case. Here is what the same sortedness is really worth.
RuleRule 10.3: bisection search on a sorted list
Conditions
The list must be sorted ascending. On any other list the function returns a wrong answer and raises nothing.
last is an index, so it starts at len(L) - 1. The loop condition is first <= last with the equals sign, because a window of one item still has to be looked at.
The two window ends move past mid, to mid - 1 and mid + 1. Moving them to mid itself leaves the window the same size and the loop never ends.
Keep two indices marking the stretch of the list that could still hold the target. Look at the item halfway between them. If the target is smaller, the answer cannot be to the right of that item, so move the right end just left of it. If the target is larger, move the left end just right of it. If it is equal, you are done. When the two ends cross, the stretch is empty and the target was never there.
Proof
Why the number of comparisons is the number of halvings. Start with a window of n items. Each turn of the loop looks at one item and then keeps at most half of what was left, because mid is the middle and one of the two sides is dropped along with mid itself.
So after k turns the window holds at most n divided by 2 to the power k items. The loop can only continue while the window holds at least one item, so it stops by the time n over 2 to the k reaches 1.
Solving 1 = n over 2 to the k gives 2 to the k equals n, that is k equals log base 2 of n. The count of comparisons is that many plus one, because the last window of one item is also compared.
Checked against a run rather than left as algebra: 20000 items can be halved 14 times, and the measured worst case for 20000 items is 15 comparisons, which is 14 plus one.
Three comparisons answer a nine item list. Notice what is thrown away at step 1: five of the nine boxes go, including the one that was compared, because the target is above it and the list is sorted.
Looks like this, but is not
Bisection search on a list of room numbers that nobody sorted. The list is a perfectly good list and the function is the function from the box, unchanged.
defbinary_search(L, e):
"""Assumes L is sorted ascending. Returns the index of e, or -1."""
first = 0
last = len(L) - 1while first <= last:
mid = (first + last) // 2if e < L[mid]:
last = mid - 1elif e > L[mid]:
first = mid + 1else:
return mid
return -1
room = [12, 45, 3, 88, 27]
print(binary_search(room, 27))
print(27in room)
print(binary_search(room, 12))
print(12in room)
Run:
-1True
-1True
Two values that are in the list are reported as absent, and in disagrees with the search on both. Nothing is broken; the function did exactly what it promises, which is to answer correctly for a sorted list. Sortedness is not a nice to have here the way it was for linear search.
binary_search for 156, 102 and 150 in a sorted list of nine ids
Write the loop version. It should return the index of the value, or -1 when the value is absent. Then run the same function with the window printed at every turn so the three values of mid are visible.
defbinary_search(L, e):
"""Assumes L is a list sorted in ascending order. Returns the index of e in L, or -1 if e is not in L."""
first = 0
last = len(L) - 1while first <= last:
mid = (first + last) // 2if e < L[mid]:
last = mid - 1elif e > L[mid]:
first = mid + 1else:
return mid
return -1
ids = [102, 118, 125, 137, 140, 156, 171, 188, 190]
print('index of 156:', binary_search(ids, 156))
print('index of 102:', binary_search(ids, 102))
print('index of 150:', binary_search(ids, 150))
Sample Run:
index of 156: 5
index of 102: 0
index of 150: -1
The same search for 188, with the window printed before every comparison.
defbinary_search(L, e):
"""Prints the window it is still searching before every comparison."""
first = 0
last = len(L) - 1while first <= last:
mid = (first + last) // 2print('first', first, 'last', last, 'mid', mid, 'L[mid]', L[mid])
if e < L[mid]:
last = mid - 1elif e > L[mid]:
first = mid + 1else:
return mid
return -1
ids = [102, 118, 125, 137, 140, 156, 171, 188, 190]
print('answer:', binary_search(ids, 188))
Sample Run:
first 0 last 8 mid 4 L[mid] 140
first 5 last 8 mid 6 L[mid] 171
first 7 last 8 mid 7 L[mid] 188
answer: 7
FindThe three returned values, and the values mid takes for 188.
Given
The list is [102, 118, 125, 137, 140, 156, 171, 188, 190], sorted ascending, nine items.
Three targets: 156 at index 5, 102 at index 0, and 150 which is absent.
Solution
Set the window to the whole list
$$\texttt{first = 0},\ \texttt{last = 8}$$
last is len(L) - 1 and not len(L), because it is an index into the list and 9 is not one.
Equal, so the function returns 7. Three comparisons for nine items.
Check the case with no answer
$$e = 150:\ \texttt{first} > \texttt{last}$$
The window closes without a match, the loop condition fails, and the return -1 after the loop is reached. That return must be outside the loop; inside it, the search would give up after one comparison.
Answer $$\boxed{5 \;/\; 0 \;/\; -1}$$
Check
Independent check without the algorithm: L[5] is 156 and L[0] is 102, so the two returned indices really do hold the values asked for, and 150 is not among the nine numbers listed.
Three comparisons for nine items. Nine can be halved three times, and 2 to the power 3 is 8, just under 9.
The return value is an index, not True or False. That is worth more: an index lets the caller read, change or delete the item, and -1 is the agreed way of saying there is none.
The same search written recursively, with start and end as parameters
Write bisection search again with no loop. The window ends become parameters, and dropping half the list becomes a call on a narrower window.
defbinary_search(arr, s_val, start, end):
"""Assumes arr is sorted ascending, start and end are indices. Returns the index of s_val between start and end, or -1."""if start > end:
return -1
mid = (start + end) // 2if arr[mid] == s_val:
return mid
elif arr[mid] > s_val:
return binary_search(arr, s_val, start, mid - 1)
else:
return binary_search(arr, s_val, mid + 1, end)
ids = [102, 118, 125, 137, 140, 156, 171, 188, 190]
print('index of 125:', binary_search(ids, 125, 0, len(ids) - 1))
print('index of 999:', binary_search(ids, 999, 0, len(ids) - 1))
Sample Run:
index of 125: 2
index of 999: -1
FindWhat plays the part of the loop, and what plays the part of the final return -1.
Given
The list is the same nine sorted ids.
The first call is made with start 0 and endlen(ids) - 1.
Targets: 125, which is at index 2, and 999, which is absent.
Solution
Turn the loop condition into a base case
$$\texttt{if start > end: return -1}$$
The loop ran while first <= last, so it stopped when the ends crossed. The same crossing is the base case here, and it carries the same answer.
Instead of assigning to last and going round again, the narrower window is handed to a new call. The return in front is essential: the answer comes back from the deeper call and has to be passed on.
The other half. Both calls shrink the window, so the crossing base case is always reached.
Read the two answers
$$125 \Rightarrow 2$$
Same index the loop version gives, because it is the same algorithm with the state moved into parameters.
$$999 \Rightarrow -1$$
The target is above everything, so start climbs past end and the base case answers.
Answer $$\boxed{2 \;/\; -1}$$
Check
Independent check against the loop version: both versions were run on the same nine item list and returned the same index for a present value and -1 for an absent one.
The same number of comparisons as the loop, so both are O(log n) in time. The recursive one also keeps one call frame per comparison, so its memory is O(log n) where the loop's is O(1).
Whenever a loop keeps a couple of index variables and narrows them, it can be rewritten as a recursion by making those variables parameters. The loop condition becomes the base case, inverted.
Checkpoint
§10.3 — three windows and the answer
Thirty seconds. The function prints first, last and mid before each comparison, and the target is the very first item of the list.
defbinary_search(L, e):
"""Prints first, last and mid before every comparison."""
first = 0
last = len(L) - 1while first <= last:
mid = (first + last) // 2print(first, last, mid)
if e < L[mid]:
last = mid - 1elif e > L[mid]:
first = mid + 1else:
return mid
return -1
marks = [12, 25, 33, 41, 58, 64, 77]
print('answer', binary_search(marks, 12))
Find(a) Write every line, including the last one.
Given
The list is [12, 25, 33, 41, 58, 64, 77], sorted ascending, seven items.
The target is 12, which sits at index 0.
mid is (first + last) // 2, whole division.
IPython console
In [1]: %run untitled0.py
The first item of the list is the most expensive one for this search, not the cheapest. Linear search finds it in one comparison and bisection needs three, which is the worst case for seven items. For a single lookup on a short list, walking the list can genuinely win.
Hint 1/4
The item is at the front, and this search does not start at the front. So the question is how many windows it takes to walk down to index 0.
Hint 2/4
Each turn prints the window and then moves one end past mid. The target is below L[mid] every time, so last is the end that moves.
Hint 3/4
Start with first 0 and last 6. The list is [12, 25, 33, 41, 58, 64, 77], so L[3] is 41 and L[1] is 25.
Hint 4/4
Three windows, then the returned index on a fourth line.
Independent check on the number of turns: seven items can be halved twice to get below one, so the worst case is three comparisons, and three windows were printed.
A window of one item is still a window. Every off by one bug in this algorithm is about whether that last window gets looked at.
⚠ last starting at len(L) instead of len(L) - 1
len(L) is the number people say out loud, and range(len(L)) is written that way everywhere else. Here the name holds an index, not a count.
wrong$$\texttt{last = len(L)}$$
right$$\texttt{last = len(L) - 1}$$
⚠ while first < last, without the equals sign
It reads as a sensible guard, and it even works for most targets, so a quick test passes. It skips the case where the window has shrunk to exactly one item.
wrong$$\texttt{while first < last:}$$
right$$\texttt{while first <= last:}$$
⚠ Moving a window end to mid rather than past it
last = mid looks tighter and safer than last = mid - 1, as though it were the cautious choice. The item at mid has already been compared and cannot be the answer, so keeping it means the window can stop shrinking.
10.4What each search costs: one, half the list, the whole list, or the number of halvings
Linear search costs the list length at worst and bisection search costs the number of times the list can be halved.
Both searches are written. Before the sorts, put a price on them, because the price is what the exam asks about and it is what decides which one to use.
TheoremResult 10.4: the cost of the two searches
Conditions
Counted in comparisons of one list item against the target. Every other step in both loops happens a fixed number of times per comparison, so the comparison count and the step count grow together.
n is the length of the list. The for linear search assumes the target is present and equally likely to be at any position.
Walking a list costs, at worst, one look per item, and on average half of that. Halving a list costs one look per halving, and a list can only be halved as many times as the power of two that reaches its length. That is why a hundred thousand items cost a hundred thousand looks one way and seventeen the other.
Both axes step by a factor of ten, which is the only way the two lines fit on one picture. Multiplying the list length by ten multiplies linear search's work by ten and adds about three comparisons to bisection search's. The dots are measured counts, not the formula.
Looks like this, but is not
It is tempting to read O(log n) beats O(n) as bisection search is faster. Here is a case where it is not. The list has seven sorted items and the target is the first one.
Linear search compares L[0] with the target and returns: one comparison. Bisection search starts in the middle at index 3 and has to walk down, which the check above measured at three comparisons.
O describes how the cost grows, not what the cost is for one particular list and one particular target. For small n, and for targets near the front, linear search can win. The two lines in the figure would cross somewhere on the left if the plot went low enough.
n
linear search, worst
bisection search, worst
halvings of n
10
10
4
3
100
100
7
6
1000
1000
10
9
10000
10000
14
13
100000
100000
17
16
Read the last two columns together: the measured worst case is always the number of halvings plus one, which is the count the derivation in the box predicted. Read the first two columns together: ten times the data costs ten times the work one way and three more comparisons the other. Between the first row and the last, the list grew ten thousand fold and bisection search's bill went from 4 to 17.
How many comparisons for the 20000 codes in the hook
The till program at the top of the page held 20000 sorted codes and spent 79994 comparisons on four lookups. Work out what bisection search would spend, from the halving count rather than from a library.
# How many times can 20000 be halved before only one item is left?
size = 20000
halvings = 0while size > 1:
size = size // 2
halvings += 1print('halvings:', halvings)
print('2 to that power:', 2 ** halvings)
Sample Run:
halvings: 142 to that power: 16384
And the same four lookups done by bisection search, counted the same way the hook counted its own.
# Same list, same four codes, but each lookup throws away half the list.
codes = list(range(0, 40000, 2))
wanted = [39998, 39996, 39994, 39992]
comparisons = 0for w in wanted:
first = 0
last = len(codes) - 1while first <= last:
mid = (first + last) // 2
comparisons += 1if codes[mid] == w:
breakelif codes[mid] > w:
last = mid - 1else:
first = mid + 1print('comparisons for the same 4 lookups:', comparisons)
Sample Run:
comparisons for the same 4 lookups: 56
FindThe worst case comparisons per lookup, and the total for four.
Given
The list holds 20000 codes, sorted ascending.
Four lookups are made, all near the end of the list.
Linear search spent 79994 comparisons on them.
Solution
Count the halvings
$$20000 \to 10000 \to 5000 \to \cdots \to 1$$
Whole division each time, so the count is exact rather than fractional. A loop does this in three lines and needs no logarithm function.
$$14 \text{ halvings},\quad 2^{14} = 16384$$
16384 is under 20000 and 2 to the 15 is 32768, which is over, so 14 is right and log base 2 of 20000 lies between 14 and 15.
Turn halvings into comparisons
$$14 + 1 = 15 \text{ comparisons at worst}$$
The plus one is the last window, which holds a single item and still has to be compared.
$$4 \times 15 = 60 \text{ at worst}$$
Four independent lookups, so the worst cases simply add.
Compare with what the linear version paid
$$79994 / 56 \approx 1428$$
The measured bisection total is 56 rather than the worst case 60, because three of the four targets were found before the window closed. Either way the linear program did over fourteen hundred times the work.
Answer $$\boxed{15 \text{ per lookup at worst},\ 56 \text{ measured for four}}$$
Check
Independent check from the other direction: the largest list a 15 comparison bisection search can handle is 2 to the 15 minus 1, which is 32767, and 20000 is under that.
Both programs read the same list in the same order with the same list operations. The only difference is which index is looked at next.
The claim in the hook, fewer than sixty comparisons, was not a guess: it is four times the halving count plus one, and the measured run came in at 56.
Checkpoint
§10.4 — the cost of a doubled list
Thirty seconds. A sorted list of 1000 names is searched by bisection search, and the measured worst case is 10 comparisons. The list then grows to 2000 names, still sorted.
Find(a) What is the worst case now?
Given
The measured worst case for 1000 items is 10 comparisons.
The list doubles in length and stays sorted.
The same bisection search is used.
Hint 1/4
The question is how many extra halvings a doubled list needs, not how much longer the list is.
Hint 2/4
The worst case is the number of halvings plus one, and doubling n adds exactly one halving, because 2 times n halved once is n again.
Hint 3/4
The old list was 1000 items and cost 10. The new one is 2000 items, which halves once down to 1000.
Hint 4/4
One more comparison than before.
Show solution
Relate the two windows
$$2000 \xrightarrow{\text{1 comparison}} 1000$$
One comparison on the 2000 item window discards half of it, leaving 1000 items.
$$10 + 1 = 11$$
The rest of the work is exactly the 1000 item problem, whose cost is given as 10.
Answer $$\boxed{11}$$
Check
Independent check against the measured table: 1000 items cost 10 and 10000 items cost 14.
Multiply the data by a constant and an O(log n) cost goes up by a constant. Multiply it by a constant and an O(n) cost is multiplied by that constant.
⚠ Writing O(n/2) or O(2n)
The measured average really is about n over 2, so writing it down feels more precise. O has already thrown constants away, so the extra precision is not extra information, it is a different notation.
wrong$$O(n/2)\;\text{or}\;O(2n)$$
right$$O(n)$$
⚠ Calling the whole task O(log n) when the list had to be sorted first
The search really is O(log n), and the sentence use bisection search, it is O(log n) is true about the search. The sort that made it legal is O(n log n) and belongs in the same bill.
⚠ Counting the lines of the loop body instead of the turns
The body of the bisection loop has four or five lines, so a count of 5 log n appears. It is not wrong arithmetic, it is a constant multiple, and reporting it as the complexity mixes a step count with a growth class.
wrong$$O(5\log n)$$
right$$O(\log n)$$
10.5Bubble sort and selection sort: swapping the list into order where it lies
Two sorts that rearrange the list you handed in and return nothing, both paying about n squared over 2 comparisons.
Bisection search needs a sorted list, and so far we have only ever been handed one. Here is the first way of making one, and the price of it.
MethodRule 10.5: the two in place sorts
Conditions
Both change the list they are given and return nothing. Calling one of them on the right of an assignment stores None.
Both need only the less than or greater than sign on the items, so both work on numbers, on text, and on objects of a class that defines the comparison.
Bubble sort's inner range must be len(data) - j - 1. The minus one is because the loop compares data[k] with data[k + 1], and the minus j is because the last j items are already in place.
$$\boxed{\begin{aligned}&\textbf{bubble}&&\text{pass over the list, swap any neighbours out of order};\\& &&\text{each pass carries one largest item to the end}\\&\textbf{selection}&&\text{find the smallest of what is left, put it at the front};\\& &&\text{repeat with the front one place further along}\\&\textbf{cost}&&\tfrac{n(n-1)}{2}\ \text{comparisons} = O(n^{2})\end{aligned}}$$
Bubble sort keeps walking the list and swapping any two neighbours that are in the wrong order; after one walk the largest item cannot be anywhere but the end, after two walks the two largest are at the end, and so on. Selection sort works from the other end: it scans for the smallest item, puts it first, and then does the same for the rest of the list. Both compare about half of every pair of items, which is n times n minus one over two.
Proof
Where n times n minus one over two comes from. Bubble sort's first pass makes n minus 1 comparisons, the second n minus 2, and so on down to 1 on the last pass.
So the total is the sum of 1 up to n minus 1, which is n times n minus one over two. For n = 5 that is 10, and the reversed five item list measured in the table below took exactly 10 comparisons.
Selection sort makes the same comparisons in a different order: its first scan looks at n items, the second at n minus 1, down to 1, which is the same sum plus n.
Both are therefore O(n squared), and the factor of one half is dropped because O drops constants.
One pass of bubble sort. The 90 does all the travelling: it is swapped three times in a row and ends at the last position, which is where the name comes from. The rest of the list is still out of order, which is why one pass is not a sort.
Looks like this, but is not
One pass swaps every neighbour that is out of order, so it is tempting to think the list comes out sorted. Here is one pass, and then a second one on the result.
defone_pass(data):
"""Walks the whole list once, swapping neighbours out of order."""for k inrange(len(data) - 1):
if data[k] > data[k + 1]:
data[k], data[k + 1] = data[k + 1], data[k]
return data
queue = [7, 3, 9, 1, 5]
print(one_pass(queue))
print(one_pass(queue))
Run:
[3, 7, 1, 5, 9]
[3, 1, 5, 7, 9]
After the first pass the list is [3, 7, 1, 5, 9], and 1 is still behind 7. A value can only move left one place per pass, because once the walk has gone past a position it never comes back. The 9 moved three places right in a single pass; the 1 needs as many passes as places it has to travel left.
version
swaps
final list
the course's version, swaps at every smaller item
10
[1, 2, 3, 4, 5]
find the smallest first, then swap once
2
[1, 2, 3, 4, 5]
Both sort correctly, and the course's version does five times the swaps. The comparison count is the same for both, so both are still O(n squared), and this is a constant factor rather than a class. Worth knowing because the lecture's version is the one to reproduce in an exam and the other is the one to reach for when a swap is expensive.
bubble_sort with the already sorted flag, and the passes it actually makes
Write bubble sort the way the lecture writes it, with a flag that stops the outer loop early when a pass makes no swaps, and print the list after every pass so the outer loop's work is visible.
defbubble_sort(data):
"""Prints the list after every pass."""
issorted = False
j = 0while j < len(data) - 1andnot issorted:
issorted = Truefor k inrange(len(data) - j - 1):
if data[k] > data[k + 1]:
issorted = False
temp = data[k]
data[k] = data[k + 1]
data[k + 1] = temp
print('after pass', j + 1, data)
j = j + 1
marks = [55, 90, 40, 72, 61]
bubble_sort(marks)
Sample Run:
after pass1 [55, 40, 72, 61, 90]
after pass2 [40, 55, 61, 72, 90]
after pass3 [40, 55, 61, 72, 90]
And the point of the flag: the number of passes on a list that is already sorted against a list that is in reverse order.
defbubble_sort(data):
"""Counts the passes it actually makes."""
issorted = False
j = 0while j < len(data) - 1andnot issorted:
issorted = Truefor k inrange(len(data) - j - 1):
if data[k] > data[k + 1]:
issorted = False
temp = data[k]
data[k] = data[k + 1]
data[k + 1] = temp
j = j + 1return j
print('passes on a sorted list: ', bubble_sort([40, 55, 61, 72, 90]))
print('passes on a shuffled one:', bubble_sort([90, 72, 61, 55, 40]))
Sample Run:
passes on a sortedlist: 1
passes on a shuffled one: 4
FindThe sorted list, the number of passes, and what the flag saves.
Given
The list is [55, 90, 40, 72, 61].
The outer loop runs while j < len(data) - 1 and not issorted.
issorted is set to True at the start of each pass and back to False by any swap.
Solution
Write the inner pass
$$\texttt{for k in range(len(data) - j - 1):}$$
Two corrections in one expression: minus 1 because the body reads data[k + 1], and minus j because the last j positions already hold the j largest items and comparing them again is wasted work.
$$\texttt{if data[k] > data[k + 1]:}$$
Strictly greater, so equal neighbours are left alone. Using >= instead would swap equal items and the flag would never settle on a list with duplicates.
$$\texttt{temp = data[k]}$$
The three line swap needs the temporary because the first assignment destroys the value it is about to need. The next box down shows what happens without it.
Write the outer loop and the flag
$$\texttt{issorted = True}\ \text{at the top of each pass}$$
Optimism: assume this pass will find nothing to do, and let any swap contradict it. Setting it once before the outer loop instead would stop the sort after the first pass.
$$\texttt{while j < len(data) - 1 and not issorted:}$$
At most n minus 1 passes are ever needed, because each pass settles one more position, and the flag cuts it short when the list is already in order.
Read the passes
$$\text{pass }1 \to [55, 40, 72, 61, 90]$$
90 travelled to the end, matching the figure.
$$\text{pass }2 \to [40, 55, 61, 72, 90]$$
Sorted already, but the loop cannot know that until a pass makes no swaps.
$$\text{pass }3:\ \text{no swaps, flag stays True}$$
The third pass is the price of knowing, and the flag stops a fourth. On the already sorted list this single pass is the whole cost, measured as 1 pass against 4.
Independent check on the flag rather than on the sort: an already sorted list takes 1 pass and its reverse takes 4, and 4 is len minus 1 for a five item list, which is the most the outer loop can ever run.
Ten comparisons for five items in the worst case, which is 5 times 4 over 2. The flag improves the best case to n minus 1 comparisons and leaves the worst case alone.
The flag makes bubble sort O(n) on an already sorted list and leaves it O(n squared) everywhere else. When an exam asks for the complexity of bubble sort the answer is O(n squared), because that is the worst case.
selection_sort, and why the lecture's version swaps more than it needs to
Write selection sort the way the lecture writes it: walk the unsorted tail and swap whenever you meet something smaller than the item at the front of that tail, printing the list each time a position is settled.
defselection_sort(L):
"""Prints the list every time the front of the unsorted part is fixed."""
suffix_start = 0while suffix_start != len(L):
for i inrange(suffix_start, len(L)):
if L[i] < L[suffix_start]:
L[suffix_start], L[i] = L[i], L[suffix_start]
print('position', suffix_start, 'settled:', L)
suffix_start += 1
marks = [55, 90, 40, 72, 61]
selection_sort(marks)
Sample Run:
position 0 settled: [40, 90, 55, 72, 61]
position 1 settled: [40, 55, 90, 72, 61]
position 2 settled: [40, 55, 61, 90, 72]
position 3 settled: [40, 55, 61, 72, 90]
position 4 settled: [40, 55, 61, 72, 90]
The lecture's version swaps on every smaller item it meets. The textbook description says find the smallest, then swap once. Both sort; count the swaps.
defselection_sort_swaps(L):
"""The course version: swaps whenever it meets a smaller item. Returns how many swaps it did."""
swaps = 0
suffix_start = 0while suffix_start != len(L):
for i inrange(suffix_start, len(L)):
if L[i] < L[suffix_start]:
L[suffix_start], L[i] = L[i], L[suffix_start]
swaps += 1
suffix_start += 1return swaps
deffind_min_then_swap(L):
"""Finds the smallest item first and swaps once per position. Returns how many swaps it did."""
swaps = 0for start inrange(len(L)):
smallest = start
for i inrange(start + 1, len(L)):
if L[i] < L[smallest]:
smallest = i
if smallest != start:
L[start], L[smallest] = L[smallest], L[start]
swaps += 1return swaps
a = [5, 4, 3, 2, 1]
b = [5, 4, 3, 2, 1]
print('course version swaps: ', selection_sort_swaps(a), a)
print('find min then swap: ', find_min_then_swap(b), b)
Sample Run:
course version swaps: 10 [1, 2, 3, 4, 5]
find min then swap: 2 [1, 2, 3, 4, 5]
FindThe sorted list, what each printed row means, and the swap counts of the two versions.
Given
The list is [55, 90, 40, 72, 61].
suffix_start is the index of the first unsorted item.
The swap is written with the two names on one line, L[a], L[b] = L[b], L[a].
Solution
Fix one position at a time
$$\texttt{while suffix\_start != len(L):}$$
The name says what the variable is: the start of the part not yet settled. When it reaches the length there is nothing left.
$$\texttt{for i in range(suffix\_start, len(L)):}$$
The inner scan starts at the front of the unsorted tail, not at 0, because everything before it is already smaller than everything in it.
$$\texttt{if L[i] < L[suffix\_start]: swap}$$
The item at the front of the tail is the current best guess at the smallest, and every swap improves the guess.
Read the settled rows
$$\text{position }0 \to [40, 90, 55, 72, 61]$$
40 is the smallest of all five and is now first. Notice 55 and 90 have traded places as a side effect of the swaps.
$$\text{position }3 \to [40, 55, 61, 72, 90]$$
Sorted after four positions, and the fifth pass runs anyway because the loop is counting positions and not checking for order.
Count the swaps of both versions
$$10 \text{ against } 2 \text{ on }[5,4,3,2,1]$$
The lecture's version swaps every time it meets a smaller item, which on a reversed list is every time. Finding the smallest first and swapping once per position costs at most n swaps.
Independent check that both versions agree on the answer and not just on the count: both printed [1, 2, 3, 4, 5] from the same reversed input, so the cheaper one is not cheaper by being wrong.
Selection sort makes its n times n minus one over two comparisons whatever the list looks like. There is no flag and no early exit, so best case and worst case are the same, unlike bubble sort.
Bubble sort can notice that it is finished; selection sort cannot. That is the one difference an exam question can hang on, since their complexity class is identical.
Checkpoint
§10.5 — how many passes on a nearly sorted list
Thirty seconds. Bubble sort with the flag, printing the list after every pass and returning the number of passes it made.
defbubble_sort(data):
"""Prints the list after every pass and returns the number of passes."""
issorted = False
j = 0while j < len(data) - 1andnot issorted:
issorted = Truefor k inrange(len(data) - j - 1):
if data[k] > data[k + 1]:
issorted = False
data[k], data[k + 1] = data[k + 1], data[k]
print(data)
j = j + 1return j
passes = bubble_sort([2, 1, 4, 3])
print('passes', passes)
Find(a) Write every printed line, including the passes count.
Given
The list is [2, 1, 4, 3].
issorted is set True at the top of each pass and cleared by any swap.
The outer loop runs while j < len(data) - 1 and not issorted.
IPython console
In [1]: %run untitled0.py
The first pass sorts the list and the second pass is the one that proves it. Both printed lines are identical, and that repetition is exactly what the flag detects. The cost of knowing you are finished is one full pass.
Hint 1/4
Two questions: what the list looks like after each pass, and how many passes the loop makes before the flag stops it.
Hint 2/4
One pass swaps every neighbour out of order. The flag can only stop the loop after a pass in which nothing was swapped, so a sorted list still costs one extra pass.
Hint 3/4
The list [2, 1, 4, 3] has two swaps to make and they are not neighbours of each other, so one pass fixes both.
Hint 4/4
Two printed lists and then the number 2.
Show solution
Pass 1
$$2 > 1 \Rightarrow [1, 2, 4, 3]$$
First pair out of order, so it is swapped.
$$2 < 4 \Rightarrow \text{leave}$$
Nothing to do in the middle.
$$4 > 3 \Rightarrow [1, 2, 3, 4]$$
Last pair swapped. The pass made two swaps, so the flag is False and the loop goes round again.
Independent check on the count: the outer loop can run at most len(data) - 1 which is 3 times, and it ran 2, so the flag really did cut it short rather than the bound doing it.
Any bubble sort question that asks for a number of passes wants one more pass than the number that did work, unless the last needed swap happens on the very last allowed pass.
⚠ Swapping without a temporary name
The two assignments look symmetric and the temporary looks like clutter. The first assignment overwrites the value the second one needs, so the list ends up with duplicates and no error is raised.
range(len(data) - j) looks like the right number of items to visit, and it is. The loop body reads one place past k, so the last turn reads one place past the end.
wrong$$\texttt{for k in range(len(data) - j):}$$
right$$\texttt{for k in range(len(data) - j - 1):}$$
⚠ Setting the flag once, outside the outer loop
It is declared before the loop like any other variable, so initialising it there feels natural. It has to be reset at the top of every pass, because it is a claim about that pass and not about the sort.
wrong$$\texttt{issorted = False}\;\text{once, before the while}$$
right$$\texttt{issorted = True}\;\text{as the first line inside the while}$$
10.6Merge sort: split, sort each half the same way, merge the two sorted halves
Split the list, sort both halves by the same function, and build one sorted list by taking the smaller front item repeatedly.
The two swapping sorts pay n squared, which on ten thousand items is a hundred million comparisons. Here is the sort that does the same job in about a hundred and thirty thousand, and it needs the recursion from the first box on this page.
RuleRule 10.6: merge sort and its merge step
Conditions
Merge assumes both of the lists it is given are already sorted. Handed unsorted lists it returns a list that is not sorted, and raises nothing.
Merge sort returns a new list and never changes the list it was given. The base case returns L[:] and not L, so even a one item list comes back as a copy.
The two halves must be L[:middle] and L[middle:] with the same middle. Any other pair either loses an item or keeps one twice.
$$\boxed{\begin{aligned}&\textbf{merge sort}&&\texttt{len(L) < 2} \Rightarrow \texttt{return L[:]}\\& &&\texttt{middle = len(L) // 2}\\& &&\texttt{return merge(sort(L[:middle]), sort(L[middle:]))}\\&\textbf{merge}&&\text{take the smaller of the two front items, repeatedly};\\& &&\text{then append whatever is left of the other list}\\&\textbf{cost}&&n\log_{2} n \text{ comparisons} = O(n\log n)\end{aligned}}$$
A list of one item is already sorted, so hand back a copy of it. Otherwise cut the list in half, sort each half by asking this same function, and then walk the two sorted halves side by side, always taking whichever front item is smaller, until one of them runs out; then pour the rest of the other one on the end.
Proof
Where n log n comes from. The splitting stops when the pieces hold one item, and each split halves the size, so the number of levels is the number of times n can be halved, which is log base 2 of n.
At every level, merging touches each of the n items exactly once: the pieces at one level together hold the whole list, and merging two pieces of total size m costs at most m comparisons.
So the total is about n comparisons per level times log n levels, which is n log n. The lecture puts it in numbers: for a list of 10000 the selection sort figure is 100 million and the merge sort figure about 130000. Those are n squared and n log base 2 of n, since 10000 times 13.29 is 132877; the actual comparison counts are about half of each, because both formulas ignore the factor of one half.
Checked against a run rather than left as algebra: on a reversed list of 1024 items the measured counts were 523776 for bubble sort and 5120 for merge sort. The bubble figure is 1024 times 1023 over 2 exactly. The merge figure is half of 1024 times 10, and the halving is real rather than sloppiness: on a reversed list every merge empties one side after exactly half of its comparisons and drains the other side for free.
Four items, two levels of splitting, two levels of merging. Nothing is compared on the way down; the splitting is pure bookkeeping. Every comparison happens in the merge steps on the way back up, and each level of merging looks at all four items once.
Looks like this, but is not
A merge that looks finished. The loop walks both lists, takes the smaller front item each time, and stops when one list runs out.
defmerge(left, right, compare):
"""Broken: the two drain loops at the end are missing."""
result = []
i = 0
j = 0while i < len(left) and j < len(right):
if compare(left[i], right[j]):
result.append(left[i])
i += 1else:
result.append(right[j])
j += 1return result
defbefore(a, b):
return a < b
print(merge([40, 61, 90], [55, 72], before))
Run:
[40, 55, 61, 72]
Five items went in and four came out. The loop condition is i < len(left) and j < len(right), so the moment either side is exhausted the loop ends, and whatever is still in the other side is simply never appended. Here the right list ran out after 55 and 72, and the 90 left in the left list was dropped.
n
bubble sort
merge sort
ratio
8
28
12
2.3
64
2016
192
10.5
256
32640
1024
31.9
1024
523776
5120
102.3
The ratio in the last column is the whole argument for merge sort, and it is not a constant: it grows with the list. At 8 items merge sort saves a bit more than half the work and at 1024 items it saves ninety nine per cent of it. The bubble column is exactly n times n minus one over two every time, which confirms the counter is counting comparisons.
merge of [40, 61, 90] and [55, 72] into one sorted list
Write the merge step on its own, before any recursion. Two sorted lists go in, one sorted list comes out, and neither input is changed. The comparison is passed in as a function so that the same merge can be used for ascending and descending order.
defmerge(left, right, compare):
"""Assumes left and right are sorted lists and compare(a, b) is True when a belongs before b. Returns a NEW sorted list with every item of both."""
result = []
i = 0
j = 0while i < len(left) and j < len(right):
if compare(left[i], right[j]):
result.append(left[i])
i += 1else:
result.append(right[j])
j += 1while i < len(left):
result.append(left[i])
i += 1while j < len(right):
result.append(right[j])
j += 1return result
defbefore(a, b):
"""Returns True when a belongs before b in ascending order."""return a < b
left = [40, 61, 90]
right = [55, 72]
print(merge(left, right, before))
print('left is untouched: ', left)
print('right is untouched:', right)
Sample Run:
[40, 55, 61, 72, 90]
left is untouched: [40, 61, 90]
right is untouched: [55, 72]
FindThe merged list, and why two loops follow the first one.
Given
left is [40, 61, 90] and right is [55, 72], both sorted ascending.
compare(a, b) is True when a belongs before b.
i walks the left list and j walks the right one.
Solution
Take the smaller front item, repeatedly
$$\texttt{while i < len(left) and j < len(right):}$$
This loop is only safe while both lists still have a front item, because the body reads left[i] and right[j].
$$40 < 55 \Rightarrow \texttt{result = [40]},\ i = 1$$
Only the index of the side that gave an item moves. Moving both would skip an item on the other side.
Does nothing on this data and is still required: with [55, 72] and [40, 61] the other side would be the one left over. Exactly one of the two loops ever runs.
Answer $$\boxed{[40, 55, 61, 72, 90]}$$
Check
Two independent checks. Lengths: 3 plus 2 is 5 and the result has 5 items, so nothing was lost or doubled. Inputs: both printed unchanged afterwards, so the result really is a new list.
Four comparisons for five items. A merge of two lists holding m items together costs at most m minus 1 comparisons, because every comparison places one item and the last item needs none.
The two drain loops are not tidying up, they are part of the algorithm. Any merge you write from memory should be checked by counting the items in and the items out.
merge_sort around that merge, with the comparison as a default parameter
Now the recursion. Two lines do the splitting and one line does the merging. The comparison has a default value so that the usual call takes one argument, and a caller who wants the other order can pass a different comparison.
defmerge(left, right, compare):
"""Assumes left and right are sorted. Returns a new sorted list."""
result = []
i = 0
j = 0while i < len(left) and j < len(right):
if compare(left[i], right[j]):
result.append(left[i])
i += 1else:
result.append(right[j])
j += 1while i < len(left):
result.append(left[i])
i += 1while j < len(right):
result.append(right[j])
j += 1return result
defmerge_sort(L, compare=lambda x, y: x < y):
"""Assumes L is a list. Returns a NEW sorted list; L is not changed."""iflen(L) < 2:
return L[:]
middle = len(L) // 2
left = merge_sort(L[:middle], compare)
right = merge_sort(L[middle:], compare)
return merge(left, right, compare)
marks = [55, 90, 40, 72, 61, 18]
result = merge_sort(marks)
print('sorted copy: ', result)
print('original: ', marks)
print('descending: ', merge_sort(marks, lambda x, y: x > y))
The same function with the splits and merges printed, which is the picture above in text form.
defmerge(left, right):
"""Assumes left and right are sorted ascending. Returns a new sorted list."""
result = []
i = 0
j = 0while i < len(left) and j < len(right):
if left[i] < right[j]:
result.append(left[i])
i += 1else:
result.append(right[j])
j += 1while i < len(left):
result.append(left[i])
i += 1while j < len(right):
result.append(right[j])
j += 1return result
defmerge_sort(L, depth):
"""Prints the split and the merge at every depth."""iflen(L) < 2:
print(' ' * depth + 'base ' + str(L))
return L[:]
middle = len(L) // 2print(' ' * depth + 'split ' + str(L))
left = merge_sort(L[:middle], depth + 1)
right = merge_sort(L[middle:], depth + 1)
merged = merge(left, right)
print(' ' * depth + 'merge ' + str(left) + ' and ' + str(right)
+ ' into ' + str(merged))
return merged
merge_sort([55, 90, 40, 72], 0)
Sample Run:
split [55, 90, 40, 72]
split [55, 90]
base [55]
base [90]
merge [55] and [90] into [55, 90]
split [40, 72]
base [40]
base [72]
merge [40] and [72] into [40, 72]
merge [55, 90] and [40, 72] into [40, 55, 72, 90]
FindThe sorted copy, the state of the original, and what the second call with a different comparison gives.
Given
marks is [55, 90, 40, 72, 61, 18], six items in no particular order.
The default comparison is lambda x, y: x < y, which is the one line form of def before(x, y): return x < y.
merge is the function from the previous example.
Solution
Write the base case as a copy
$$\texttt{if len(L) < 2: return L[:]}$$
len(L) < 2 covers both the empty list and the one item list, so a list of any length reaches it. L[:] and not L, so that the caller can never end up with a second name for its own list.
Split and recurse
$$\texttt{middle = len(L) // 2}$$
Whole division, so middle is an int usable as a slice bound. For six items it is 3.
$$\texttt{L[:middle]},\ \texttt{L[middle:]}$$
The same middle in both, once as a stop and once as a start, which is what makes the two pieces fit together with nothing lost and nothing duplicated.
$$\texttt{merge\_sort(L[:middle], compare)}$$
The comparison is passed down, otherwise the deeper calls would fall back on the default and half the list would be sorted the other way.
Read the three printed lines
$$\text{sorted copy } [18, 40, 55, 61, 72, 90]$$
The default comparison gives ascending order.
$$\text{original still } [55, 90, 40, 72, 61, 18]$$
Nothing was assigned into marks, and every piece the recursion touched was a slice, which is a new list.
$$\texttt{lambda x, y: x > y} \Rightarrow \text{descending}$$
The only change is which of the two front items counts as belonging first, so the same merge produces the reverse order.
Independent check against the built in: sorted(marks) gives the same six numbers in the same order, and sorted(marks, reverse=True) gives the descending one.
Eleven comparisons for six items. The same list was run through the two in place sorts with counters: bubble sort made 15 and the course's selection sort made 21.
The printed tree is worth reading twice. Every split line appears before its two children and every merge line after them, which is the same top down then bottom up pattern as the factorial stack.
Checkpoint
§10.6 — a merge with ties in it
Thirty seconds. Two sorted lists, both containing a 2, merged by the function from this block.
defmerge(left, right):
"""Assumes both lists are sorted ascending. Returns one sorted list."""
out = []
i = 0
j = 0while i < len(left) and j < len(right):
if left[i] < right[j]:
out.append(left[i])
i += 1else:
out.append(right[j])
j += 1while i < len(left):
out.append(left[i])
i += 1while j < len(right):
out.append(right[j])
j += 1return out
a = [2, 2, 9]
b = [1, 2, 4]
print(merge(a, b))
print(len(merge(a, b)))
Find(a) Write both lines of output.
Given
a is [2, 2, 9] and b is [1, 2, 4], both sorted ascending.
The comparison inside the loop is left[i] < right[j], strictly less than.
The second print asks for the length of the result.
IPython console
In [1]: %run untitled0.py
Three values equal to 2 come out in a row and the tie rule decided their order: because the test is strictly less than, an equal front pair sends the right list's item first. Change the test to less than or equal and the same six numbers come out in the same order on the screen, but the 2 from the left list would now be the first of the three. That is invisible for numbers and very visible for objects sorted by one field.
Hint 1/4
Two questions: the order of the six items, and how many items come out.
Hint 2/4
The test is strictly less than, so when the two front items are equal it is False and the item from the right list is taken first.
Hint 3/4
The lists are [2, 2, 9] and [1, 2, 4]. The 1 goes first, then the two front items are both 2.
Hint 4/4
Six items on the first line and the number 6 on the second.
Show solution
Walk both fronts
$$2 < 1?\ \text{no} \Rightarrow \text{take } 1$$
The right front is smaller, so j moves and i stays.
$$2 < 2?\ \text{no} \Rightarrow \text{take the right } 2$$
Strictly less than is False on a tie, so the else branch runs and the right list's item is appended.
$$2 < 4?\ \text{yes} \Rightarrow \text{take both left } 2\text{s}$$
Two turns in a row take from the left, because both of its 2s are below the right's 4.
The first drain loop runs. Without it the answer would have five items.
$$3 + 3 = 6$$
Nothing is created or lost by a merge, so the length is the sum of the two inputs.
Answer $$\boxed{[1, 2, 2, 2, 4, 9]\;/\;6}$$
Check
Independent check by sorting the concatenation: the six values 2, 2, 9, 1, 2, 4 sorted ascending are 1, 2, 2, 2, 4, 9, which is what merge returned.
Whenever you write a merge, count the items in and the items out before you look at the order. A missing drain loop shows up in the count immediately.
⚠ Stopping the merge when one side runs out
The main loop looks like the whole algorithm, and it is the part that does the comparing. The leftovers need no comparisons at all, which is exactly why they are easy to forget.
wrong$$\texttt{while i < len(left) and j < len(right)}\;\text{only}$$
right$$\text{then }\texttt{while i < len(left)}\;\text{and }\texttt{while j < len(right)}$$
⚠ Expecting merge sort to change the list it was given
Bubble sort and selection sort were written two boxes earlier and both work in place, so the habit is fresh. Merge sort cannot work in place: it builds result as a new list.
⚠ Splitting with middle in one half and middle plus one in the other
Bisection search moves its window ends past mid, because there the item at mid has been dealt with. In merge sort nothing has been dealt with, so the item at middle still has to be in one of the halves.
10.7Paying for a sort once: when it is worth it, and when a dictionary makes it unnecessary
Sorting costs more than a single search, so it pays only when many searches follow, and sometimes nothing needs sorting.
Every box so far has priced one operation. The last question of the chapter is the one a program actually has to answer: given this list and this many lookups, is sorting worth it at all?
RuleRule 10.7: the break even point for sorting first
Conditions
k is the number of lookups the program will make and n is the length of the list. Both costs are counted in comparisons of list items.
The list must not change between the lookups. One insertion puts it back out of order, and then either the sort is paid again or the bisection search is no longer legal.
$$\boxed{\begin{aligned}&\text{leave it unsorted, } k \text{ linear searches:}&&k\,n\\&\text{sort once, then } k \text{ bisection searches:}&&n\log_{2} n + k(\log_{2} n + 1)\\&\text{sorting wins from about}&&k = \log_{2} n\end{aligned}}$$
Searching an unsorted list k times costs k times the list length. Sorting it costs about n log n once, and then each of the k searches costs only the number of halvings. Setting those two against each other, the sort has paid for itself by about the log n th lookup, which for twenty thousand items means from the fifteenth lookup onward.
The same five marks under the two ways of sorting. sorted leaves the first object alone and builds a second, so both orders exist afterwards. sort rearranges the one object, so the original order is gone, and its return value is None, which is the single most common way to lose a sorted list.
Looks like this, but is not
Sorting makes every later search nearly free, so sorting first sounds like it can only help. Here are the measured totals for 20000 items, sorting cost included.
deflinear_cost(n, lookups):
"""Comparisons for `lookups` failed linear searches in a list of n items."""return n * lookups
defbinary_cost(n, lookups):
"""Comparisons for `lookups` bisection searches, plus the sort that makes them possible. Merge sort does about n * log2(n) comparisons."""
halvings = 0
size = n
while size > 1:
size = size // 2
halvings += 1return n * halvings + (halvings + 1) * lookups
n = 20000for lookups in [1, 5, 20, 100, 1000]:
print(lookups, linear_cost(n, lookups), binary_cost(n, lookups))
Run, printing lookups, then the unsorted total, then the sorted total:
At one lookup the unsorted route costs 20000 and the sorted route costs 280015, fourteen times more. At five lookups it is still losing. A program that sorts a 20000 item list to answer one question has done fourteen times the work for nothing, and it will not look slow in testing because both numbers are small on a modern machine.
lookups
leave unsorted
sort first, then bisect
which wins
1
20000
280015
unsorted
5
100000
280075
unsorted
15
300000
280225
sorted, just
100
2000000
281500
sorted
1000
20000000
295000
sorted
The middle row is the break even point, found by a loop rather than by algebra: at 15 lookups the sorted route becomes the cheaper one and it never looks back. Notice how flat the third column is. Going from 1 lookup to 1000 raises it by five per cent, because almost all of it is the one sort.
Finding the break even lookup count for a 20000 item list
A program holds 20000 records and will look up k of them. Find the smallest k for which sorting first is cheaper, using the halving count and a loop rather than algebra.
# Where does paying for the sort start to pay off?
n = 20000
halvings = 0
size = n
while size > 1:
size = size // 2
halvings += 1
lookups = 1while n * lookups <= n * halvings + (halvings + 1) * lookups:
lookups += 1print('halvings:', halvings)
print('sorting pays off from lookup number', lookups)
Sample Run:
halvings: 14
sorting pays off from lookup number 15
FindThe smallest number of lookups at which sorting first wins.
Given
The list holds 20000 items and can be halved 14 times.
Merge sort on n items costs about n times the halving count, so about 280000 comparisons here.
Each bisection search costs 15 comparisons at worst, and each linear search 20000.
Solution
Write both totals as a cost in k
$$\text{unsorted} = 20000k$$
Each lookup is a separate full walk, and the walks do not help each other.
$$\text{sorted} = 280000 + 15k$$
The sort is paid once whatever k is, which is the whole point of the word amortise.
Find where the lines cross
$$20000k > 280000 + 15k$$
Ask when the unsorted route becomes the more expensive one; that is the moment sorting starts paying.
$$19985k > 280000$$
Subtracting 15k from both sides. The 15 barely matters, which is the mathematical version of the flat third column in the table.
$$k > 14.01 \Rightarrow k = 15$$
k counts lookups, so it must be a whole number, and the first whole number above 14.01 is 15. The loop in the program found the same 15 by trying k = 1, 2, 3 and so on.
Answer $$\boxed{k = 15}$$
Check
Independent check against the measured table: at 5 lookups the totals are 100000 and 280075, so unsorted wins; at 15 they are 300000 and 280225, so sorted wins.
The break even count came out at 15, and the halving count is 14. That is not a coincidence: the crossing point is about log base 2 of n whatever n is, because the sort costs n log n and each saved search saves about n.
Carry the shape rather than the number: sorting pays off after about log n lookups. For a list of a thousand that is ten lookups, for a million it is twenty, and for a list of ten it is basically never worth the trouble.
sort, sorted, sorting your own objects, and the lookup that needs no search
Three questions a lab answer has to get right. First, which of the two built in sorts copies.
marks = [55, 90, 40, 72, 61]
copy = sorted(marks)
print('sorted() gives back a list:', copy)
print('the original is untouched: ', marks)
answer = marks.sort()
print('sort() gives back: ', answer)
print('but the original is now: ', marks)
Sample Run:
sorted() gives back a list: [40, 55, 61, 72, 90]
the original is untouched: [55, 90, 40, 72, 61]
sort() gives back: None
but the original is now: [40, 55, 61, 72, 90]
Second, a list of objects. The sort code does not change at all; the only new thing is __lt__ in the class, which decides what the less than sign means and therefore what sorted means.
classTrack:
"""A song with a title and a length in seconds."""def__init__(self, title, seconds):
self.__title = title
self.__seconds = seconds
defget_title(self):
"""Returns the title."""returnself.__title
defget_seconds(self):
"""Returns the length in seconds."""returnself.__seconds
def__lt__(self, other):
"""A track is less than another if it is shorter. Ties go by title."""ifself.__seconds == other.get_seconds():
returnself.__title < other.get_title()
returnself.__seconds < other.get_seconds()
def__repr__(self):
"""Returns the title and the length."""returnself.__title + '(' + str(self.__seconds) + ')'
playlist = [Track('Oda', 210), Track('Bir', 185), Track('Cam', 210),
Track('Ada', 240)]
playlist.sort()
print(playlist)
Sample Run:
[Bir(185), Cam(210), Oda(210), Ada(240)]
Third, the case where no search is needed. A dictionary answers a lookup without walking anything, which is why sorting is sometimes the wrong question.
deflist_looks(names, wanted):
"""Returns how many items a walk of the list examines before it answers."""
looks = 0for i inrange(len(names)):
looks += 1if names[i] == wanted:
return looks
return looks
names = []
for i inrange(1000):
names.append('id' + str(i))
lookup = {}
for i inrange(1000):
lookup['id' + str(i)] = i
print('list walk for the last name:', list_looks(names, 'id999'))
print('list walk for a missing name:', list_looks(names, 'id9999'))
print('dictionary answer:', 'id999'in lookup, lookup['id999'])
print('dictionary answer for missing:', 'id9999'in lookup)
Sample Run:
list walk for the last name: 1000list walk for a missing name: 1000
dictionary answer: True999
dictionary answer for missing: False
FindWhich operations copy, what the tie rule does to Oda and Cam, and how many items the dictionary examines.
Given
The marks list is [55, 90, 40, 72, 61].
Track.__lt__ compares seconds first and titles when the seconds are equal.
The playlist holds Oda at 210, Bir at 185, Cam at 210 and Ada at 240 seconds.
The list and the dictionary both hold a thousand names built as 'id' + str(i).
Solution
Separate copying from rearranging
$$\texttt{sorted(marks)} \to \text{new list, original intact}$$
A function that returns a value and leaves its argument alone. Use it when the original order is still needed.
$$\texttt{marks.sort()} \to \texttt{None}$$
A method that rearranges and reports nothing. The printed None is the return value, not the list.
Read the tie in the object sort
$$\texttt{Bir}(185) < \texttt{Cam}(210)$$
Different lengths, so the first branch of __lt__ is skipped and the seconds decide.
$$\texttt{Cam}(210) < \texttt{Oda}(210)$$
Equal seconds, so the titles decide, and 'Cam' < 'Oda' is True. Without the tie branch the two would keep whichever order they started in, which is not what the specification asked for.
Compare a walk with a lookup
$$\text{list walk} \to 1000 \text{ looks}$$
Both for the last name and for a name that is not there, because a walk can only rule an item out by looking at it.
$$\texttt{'id999' in lookup} \to \texttt{True}$$
A dictionary computes where the key would be and looks only there, so the answer costs the same whether the dictionary holds ten keys or a million. That is the O(1) at the top of the list of complexity classes.
Independent check on the object sort against the definition rather than the output: the printed order 185, 210, 210, 240 is ascending in seconds, and inside the tie Cam comes before Oda alphabetically.
The dictionary version builds a thousand entry dictionary once, at a cost of one insertion per name, and then every lookup is free. It is the amortisation argument again with a different one time cost.
Before writing a search, ask what the lookups are keyed on. If it is one field and that field is unique, a dictionary beats both searches and needs no sort at all.
Checkpoint
§10.7 — one copy, then two changes
Thirty seconds. A copy is taken, then the original is appended to and sorted.
append adds 30 at the end, and then sort is called on sizes.
IPython console
In [1]: %run untitled0.py
The copy is frozen at the moment sorted ran and knows nothing about the 30. The second line shows the append landing at the end of an already sorted list, which puts it out of order again: this is the reason Rule 10.7 requires the list not to change between lookups. One append and the bisection search is no longer legal.
Hint 1/4
Three questions: what the copy holds, what the original holds after the append, and what it holds after the sort.
Hint 2/4
sorted builds a separate list at the moment it is called. Later changes to the original cannot reach it, because they are two objects.
Hint 3/4
At the moment of the copy the list is [38, 41, 36]. The append then puts 30 at the end of the original only.
Hint 4/4
The copy has three sorted items; the original has four, first unsorted then sorted.
Independent check that the two really are separate objects: the copy still has three items after the append, and a second name for one object could not have a different length from the first.
A sorted list stays sorted only until something is added. Any program that keeps a sorted list has to insert in the right place or sort again.
⚠ Sorting text that holds numbers
A file gives strings, and the strings look like numbers on the screen. Sorting them compares character by character, so 104 comes before 11 because 0 comes before 1.
right$$\texttt{int(item)}\;\text{for each item first, then }\texttt{sort}$$
⚠ Walking a list when the lookup key is unique
The data arrives as a list of records and a list is what you have, so a loop is the first thought. If every lookup is on one unique field, a dictionary built once turns every later lookup from n looks into one.
wrong$$\texttt{for i in range(len(names)): if names[i] == w}$$
right$$\texttt{if w in lookup: lookup[w]}$$
⚠ Searching a list with bisection after appending to it
The list was sorted and the search worked, so the search is trusted. An append goes on the end, and one item in the wrong place is enough to make the search return -1 for things that are there.
Any lab step or exam part that says find, or search, or in order, without naming an algorithm.
Count the lookups
One or two lookups on a list that is not already sorted: linear search, and do not sort. Many lookups: the sort is worth paying for from about the log n th lookup.
Ask what the lookups are keyed on
If every lookup is by one unique field, build a dictionary from that field once and stop thinking about searching. If the lookups are by range, or order, or the smallest few, sorting is the right tool.
Check whether the list is allowed to change
If the caller still needs the original order, use sorted or merge sort. If not, sort or one of the in place sorts is cheaper because it copies nothing.
Pick the sort by what the sheet says and how long the list is
If a specification names an algorithm, write that one; marks are for the named algorithm and not for a faster substitute. Otherwise: short list, bubble or selection; long list, merge sort; and in real code the built in sort.
Write the precondition in the docstring
Bisection search and merge both assume sorted input. Writing it down is what stops the next reader from calling them on an unsorted list.
Where it goes wrong
Sorting a list to answer one question, which on the measured 20000 item list is fourteen times the work of just walking it.
Substituting the built in sort where a sheet asked for bubble sort. The output is right and the marks are for the algorithm.
Using bisection search on a list that is sorted by a different field from the one being searched.
Tracing a search or a sort loop on paper
Exam questions that give a short program and ask what it prints, which on the two real papers checked for this page were worth 30 and 20 marks.
Write the variable names as column headings
For a search: first, last, mid, L[mid]. For a sort: the list itself, plus the loop counters. One row per turn of the loop.
Write the list once, with the indices above it
Almost every mistake in these questions is reading the wrong index. Writing 0 to n minus 1 above the values, once, removes most of them.
Fill one row per turn and never skip ahead
Compute the new values from the row above, not from your idea of where the algorithm is going. A loop that is about to end looks exactly like one that is not.
Mark where each print happens
Put the printed text in its own column as you go. The answer asked for is the print column read downwards, not the final value of the variables.
Check the loop's exit before writing the answer
For a while loop, state the condition that became False. For a for over range, check the last value the counter actually took, which is one less than the stop.
Where it goes wrong
Giving the final value of the variables instead of the printed lines.
Losing a blank line or a trailing space. The exam asks for the exact output, and print with several arguments puts one space between them.
Tracing the algorithm you remember instead of the code on the paper, which is how a planted off by one goes unnoticed.
linear search on fifteen sorted ids, counted
The same sorted list of fifteen ids, searched by walking it. The counter reports how many items were looked at.
deflinear_search(L, e):
"""Assumes L is a list. Returns (index, comparisons), index -1 if absent."""
comparisons = 0for i inrange(len(L)):
comparisons += 1if L[i] == e:
return i, comparisons
return -1, comparisons
ids = [102, 118, 125, 137, 140, 156, 171, 188, 190,
204, 211, 226, 233, 245, 260]
print('looking for 245:', linear_search(ids, 245))
print('looking for 250:', linear_search(ids, 250))
Sample Run:
looking for245: (13, 14)
looking for250: (-1, 15)
FindThe index and the comparison count for both targets.
Given
The list holds fifteen ids in ascending order.
245 is the fourteenth of them, at index 13.
250 is not in the list.
Solution
Walk to the target
$$245 \text{ at index } 13 \Rightarrow 14 \text{ comparisons}$$
The counter goes up before the test, so an item at index 13 has cost 14 looks by the time it is found.
$$250 \Rightarrow 15 \text{ comparisons}$$
Absence costs the whole list, because this version has no early stop even though the list is sorted.
Answer $$\boxed{(13, 14)\;/\;(-1, 15)}$$
Check
Independent check: the absent target costs 15 and the list has 15 items, so the counter is counting one look per item.
bisection search on the same fifteen ids, counted
The same list, the same two targets, the halving search. The counter is in the same place, just inside the loop.
defbinary_search(L, e):
"""Assumes L is sorted ascending. Returns (index, comparisons)."""
comparisons = 0
first = 0
last = len(L) - 1while first <= last:
mid = (first + last) // 2
comparisons += 1if e < L[mid]:
last = mid - 1elif e > L[mid]:
first = mid + 1else:
return mid, comparisons
return -1, comparisons
ids = [102, 118, 125, 137, 140, 156, 171, 188, 190,
204, 211, 226, 233, 245, 260]
print('looking for 245:', binary_search(ids, 245))
print('looking for 250:', binary_search(ids, 250))
Sample Run:
looking for245: (13, 3)
looking for250: (-1, 4)
FindThe index and the comparison count for both targets.
Given
The list is the same fifteen ids in ascending order.
The window starts as index 0 to index 14.
mid is (first + last) // 2.
Solution
Halve down to the target
$$\texttt{mid} = 7 \to 11 \to 13$$
Three windows: the whole list, the upper half, then the upper quarter, which lands on 245.
$$245 \Rightarrow 3 \text{ comparisons}$$
Fifteen items can be halved three times, so three is also the worst case here.
$$250 \Rightarrow 4 \text{ comparisons}$$
Absence is found when the window closes, which takes one more turn than landing on the value.
Answer $$\boxed{(13, 3)\;/\;(-1, 4)}$$
Check
Independent check against the other version: both returned index 13 for 245 and -1 for 250, so the cheap one is not cheap by being wrong.
Same list, same targets, same answers: 14 and 15 comparisons one way, 3 and 4 the other. The only difference in the code is which index is examined next, and the only thing that makes the cheap version legal is that the list is sorted.
How to tell them apart
If the code reads L[i] for i going 0, 1, 2 in order, it is linear search and it works on any list. If the code computes an index from two other indices, it is bisection search and it requires a sorted list.
Scaffolding comes off
The common skeleton
Decide what shrinks and make it a parameter, usually an index i.
Write the base case first: if i == len(L): and the answer for no items at all.
Assume the same function already returns the right answer for i + 1.
Write one line that combines L[i] with that assumed answer, and put return in front of it.
Call it on the full list and on the empty or one item list, and check both.
1 · fully worked
Recursive total of a list of prices, from index i onward
Write total_from(prices, i) which returns the sum of prices from index i to the end. No loop.
deftotal_from(prices, i):
"""Assumes prices is a list of numbers and i an index into it. Returns the sum of prices from index i to the end."""if i == len(prices):
return0return prices[i] + total_from(prices, i + 1)
basket = [12.5, 4.0, 7.25]
print('all three:', total_from(basket, 0))
print('last two: ', total_from(basket, 1))
print('nothing: ', total_from(basket, 3))
Sample Run:
all three: 23.75
last two: 11.25
nothing: 0
FindThe three printed sums.
Given
prices is [12.5, 4.0, 7.25].
The function is called with i equal to 0, then 1, then 3.
Solution
What shrinks
$$\texttt{total\_from(prices, i)}$$
The index shrinks the problem while the list stays whole, so no slicing and no copying. Chosen over prices[1:] because a sum over a long list would then copy the tail on every call.
The base case
$$\texttt{if i == len(prices): return 0}$$
The sum of no numbers is 0, and 0 is the right answer rather than a placeholder: adding it changes nothing. Using len(prices) - 1 here would drop the last price.
Combine one item with the rest
$$\texttt{return prices[i] + total\_from(prices, i + 1)}$$
Assume the call for i + 1 is right; then the whole answer is this one price plus that. The return is in front because the value has to travel back up.
$$12.5 + (4.0 + (7.25 + 0)) = 23.75$$
The additions happen on the way back up, innermost first, exactly as in the factorial figure.
$$\texttt{total\_from(basket, 3)} = 0$$
i already equals len, so the base case answers immediately. That is the call that proves the base case is right.
Answer $$\boxed{23.75 \;/\; 11.25 \;/\; 0}$$
Check
Independent check by adding the three prices by hand: 12.5 plus 4.0 is 16.5, plus 7.25 is 23.75.
Four calls for three prices: one per item plus the base case. O(n) in time and O(n) in call frames.
Every rung below uses the same four lines. Only the base case value and the combining line change.
2 · you write the reasoning
Same skeleton, simpler combining step. Write count_below(counts, limit, i), which returns how many items of counts from index i on are below limit. The steps are given; write the reason for each one yourself before you open the answers.
defcount_below(counts, limit, i):
"""Assumes counts is a list of ints, limit an int, i an index. Returns how many items of counts from index i on are below limit."""if i == len(counts):
return0if counts[i] < limit:
return1 + count_below(counts, limit, i + 1)
return count_below(counts, limit, i + 1)
stock = [4, 15, 5, 23, 7]
print(count_below(stock, 10, 0))
print(count_below(stock, 4, 0))
Sample Run:
30
reasoning
The count of items below the limit in an empty stretch is 0, and 0 is also the neutral value for the additions that follow.
reasoning
The test looks at one item only. Whether any other item is below the limit is the recursive call's business.
reasoning
This item counts for one, and the rest of the answer comes from the call. The 1 + is the whole combining step, which is why this rung is easier than a sum: there is no arithmetic on the data at all.
reasoning
This item counts for nothing, so the answer for the whole stretch is exactly the answer for the rest. Notice both branches end in a return and both make the same recursive call; leaving the return off either one would give None.
reasoning
The items below 10 are 4, 5 and 7, so three of the five, and the run agrees. The second call in the program, with limit 4, gives 0, which checks the other end: nothing is below 4.
3 · find the buried error
Harder: the answer is now an index rather than a number, and there has to be a way of saying there is none. A student wrote first_below(counts, limit, i), which should return the index of the first item from i on that is below limit, or -1 if there is none. It is meant to print 2 and then -1 for the list [15, 23, 7, 30] with limits 10 and 5. It prints None and then 0. Exactly two of the three steps are wrong.
deffirst_below(counts, limit, i):
"""Returns the index of the first item from i on that is below limit, or -1 if there is none."""if i == len(counts) - 1: # Step 1return -1if counts[i] < limit: # Step 2return i
first_below(counts, limit, i + 1) # Step 3
the two buried errors (2)
⚠ step 1
The base case fires one item too early: it is reached when i is the index of the last item, so that last item is never tested against the limit.
len(counts) - 1 is the correct bound in range and in the last of a bisection search, so the hand writes it automatically. Here i is compared with the position one past the end, not with the last valid index.
right
Write if i == len(counts): return -1.
⚠ step 3
The recursive call has no return in front of it, so every answer computed by a deeper call is thrown away and the function returns None.
The same line in the printing version of a recursive search correctly has no return, because there is nothing to hand back. Here there is.
right
Write return first_below(counts, limit, i + 1).
4 · the bare problem
§10.1 — a recursive largest value, from a bare specification
No skeleton this time. A weather log holds one temperature reading per hour and you need the highest one, written recursively because the specification says so.
Find
(a) Write the function and the two test prints.
(b) Say why the base case here is i == len(values) - 1 and not i == len(values), which is the opposite of every other rung on this ladder.
Given
Write max_from(values, i) in a file called readings.py, with a docstring in the course's form.
It assumes values is a list of numbers with at least one item and i is an index inside it.
It returns the largest value from index i to the end. No loop anywhere in the function.
Test it with readings = [18, 25, 21, 25, 9], printing the largest of the whole list and the largest from index 2.
Hint 1/4
The answer for the whole list is either the first value or the largest of the rest, so the shape is the same as the other rungs: one item combined with an assumed answer.
Hint 2/4
The combining step is a comparison rather than an addition: compute the answer for the rest, then return whichever of that and values[i] is bigger.
Hint 3/4
For [18, 25, 21, 25, 9] the call at i equal to 4 has only the 9 left. Ask what the largest of an empty stretch would even be, and that tells you where the base case has to sit.
Hint 4/4
The base case returns values[i] when i is the last index, and the recursive line returns the larger of values[i] and max_from(values, i + 1).
Show solution
Choose the base case that can answer
$$\texttt{if i == len(values) - 1: return values[i]}$$
The last item on its own is the largest of the stretch that holds only it. Chosen over i == len(values) because that stretch is empty and has no largest to return.
Combine by comparing
$$\texttt{rest = max\_from(values, i + 1)}$$
Assume it is right. Storing it in a name rather than calling twice matters: two calls would double the work at every level and turn an O(n) function into an exponential one.
Strictly greater, so a tie returns rest. Either is correct for a largest value, and consistency matters if the values were objects.
$$\texttt{return rest}$$
The other branch, and it must be a return too. Both paths out of the recursive case hand a value back.
Read the two tests
$$\texttt{max\_from(readings, 0)} = 25$$
The largest of all five readings.
$$\texttt{max\_from(readings, 2)} = 25$$
From index 2 the stretch is 21, 25, 9, whose largest is still 25. Same answer from a different route, which is a small check that the index parameter is really being used.
Answer $$\boxed{25 \;/\; 25}$$
Check
Independent check with a different function on the same data: max(readings) is 25 and max(readings[2:]) is 25.
When the answer for an empty input does not exist, the base case moves up one and the docstring has to say the input is non empty. That is the one place this ladder's skeleton needs adjusting.
Full exam-style question
A Playlist class with a bubble sort over its objects and a recursive reportexam format
This is the shape of the eighth lab and of the long question on the final: a class given an ordering, a container class whose data is private, a sort over the objects that uses that ordering, and a recursive search that prints a report. Nothing here is new; every part came from a box above or from the classes week.
Write playlist.py containing:
A class Track with private title, seconds and plays, one get method per attribute, a __lt__ that makes a track less than another if it is shorter and compares titles when the lengths are equal, and a __repr__ printing the three fields one per line.
A class Playlist with private name, owner and tracks, an __init__ taking the name and the owner and starting with an empty list, get_track(index), get_num_tracks(), add_track(track), a bubble_sort() that orders the tracks shortest first using __lt__, a recursive linear_search(limit, i=0) that prints every track whose play count is below limit, and a __repr__ giving the name, the owner and every track.
A function create_playlist(lines) taking a list of strings, the first holding the name and the owner and the rest holding a title, a length in seconds and a play count, and returning the filled Playlist.
A script that builds the playlist, sorts it, displays it, and then reports the tracks with fewer than 10 plays.
classTrack:
"""One song: a title, a length in seconds and how many times it was played."""def__init__(self, title, seconds, plays):
self.__title = title
self.__seconds = seconds
self.__plays = plays
defget_title(self):
"""Returns the title."""returnself.__title
defget_plays(self):
"""Returns the number of plays."""returnself.__plays
defget_seconds(self):
"""Returns the length in seconds."""returnself.__seconds
def__lt__(self, other):
"""A track is less than another if it is shorter. If the lengths are equal the titles are compared."""ifself.__seconds == other.get_seconds():
returnself.__title < other.get_title()
returnself.__seconds < other.get_seconds()
def__repr__(self):
"""Returns the title, the length and the plays, one per line."""return ('Title: ' + self.__title + '\nSeconds: ' + str(self.__seconds)
+ '\nPlays: ' + str(self.__plays) + '\n')
classPlaylist:
"""A named list of Track objects."""def__init__(self, name, owner):
self.__name = name
self.__owner = owner
self.__tracks = []
defget_track(self, index):
"""Returns the Track at the given index."""returnself.__tracks[index]
defget_num_tracks(self):
"""Returns how many Tracks the playlist holds."""returnlen(self.__tracks)
defadd_track(self, track):
"""Adds one Track to the end of the playlist."""self.__tracks.append(track)
defbubble_sort(self):
"""Sorts the tracks in place, shortest first, using __lt__."""
issorted = False
j = 0while j < len(self.__tracks) - 1andnot issorted:
issorted = Truefor k inrange(len(self.__tracks) - j - 1):
ifself.__tracks[k + 1] < self.__tracks[k]:
issorted = False
temp = self.__tracks[k]
self.__tracks[k] = self.__tracks[k + 1]
self.__tracks[k + 1] = temp
j = j + 1deflinear_search(self, limit, i=0):
"""Displays every track from index i on whose play count is below limit. Uses a recursive linear search."""if i == self.get_num_tracks():
return
track = self.__tracks[i]
if track.get_plays() < limit:
print(track.get_title(), 'has only', track.get_plays(), 'plays')
self.linear_search(limit, i + 1)
def__repr__(self):
"""Returns the name, the owner and every track."""
out = self.__name + ' ' + self.__owner + '\n\n'for track inself.__tracks:
out = out + str(track) + '\n'return out
defcreate_playlist(lines):
"""Assumes lines is a list of strings. The first holds the name and the owner; each later line holds a title, seconds and plays. Returns a Playlist holding one Track per later line."""
head = lines[0].strip().split(',')
playlist = Playlist(head[0], head[1])
for line in lines[1:]:
parts = line.strip().split(',')
playlist.add_track(Track(parts[0], int(parts[1]), int(parts[2])))
return playlist
LINES = ['Sabah,Ankara',
'Oda,210,14',
'Bir,185,3',
'Cam,210,41',
'Ada,240,7']
playlist = create_playlist(LINES)
playlist.bubble_sort()
print('Bubble sorted playlist:')
print(playlist)
print('Tracks nobody listens to:')
playlist.linear_search(10)
Sample Run:
Bubble sorted playlist:
Sabah Ankara
Title: Bir
Seconds: 185
Plays: 3
Title: Cam
Seconds: 210
Plays: 41
Title: Oda
Seconds: 210
Plays: 14
Title: Ada
Seconds: 240
Plays: 7
Tracks nobody listens to:
Bir has only 3 plays
Ada has only 7 plays
FindThe sorted display and the report, and which parts of the answer carry the marks.
Given
The data lines are Sabah,Ankara, then Oda,210,14, Bir,185,3, Cam,210,41 and Ada,240,7.
Track.__lt__ compares seconds first and titles only when the seconds are equal.
The report limit is 10 plays.
Solution
Put the ordering in the class, not in the sort
$$\texttt{def \_\_lt\_\_(self, other):}$$
The sort below compares two Track objects with the less than sign and nothing else, so all the knowledge about what shorter means lives here. Putting it in the sort instead would mean writing a new sort for every ordering.
The tie branch reads the other object through its get method rather than through other.__seconds, because the two underscores rename the attribute and the direct read is the fragile version.
The only line that touches the data, and it is the same comparison as data[k] > data[k+1] written the other way round, because a class is only required to define __lt__.
$$\texttt{temp = self.\_\_tracks[k]}$$
The same three line swap. Objects are swapped by moving the references, and no Track is copied or changed.
$$\texttt{while j < len(...) - 1 and not issorted}$$
The flag is kept, so a playlist that arrives already in order costs one pass.
The index is a parameter with a default, so the script calls playlist.linear_search(10) and the recursion supplies the index itself. This is the detail the lab sheet does not spell out and the one that makes the call site read normally.
$$\texttt{if i == self.get\_num\_tracks(): return}$$
Base case on the container's own count. A bare return because the method reports by printing.
$$\texttt{self.linear\_search(limit, i + 1)}$$
Last line, no return needed, and note it is called on self: inside a method the recursive call still needs the object.
Read the two blocks of output
$$185, 210, 210, 240$$
Ascending in seconds, and inside the 210 tie Cam comes before Oda, so both branches of __lt__ fired.
$$\texttt{Bir}(3),\ \texttt{Ada}(7)$$
The only two play counts below 10, reported in the sorted order because the sort ran first and the search walks the list as it now stands.
Answer $$\boxed{\text{Bir, Cam, Oda, Ada};\ \text{report: Bir with 3 and Ada with 7}}$$
Check
Two independent checks. The tie: Cam and Oda both run 210 seconds and Cam is printed first, which only the title branch of __lt__ can explain. The report: the five play counts are 14, 3, 41, 7, and exactly two of them are under 10, so two lines is right.
Four tracks, so the sort makes at most six comparisons and the search makes four calls. The whole answer is about eighty lines and none of it is new code; it is the classes week plus two boxes from this page.
One thing changes between this page and the lab: the lab gives you the data in a file rather than in a list. That is two lines, and the rest of the answer is untouched.
Practice
A · concept 3 questions
1§10.2 — when the early stop is allowed
A helper is written for a list of room numbers. It stops as soon as it meets a number larger than the one it is looking for, which on a sorted list saves about half the work.
The claim: adding that line can only make the search faster, never wrong, because it only ever stops early on a value that is not there.
Find(a) True or false, with the reason.
Given
The line added is if L[i] > e: return False.
The list it is used on is [34, 6, 16, 35, 1].
The value looked for is 16, which is in that list.
Hint 1/4
The claim has a hidden assumption in it. Find the assumption before judging the claim.
Hint 2/4
The reasoning everything after this item is bigger is only valid when the list is sorted ascending. Nothing in the line itself checks that.
Hint 3/4
On [34, 6, 16, 35, 1] the first item is 34, and the value looked for is 16.
Hint 4/4
False: on that list the search reports 16 as absent after one comparison.
Show solution
Run the first comparison
$$\texttt{L[0] = 34},\ 34 \neq 16$$
No match, so the next test runs.
$$34 > 16 \Rightarrow \texttt{return False}$$
The early stop fires immediately, and four items are never looked at.
Compare with the truth
$$\texttt{16 in L} \to \texttt{True}$$
in walks the whole list, so it is right and the helper is wrong.
Answer $$\boxed{\text{False}}$$
Check
Independent check: the same line on the sorted list [1, 6, 16, 34, 35] answers True for 16, so the fault is in the list and not in the line.
Any claim of the form this can only help should be tested on the input the claim's reasoning quietly assumed away.
2§10.1 — what a base case guarantees
A student is told their recursive function never stops and adds a base case at the top of it.
The claim: once a recursive function has a base case, it is guaranteed to finish, because the base case is the line that ends the recursion.
Find(a) True or false, with the reason.
Given
The base case added is if i == len(L): return 0.
The recursive line is return L[i] + f(L, i).
The first call is f(L, 0) on a list of three items.
Hint 1/4
Two separate things have to be true for a recursion to finish. The claim names one of them.
Hint 2/4
A base case says what the answer is at the end. Something else has to guarantee the end is reached, and that something is the argument getting closer to it on every call.
Hint 3/4
Here the recursive line passes i unchanged, so the second call has i equal to 0 just like the first, on a list of length 3.
Hint 4/4
False: with i never increasing, i == len(L) is never true.
Show solution
Trace the argument
$$f(L, 0) \to f(L, 0) \to f(L, 0)$$
The second argument is passed through unchanged, so the sequence of calls is constant.
$$i = 0 \neq 3 = \texttt{len(L)}$$
The base case test is False on every call.
Name what is missing
$$f(L, i + 1)$$
The fix is in the recursive call and not in the base case. A base case is a statement about the end; the shrinking is what gets you there.
Answer $$\boxed{\text{False}}$$
Check
Independent check on the direction of the fix: with i + 1 the values of i are 0, 1, 2, 3, and the fourth call matches len(L).
3§10.7 — which line leaves the caller's list alone
A report needs the three smallest marks and must also print the marks in the order they were entered, later in the same program. So the original order has to survive.
Find(a) Which single line gives you a sorted list and leaves marks in entry order?
Given
marks is [55, 90, 40, 72, 61], in entry order.
merge_sort is the version from this page, which returns a new list.
bubble_sort is the version from this page, which sorts in place and returns nothing.
Hint 1/4
Two requirements, and each line has to be checked against both: does it give you a sorted list, and is marks still in entry order afterwards.
Hint 2/4
Two of the four operations return a new list and leave their input alone: sorted and merge_sort. The other two rearrange the input and return None.
Hint 3/4
The list is [55, 90, 40, 72, 61] and it must still read in that order after the line. Look at which name each line assigns to.
Hint 4/4
The line that both returns a list and does not touch marks is the merge sort one.
Show solution
Returns a list, changes nothing
$$\texttt{merge\_sort(marks)}$$
Returns a new list, leaves the argument alone. Both boxes ticked.
$$\texttt{sorted(marks)}$$
Also returns a new list, but the offered line assigns it back over marks, which fails the second requirement.
Independent check: after the chosen line, marks[0] is still 55.
B · computation 6 questions
1§10.1 — a print before the call and a print after it
A recursive function with a print on each side of the recursive call.
defshow(n):
"""Prints on the way down and again on the way back up."""if n == 0:
returnprint('down', n)
show(n - 1)
print('up', n)
show(3)
Find(a) Write exactly what this prints.
Given
The call is show(3).
The base case is n == 0 and returns without printing.
There is a print before the recursive call and another after it.
IPython console
In [1]: %run untitled0.py
The down lines come out in the order the calls were made and the up lines in the reverse order, because a call cannot reach its second print until the call it made has finished. This is the factorial stack again: the same picture, with prints instead of multiplications.
Hint 1/4
Six lines come out of three calls. The question is the order, not the count.
Hint 2/4
Everything before a recursive call runs on the way down, and everything after it runs on the way back up, once the deeper call has returned.
Hint 3/4
The calls are made with n equal to 3, 2, 1 and then 0. The 0 call prints nothing and returns.
Hint 4/4
Three down lines with n falling, then three up lines with n rising.
Show solution
On the way down
$$\texttt{show(3)} \to \texttt{down 3}$$
Printed before the call, so it comes first.
$$\texttt{show(2)} \to \texttt{down 2}$$
The frame for n equal to 3 is still waiting.
$$\texttt{show(1)} \to \texttt{down 1}$$
Three frames now waiting.
$$\texttt{show(0)} \to \text{returns}$$
The base case prints nothing, which is why there are no down 0 or up 0 lines.
On the way back up
$$\texttt{up 1},\ \texttt{up 2},\ \texttt{up 3}$$
The innermost waiting frame resumes first, so the n values come out in the reverse of the order they went in.
Answer $$\boxed{\text{down }3,2,1\ \text{then up }1,2,3}$$
Check
Independent check on the count: three calls print, each prints twice, so six lines, and the base case contributes none.
If an exam asks you to print a list forwards and backwards with one recursive function, this is the whole trick.
2§10.1 — a recursive count with two answers
A recursive count over a list, called twice with different values.
defcount_value(L, value, i):
"""Assumes L is a list and i an index. Returns how many times value appears in L from index i onwards."""if i == len(L):
return0if L[i] == value:
return1 + count_value(L, value, i + 1)
return count_value(L, value, i + 1)
seats = [3, 5, 3, 3, 8]
print(count_value(seats, 3, 0))
print(count_value(seats, 9, 0))
Find(a) Write exactly what this prints.
Given
The list is [3, 5, 3, 3, 8].
The base case is i == len(L) and returns 0.
The two calls look for 3 and for 9, both starting at index 0.
IPython console
In [1]: %run untitled0.py
The second call is the one worth looking at: with no matches at all, every call takes the third line and the answer is the base case's 0 passed straight back up through five frames. That is why the base case value has to be 0 and not, say, -1.
Hint 1/4
Two numbers come out. Each is a count over the whole list.
Hint 2/4
The matching branch returns 1 + the call for the rest, and the other branch returns just the call for the rest, so the answer is built by adding one per match on the way back up.
Hint 3/4
The list [3, 5, 3, 3, 8] holds three 3s and no 9s.
Hint 4/4
Three, then zero.
Show solution
Looking for 3
$$\text{indices } 0, 2, 3 \text{ match}$$
Three of the five items equal 3.
$$1 + 1 + 1 + 0 = 3$$
Each matching frame adds one to what comes back from below, and the base case starts the sum at 0.
Looking for 9
$$\text{no index matches}$$
Every frame takes the pass it on branch, so nothing is added.
$$0$$
The base case's 0 travels back up unchanged, which is the correct answer for none.
Answer $$\boxed{3 \;/\; 0}$$
Check
Independent check with a different tool: [3, 5, 3, 3, 8].count(3) is 3 and .count(9) is 0.
3§10.3 — a bisection loop with the wrong comparison
A bisection search whose loop condition is missing its equals sign. Three targets are tried.
defbinary_search(L, e):
"""Broken: the loop stops while one item is still unchecked."""
first = 0
last = len(L) - 1while first < last:
mid = (first + last) // 2if e < L[mid]:
last = mid - 1elif e > L[mid]:
first = mid + 1else:
return mid
return -1
ids = [11, 22, 33, 44, 55]
print(binary_search(ids, 33))
print(binary_search(ids, 55))
print(binary_search(ids, 11))
Find(a) Write exactly what this prints.
Given
The list is [11, 22, 33, 44, 55], sorted ascending, five items.
The loop condition is while first < last, without the equals sign.
The targets are 33, 55 and 11, and all three are in the list.
IPython console
In [1]: %run untitled0.py
33 is found immediately because it happens to sit at the first midpoint. 11 is found because the window 0 to 1 still has first below last, and mid is 0. 55 is lost: its window shrinks to 4 to 4 and the loop refuses to look. A quick test with a target in the middle would have passed, which is exactly why this bug reaches an exam paper.
Hint 1/4
All three targets are present, so any -1 in the output is the bug showing itself. Work out which of the three the loop can still reach.
Hint 2/4
The loop stops when first equals last, that is when the window has shrunk to exactly one item. That last item is therefore never compared.
Hint 3/4
The list is [11, 22, 33, 44, 55]. For 33 the first mid is 2; for 55 and for 11 the window has to shrink to one item before the target is reached.
Hint 4/4
One of the three is found at the first comparison, one is found at index 0, and one is reported absent.
Show solution
Target 33
$$\texttt{first 0, last 4, mid 2} \to \texttt{L[2] = 33}$$
Equal on the first turn, so the missing equals sign never matters.
$$\texttt{first 0, last 1, mid 0} \to \texttt{L[0] = 11}$$
first is still below last, so the loop runs and finds it.
Answer $$\boxed{2 \;/\; -1 \;/\; 0}$$
Check
Independent check on which item the bug loses: the only target reported absent is the one whose window closes to a single item, and 11 shows that reaching index 0 is not the problem.
Test a search on the first item, the last item and an absent item. Those three calls catch every off by one this algorithm has.
4§10.5 — a swap written without a temporary
A bubble sort whose swap is written as two plain assignments, with no temporary name and no simultaneous assignment.
defbroken_sort(data):
"""The swap is written without a temporary name."""for j inrange(len(data) - 1):
for k inrange(len(data) - j - 1):
if data[k] > data[k + 1]:
data[k] = data[k + 1]
data[k + 1] = data[k]
marks = [55, 90, 40, 72, 61]
broken_sort(marks)
print(marks)
Find(a) Write exactly what this prints.
Given
The list is [55, 90, 40, 72, 61].
The swap is data[k] = data[k + 1] followed by data[k + 1] = data[k].
The loop bounds themselves are correct.
IPython console
In [1]: %run untitled0.py
A swap that should exchange two values instead copies one of them over the other. The list still comes out in ascending order, which is the cruel part: it looks sorted. Count the values instead. The output has three 40s and two 61s, and the 90 and the 72 and the 55 are gone. Any sort answer should be checked by asking whether the same values came out, not just whether they are in order.
Hint 1/4
The loops are fine, so trace what the two assignment lines do to a single pair before worrying about the whole sort.
Hint 2/4
The first assignment overwrites data[k]. The second one then reads data[k], which no longer holds the value it held a line earlier.
Hint 3/4
Take the pair 90 and 40 at indices 1 and 2 of [55, 90, 40, 72, 61] and run the two lines on it.
Hint 4/4
Every swap copies the smaller value into both places, so values are duplicated and others are lost.
data[1] is now 40, so this writes 40 back over the 40 that was already there and achieves nothing.
What the whole sort then does
$$\text{every swap duplicates the smaller value}$$
So the smallest value spreads leftwards through the list, and the result is ascending and wrong.
Answer $$\boxed{[40, 40, 40, 61, 61]}$$
Check
Independent check that does not need the trace: the input held five different values and the output holds two, so the function cannot be a sort whatever the order looks like.
Check a sort by the multiset of values first and the order second. Order alone cannot detect a broken swap.
5§10.6 — a merge sort called and then ignored
A merge sort is called on a list, and then both the original list and a fresh call are printed.
defmerge_sort(L):
"""Returns a NEW sorted list built from L."""iflen(L) < 2:
return L[:]
middle = len(L) // 2
left = merge_sort(L[:middle])
right = merge_sort(L[middle:])
out = []
i = 0
j = 0while i < len(left) and j < len(right):
if left[i] < right[j]:
out.append(left[i])
i += 1else:
out.append(right[j])
j += 1while i < len(left):
out.append(left[i])
i += 1while j < len(right):
out.append(right[j])
j += 1return out
weights = [8, 2, 5]
merge_sort(weights)
print(weights)
print(merge_sort(weights))
Find(a) Write exactly what this prints.
Given
weights is [8, 2, 5].
merge_sort returns a new list and contains no assignment to its parameter.
The first call's return value is not stored anywhere.
IPython console
In [1]: %run untitled0.py
The first call really did sort. It built [2, 5, 8], returned it, and the program threw it away, which costs the same work as keeping it. The correct line is weights = merge_sort(weights). This is the exact mirror of the marks = bubble_sort(marks) fault: with an in place sort you must not assign, and with merge sort you must.
Hint 1/4
Two lines come out. They are not the same, and the difference is the whole point of the question.
Hint 2/4
Merge sort builds its answer in a new list and hands it back. A call whose value is not assigned has no lasting effect at all.
Hint 3/4
weights is [8, 2, 5]. The first call's result is discarded; the second call's result is printed directly.
Hint 4/4
The original order first, then the sorted order.
Show solution
The discarded call
$$\texttt{merge\_sort(weights)} \to [2, 5, 8]$$
Computed, returned, and not assigned, so the only reference to it disappears at the end of the statement.
$$\texttt{weights} \to [8, 2, 5]$$
Unchanged, because the function only ever read from it and wrote into slices and a new list.
Here the return value is used immediately, so it survives long enough to be printed.
Answer $$\boxed{[8, 2, 5] \;/\; [2, 5, 8]}$$
Check
Independent check that the function is not in place: the second print shows a sorted list while the first shows the original, so both orders exist at once and must be two objects.
6§10.3 — a window that stops shrinking
A bisection search whose window ends are moved to mid rather than past it. A step counter stops the run after five turns so the repetition is visible.
defbinary_search(L, e):
"""Broken: the window ends are set to mid instead of mid - 1 and mid + 1. A step counter stops the run so the repetition is visible."""
first = 0
last = len(L) - 1
steps = 0while first <= last and steps < 5:
mid = (first + last) // 2print('step', steps, 'first', first, 'last', last, 'mid', mid)
if e < L[mid]:
last = mid
elif e > L[mid]:
first = mid
else:
return mid
steps += 1return -1
ids = [11, 22, 33, 44]
print('answer:', binary_search(ids, 30))
Find(a) Write exactly what this prints.
Given
The list is [11, 22, 33, 44] and the target is 30, which is absent.
The window updates are last = mid and first = mid, without the minus one and plus one.
The loop also stops when steps reaches 5.
IPython console
In [1]: %run untitled0.py
Steps 2, 3 and 4 print exactly the same window. The search has stopped making progress: with first 1 and last 2, mid is 1, the target is above L[1], so first becomes 1 again. Without the step counter this program would never finish. The returned -1 is not the algorithm concluding the value is absent; it is the counter giving up, and on a target that was present the same loop would hang instead.
Hint 1/4
Six lines come out. Read the last three of them together and ask what is different between them.
Hint 2/4
The item at mid has already been compared, so a window that keeps it can fail to get smaller. When first and last stay put, mid stays put too.
Hint 3/4
The list is [11, 22, 33, 44] and the target 30 lies between L[1] and L[2]. Compute mid from each printed pair yourself.
Hint 4/4
Five step lines, three of them identical, and then -1 from the counter rather than from the algorithm.
The state after this turn is identical to the state before it, so the next turn must do the same thing.
$$\text{steps } 3, 4:\ \text{identical}$$
Three identical lines, and only the counter ends the loop.
$$\texttt{answer: -1}$$
Returned because steps < 5 failed, not because the window closed.
Answer $$\boxed{\text{5 step lines, the last 3 identical, then }-1}$$
Check
Independent check on the diagnosis: with mid - 1 and mid + 1 the same search on the same data prints two step lines and then -1, so the repetition really comes from the two changed lines.
If a trace ever prints the same state twice, stop tracing: the loop cannot escape, and the bug is in whatever was supposed to change.
C · exam level 3 questions
1§10.7 — 50000 records and 200 lookups
A courier program reads 50000 delivery records into a list, in the order they arrive, so the list is in no particular order. It then has to answer 200 queries. Each query gives one tracking code and asks for the matching record, and tracking codes are unique.
Find(a) Which plan does the least work in total?
Given
The list holds 50000 records in no particular order.
There are 200 queries, each on a unique tracking code.
50000 can be halved 15 times, so merge sort costs about 750000 comparisons and each bisection about 16.
Bubble sort on 50000 items costs about 1.2 billion comparisons.
Hint 1/4
Total work means the one time setup plus the per query cost times 200. Write that sum for each plan before comparing anything.
Hint 2/4
Sorting is worth paying for from about log n lookups onward, which here is 16, and 200 is well past that. But a lookup on a single unique field does not need order at all, and the structure that provides that costs one pass over the records.
Hint 3/4
The numbers in the given: 200 times 50000 for the walks, 750000 plus 200 times 16 for merge sort, 1.2 billion for bubble sort, and one pass over 50000 records to build a dictionary.
Hint 4/4
The dictionary costs about 50000 operations and then answers every query without searching.
Show solution
The two plans that sort
$$750000 + 200 \times 16 \approx 753000$$
Merge sort dominates and the searching is almost free, which is the flat column from the amortisation table again.
$$1.2 \times 10^{9} + 3200$$
Bubble sort's n squared is what makes this the worst plan, even though its searching is identical to the plan above.
The two plans that do not sort
$$200 \times 50000 = 10^{7}$$
Every query pays the full walk because the list is unsorted and nothing is remembered between queries.
$$50000 \text{ insertions, then } 0$$
One pass over the records builds the dictionary, and after that a lookup examines one place rather than searching. This is the O(1) row of the complexity table.
Answer $$\boxed{\text{the dictionary, about } 50000}$$
Check
Independent check on the size of the claim: the measured list against dictionary run in the amortisation block had a thousand names, where the walk cost 1000 looks and the dictionary answered from one place, in both the present and the absent case.
Ask what the queries are keyed on before deciding whether to sort. Order is only worth buying when the queries are about order.
2§10.5 — the line that raises the IndexError
A student's bubble sort crashes. The three candidate lines are numbered in the comments, and the error message is IndexError: list index out of range.
defbubble_sort(data):
"""The inner range is written without the -1."""for j inrange(len(data) - 1): # Step 1for k inrange(len(data) - j): # Step 2if data[k] > data[k + 1]: # Step 3
data[k], data[k + 1] = data[k + 1], data[k]
marks = [55, 90, 40]
bubble_sort(marks)
print(marks)
Find(a) Which line has to change, and to what?
Given
The list is [55, 90, 40], three items, so the valid indices are 0, 1 and 2.
The error is raised on the comparison line, which reads both data[k] and data[k + 1].
With j equal to 0 the inner loop's stop value is 3.
Hint 1/4
The error names an index, so find the largest value of k the loops allow and then ask which index the body reads with it.
Hint 2/4
The body reads data[k + 1], so the loop must stop k one place earlier than the last index. That makes the inner stop value len(data) - j - 1.
Hint 3/4
The list is [55, 90, 40], so len(data) is 3 and the valid indices are 0, 1, 2. With j equal to 0 the inner range is range(3), whose last value is 2.
Hint 4/4
With k equal to 2 the body reads data[3], which does not exist, so Step 2 needs its minus one.
range stops before its argument, so k takes 0, 1, 2 and its largest value is 2.
$$\texttt{data[k + 1]} = \texttt{data[3]}$$
One past the last valid index, so this read raises the IndexError.
Fix the bound, not the body
$$\texttt{range(len(data) - j - 1)}$$
Now the largest k is 1 and the largest index read is 2. Changing the body to data[k - 1] instead would compare each item with its left neighbour and never look at the last item.
Independent check on the fixed version: the bubble sort with the minus one, run earlier on the five item list, sorted it without an error and its inner loop's largest k was 3 on a list whose last index is 4.
Any loop whose body reads L[k + 1] must stop one short. Read the body first, then set the bound from it.
3§10.5 — a shelf program, sorted then searched
Exam length, in the lab's own format. A stock program reads a line of stock counts, puts them in order, and then answers one lookup. The user's input and the program's output are both shown in the Sample Run, with the input appearing after each prompt because the terminal echoes what is typed.
Find
(a) Write the whole file.
(b) Say why binary_search may be called on the list at that point in the script but not one line earlier.
Given
Write shelf.py with three functions and a script that uses them.
read_counts(text) assumes text is numbers separated by single spaces and returns them as a list of ints.
selection_sort(L) assumes L is a list of ints and sorts it in place, ascending. Do not use sort or sorted.
binary_search(L, e) assumes L is sorted ascending and returns the index of e, or -1 if it is absent.
The script reads one line of counts, sorts them, prints the sorted list, reads one target, and prints either the index it was found at or a message saying it is not on the shelf.
Every function needs a docstring in the course's form.
Hint 1/4
Three separate jobs and they do not interact: turn text into ints, order the ints, find one int. Write and test them one at a time.
Hint 2/4
text.split(' ') gives a list of strings and int(field) turns one into a number. Selection sort is the suffix_start loop from this page. Bisection search is the first, last, mid loop.
Hint 3/4
The Sample Run types 12 4 9 30 7 and then 9. After sorting, that list is [4, 7, 9, 12, 30] and 9 sits at index 2.
Hint 4/4
The sorted list prints as [4, 7, 9, 12, 30] and the lookup for 9 reports index 2.
Splitting gives strings. Converting here rather than later means the rest of the program never has to think about it.
$$\texttt{counts.append(int(field))}$$
int of each field, appended one at a time, which is the course's loop rather than a comprehension.
Order, then look up
$$\texttt{selection\_sort(counts)}$$
In place, called on its own line and not assigned, because it returns nothing.
$$[4, 7, 9, 12, 30]$$
The sorted order, which is what the search needs.
$$\texttt{binary\_search(counts, 9)} \to 2$$
mid is 2 on the first turn and L[2] is 9, so this particular lookup takes one comparison.
Report either outcome
$$\texttt{if where == -1:}$$
The sentinel has to be tested for explicitly, because -1 is also a legal index in Python and printing it would be misleading.
Answer $$\boxed{\texttt{sorted: [4, 7, 9, 12, 30]};\ \texttt{found at index 2}}$$
Check
Independent check on the search against the printed list: index 2 of [4, 7, 9, 12, 30] is 9, which is what was asked for.
Every lab answer that searches has a sort somewhere above it, and the marks are as much in the order of the two calls as in either function.
D · interleaved 4 questions
1§10.7 — a helper that sorts what it was handed
A menu program asks a helper for its three cheapest prices and then prints the menu.
defcheapest_three(prices):
"""Returns the three lowest prices."""
prices.sort()
return prices[:3]
menu = [24.0, 9.5, 31.0, 12.5, 18.0]
print(cheapest_three(menu))
print(menu)
Find(a) Write exactly what this prints.
Given
menu is [24.0, 9.5, 31.0, 12.5, 18.0], in menu order.
cheapest_three calls sort on its parameter and returns a slice.
The caller prints the returned value and then its own list.
IPython console
In [1]: %run untitled0.py
The helper answered correctly and destroyed the menu order while doing it, and nothing in its name or its signature warns the caller. The fix is one line, prices = sorted(prices), which sorts a copy. This is the aliasing rule from the structured types week meeting the in place rule from this page, and it is the single most common way a working sort breaks a program somewhere else.
Hint 1/4
Two lines come out. The second one is the interesting one: ask what the caller's list looks like after the call.
Hint 2/4
A parameter is another name for the same object, and sort works in place, so sorting through the parameter sorts the caller's list. A slice, by contrast, is a new list.
Hint 3/4
menu starts as [24.0, 9.5, 31.0, 12.5, 18.0] and the helper sorts it before slicing.
Hint 4/4
The three cheapest, and then the menu in price order rather than in menu order.
Independent check that the returned list is a separate object: the two printed lists have different lengths, three and five, so they cannot be two names for one list.
A function that takes a list and does not promise to change it should sort a copy. sorted inside a helper costs one copy and removes a whole class of bug.
2§10.5 — sorting rows, then changing one of them
A table of [name, mark] rows is sorted by the mark, and then the first row of the sorted table is changed through a second name.
defsort_rows(table):
"""Sorts a list of [name, mark] rows in place by the mark."""for j inrange(len(table) - 1):
for k inrange(len(table) - j - 1):
if table[k][1] > table[k + 1][1]:
table[k], table[k + 1] = table[k + 1], table[k]
rows = [['ada', 70], ['bora', 45], ['cem', 88]]
sort_rows(rows)
print(rows)
first = rows[0]
first[1] = 0print(rows)
Find(a) Write exactly what this prints.
Given
The table is [['ada', 70], ['bora', 45], ['cem', 88]].
After the sort, first = rows[0] and then first[1] = 0.
IPython console
In [1]: %run untitled0.py
first and rows[0] are two names for one inner list, so writing through either one is visible through the other. This is why a sort over rows is cheap: three references move, and not a single name or mark is copied. It is also why handing a row out of a table is handing out write access to the table.
Hint 1/4
Two lines come out, and the difference between them is caused by two lines of the program, not by the sort.
Hint 2/4
Sorting a list of lists moves the inner lists around; it does not copy them. So rows[0] after the sort is the very same inner list that used to be somewhere else in the table.
Hint 3/4
The rows are ['ada', 70], ['bora', 45] and ['cem', 88], and sorting by the mark puts bora first.
Hint 4/4
The sorted table, and then the same table with bora's mark at 0.
Show solution
Sort by the second field
$$\texttt{table[k][1] > table[k+1][1]}$$
The outer index picks a row and the inner one picks the mark, so the comparison is between marks while the swap moves whole rows.
$$[['bora', 45], ['ada', 70], ['cem', 88]]$$
45, 70, 88 ascending.
Write through the handed out row
$$\texttt{first = rows[0]}$$
A second name for bora's row. No copy was made, because indexing a list gives you the item and not a clone of it.
$$\texttt{first[1] = 0}$$
Changes the one inner list, which the table still holds, so the table shows 0.
Independent check on the claim that the row was not copied: the second printed table shows the 0, and a copy would have left the table's own row at 45.
When a sort moves whole rows, every reference anyone still holds to a row stays valid and stays writable. That is useful and it is also a leak.
3§10.6 — a class counter, a tie, and a sort
An auction class counts how many bids were made and defines what one bid being less than another means. Three bids are made, two of them for the same amount.
classBid:
"""One bid in an auction. The class counts how many were made."""
count = 0def__init__(self, bidder, amount):
self.bidder = bidder
self.amount = amount
Bid.count = Bid.count + 1def__lt__(self, other):
"""A bid is less than another if its amount is smaller."""returnself.amount < other.amount
def__repr__(self):
returnself.bidder + ':' + str(self.amount)
bids = [Bid('ada', 300), Bid('bora', 150), Bid('cem', 300)]
bids.sort()
print(bids)
print(Bid.count)
Find(a) Write exactly what this prints.
Given
The bids are ada 300, bora 150 and cem 300, created in that order.
Bid.count is a class variable increased by one in __init__.
__lt__ compares only the amount, with no tie branch.
IPython console
In [1]: %run untitled0.py
Two things worth taking away. First, ada:300 comes before cem:300 and nothing in __lt__ says so: the built in sort leaves items that compare equal in their original relative order, which is a property the two swapping sorts on this page do not have. Second, Bid.count is 3 and not 1, because a class variable is one slot for the whole class and every __init__ added to it. If a specification wants a defined tie order, __lt__ needs a second branch, the way the Track class on this page has one.
Hint 1/4
Two lines come out: the sorted list of bids and one number. They test different things and neither depends on the other.
Hint 2/4
The built in sort calls __lt__, and when two items compare as neither less than the other it leaves them in the order it found them. A class variable is one slot shared by every object, increased once per construction.
Hint 3/4
The bids in creation order are ada 300, bora 150, cem 300, and __repr__ prints the bidder and the amount joined by a colon.
Hint 4/4
bora first because 150 is smallest, then the two 300s in creation order, and the count is 3.
__lt__ is False in both directions, so the built in sort keeps the order it received: ada was created first.
The counter
$$\texttt{Bid.count} = 3$$
Written as Bid.count = Bid.count + 1 inside __init__, so it counts constructions. Written as self.count it would have created a separate instance attribute on each object and printed 1.
Independent check on the tie claim: reversing the creation order of ada and cem changes the printed order of the two 300s, which a comparison based only on the amount could not do.
If ties matter, put the tie rule in __lt__. Relying on the sort to leave them alone works for the built in sort and not for the ones you write.
4§10.1 — a recursive collector with a default list
A recursive function collects the items of a list into an accumulator, which is given a default value so that the first caller does not have to supply one. It is called twice, on two different lists.
defcollect(L, i=0, seen=[]):
"""Appends every item of L from index i on to seen, then returns seen."""if i == len(L):
return seen
seen.append(L[i])
return collect(L, i + 1, seen)
print(collect([1, 2]))
print(collect([3, 4]))
Find(a) Write exactly what this prints.
Given
The first call is collect([1, 2]) and the second is collect([3, 4]).
The signature is def collect(L, i=0, seen=[]):.
The function appends to seen and returns it.
IPython console
In [1]: %run untitled0.py
The second call's answer carries the first call's data. The default [] was built once, when the def line ran, and append changes that one object rather than rebinding the name, so nothing ever resets it. Recursion makes this worse than usual: the accumulator pattern is exactly the case where a default is tempting. The fix is seen=None and then if seen is None: seen = [] as the first line, which builds a fresh list per call. This was a real midterm question in this course, in the form of a default parameter that was a function; the principle is the same and the list version is the one that bites in a lab.
Hint 1/4
Two lines come out and the second one is longer than the list that produced it. That is the question.
Hint 2/4
A default parameter value is evaluated once, when the def line runs, not once per call. So a default that is a list is one list shared by every call that does not supply its own.
Hint 3/4
The first call appends 1 and 2 to that shared list. The second call is made with no seen argument, so it gets the same object, which already holds [1, 2].
Hint 4/4
The first line is the first list and the second line is both lists joined.
Show solution
The first call
$$\texttt{seen}\ \text{bound to the default object}$$
Created once, when the def was executed, and stored with the function.
append changes the object in place, so the change outlives the call.
$$\texttt{return seen} \to [1, 2]$$
Returned, printed, and still the function's default object.
The second call
$$\texttt{collect([3, 4])}\ \text{with no seen}$$
So the parameter is bound to the same object again, which already holds two items.
$$[1, 2, 3, 4]$$
Four items from a two item list, which is the visible symptom of the shared default.
Answer $$\boxed{[1, 2]\;/\;[1, 2, 3, 4]}$$
Check
Independent check on the diagnosis: passing the accumulator explicitly, as collect([3, 4], 0, []), prints [3, 4], which isolates the fault to the default rather than to the recursion.
Never give a parameter a list, a dictionary or any other changeable object as its default. Use None and build the real default on the first line.
Mistake ledger (25 entries)
⚠ No base case at all
The recursive line is the idea, so the stopping line feels like boilerplate.
⚠ Removing items from a list while a for loop walks it
Removing an item shifts everything after it down one, so the for loop skips the next item. On [4, 7, 4, 4, 9] it removes two of the three 4s and reports 2, with no error.
wrong$$\texttt{for c in codes: if c == 4: codes.remove(c)}$$
right$$\texttt{i = 0};\;\texttt{while i < len(codes):}\;\texttt{... codes.pop(i) else i += 1}$$
⚠ Assigning the result of an in place sort
The commonest fault here, in two faces: L = L.sort() and L = bubble_sort(L). Both store None.
wrong$$\texttt{marks = marks.sort()}$$
right$$\texttt{marks.sort()}\;\text{on its own line}$$
⚠ Calling the recursive function twice in the combining line
A tidy looking two branch if that doubles the work at every level, turning an O(n) function into 2 to the n calls: 30 items becomes a billion. Store the recursive answer in a name and use the name twice.
Bubble sort's inner bound is len(data) - j - 1; the swap needs a temporary or a simultaneous assignment.
Merge sort
$$n\log_{2} n\ \text{comparisons} = O(n\log n),\ \text{returns a new list}$$
The halves are L[:middle] and L[middle:]; the base case returns L[:]; merge needs both drain loops.
The break even point for sorting first
$$k\,n\ \ \text{against}\ \ n\log_{2} n + k(\log_{2} n + 1),\quad k \approx \log_{2} n$$
k is the number of lookups; the list must not change between them.
Check yourself
Close the page and write, from memory: the two searches with their preconditions and their worst case costs; the three sorts with their costs and whether each one returns a list or changes yours; the two parts of a recursive function; and the one sentence that decides whether to sort at all. Then open the formula card and see which of the eight rows you could not produce.
Write a recursive function over a list from a bare specification, with the base case first and a return on every path, and say what it prints if it prints before the call rather than after it?
c-recursion
Write both linear searches and say, without looking, which one of them is wrong on the list [34, 6, 16, 35, 1] and why?
c-linear-search
Write the bisection loop from memory with the right last, the right loop condition and the right window updates, and trace it on a seven item list for a target at index 0?
c-binary-search
Say what happens to each search's cost when the list length is multiplied by ten, and give the best, average and worst case for linear search in one line each?
c-cost-of-search
Write bubble sort with the flag and selection sort with the suffix index, name the two places bubble sort's bounds go wrong, and say which of the two can finish early?
c-bubble-selection
Write merge and merge sort, say why merge needs two loops after its main loop, and say what merge_sort(L) leaves L looking like?
c-merge-sort
Decide, for a list of 50000 records and a given number of lookups, whether to sort, and say when a dictionary makes the question go away?
c-amortize
Glossary (22 terms)
algorithmalgoritma
A method for solving a problem, written so that each step is unambiguous and the method ends. It is independent of the language it is later written in, which is why the same bubble sort appears in every course.
arama algoritması
A method for finding an item with a wanted property inside a collection, or reporting that there is none. The collection can be explicit, like a list of records, or implicit, like the numbers whose square is near a given value.
linear searchdoğrusal arama
Examining the items of a list one after another from one end until the wanted item is found or the list runs out. Requires nothing of the order of the list, and costs n comparisons at worst.
bisection searchikili arama
Searching a sorted list by comparing with the item halfway along and discarding the half that cannot contain the target, then repeating on what is left. Costs the number of halvings, and gives wrong answers on a list that is not sorted.
recursionözyineleme
A function calling itself, directly or through another function, on a smaller version of its own problem. It needs a base case that answers without a further call and a recursive case that gets closer to it.
base casetaban durum
The branch of a recursive function that answers without calling itself, covering the smallest input. For a list it is usually the empty list or the single last item.
recursive caseözyinelemeli durum
The branch that combines the work for one item with the result of calling the same function on the rest. It must make the input strictly smaller and must return its value.
The pile of call frames that are waiting for the calls they made to return. A recursion n levels deep keeps n frames alive, which is the memory cost a loop does not have.
sortingsıralama
Rearranging a list so that its items are in order under some comparison. On this page in order means ascending unless a block says otherwise.
bubble sort
Repeatedly passing over the list and swapping any two neighbours that are out of order, so that each pass carries one more largest item to the end. In place, and able to stop early when a pass makes no swaps.
selection sortseçmeli sıralama
Repeatedly finding the smallest item of the part not yet settled and putting it at the front of that part. In place, and its comparison count does not depend on the order it started in.
merge sort
Sorting by splitting the list in half, sorting each half by the same method, and merging the two sorted halves. Returns a new list and costs about n log n comparisons.
mergebirleştirme
Building one sorted list out of two sorted lists by repeatedly taking whichever front item comes first, then appending whatever is left of the other list.
in placeyerinde
An operation that rearranges the object it was given rather than building a new one. An in place sort is visible to every name that points at the list, and it usually returns nothing.
best caseen iyi durum
The smallest number of steps an algorithm takes over all inputs of a given size. Almost never reported, because it usually describes one lucky input.
average caseortalama durum
The number of steps averaged over all inputs of a given size. For linear search on a list that contains the target it is about half the list.
worst caseen kötü durum
The largest number of steps over all inputs of a given size. It is the case a complexity in O form reports, and the case an exam means when it does not say otherwise.
A cost that grows like log n, meaning it goes up by a fixed amount each time the input is multiplied by a fixed factor. Doubling the list adds one comparison to a bisection search.
A cost that grows like n log n, the product of a term that grows with the input and one that grows like its logarithm. Merge sort is the example this course gives.
amortisation
Spreading the cost of one expensive operation over the many cheap operations it makes possible. Sorting once and then bisecting many times is the example: the sort is worth its price from about the log n th lookup onward.
lambda
A function written as a single expression with no name, as in lambda x, y: x < y. It takes any number of parameters and holds exactly one expression, whose value it returns.
The structure behind the dictionary, which computes from a key the one place its value could be, so that a lookup examines that place and nothing else. This is what makes a dictionary lookup O(1) and independent of how many keys there are.
What comes next
§11 · Plotting (Chapter 11)
The measured tables on this page were built by hand, printed as rows of numbers, and read with a sentence of explanation underneath. Next week the same tables become pictures, and the first thing plotted will be a growth curve like the one in this section's cost figure.
Sources
kitapJohn Guttag, Introduction to Computation and Programming Using Python, with Application to Understanding Data, Second Edition, Chapter 10 The chapter this week's syllabus line names. Its own worked code is not reproduced here; the exercises and examples on this page were written for it.
ders malzemesiThe lecture material for searching and sorting The versions of linear search, bisection search, bubble sort, selection sort and merge sort written out here follow the course's own, including the `issorted` flag, the `suffixStart` name and the `compare` parameter with its lambda default, so that what you reproduce in an exam matches what was taught. The material states it was adapted from the MIT introductory course and licensed under Creative Commons.
ders malzemesiThe eighth lab sheet and its sample run The shape of the exam example, a class with an ordering inside a container class with a bubble sort and a recursive search, comes from this sheet. No question text was reused; the data and the domain are different and the exercises here were written from scratch.
ders malzemesiTwo past papers from this course The weight given to output prediction questions was read off two real papers: a midterm where that question was 30 marks of 100, and a final where it was 20 of 100. The cover sheet of the midterm also lists the functions you are given in a closed book exam, which is why this page says what each list method returns rather than only what it does.
sabitMeasured runs on Python 3 Every output block and every comparison count on this page came out of an interpreter. Where a formula and a count disagree, the count is printed and the formula is called approximate.