The cost you asked for is not the cost you got
Correct output, wrong cost. Learn to read a body for the work it hides, and to count that work exactly rather than guess at it.
The bug that passes every test
You asked for a linear solution. What came back is correct on every input you tried, reads cleanly, and is quadratic. There is no failing test to find, because there is no wrong answer. The defect only exists at a size your tests never reach, and it shows up in production as a job that used to take ninety seconds and now takes forty minutes.
This is the one failure signature you cannot catch by comparing outputs. You have to read for it.
Where the hidden loop lives
Python hides iteration behind operators and methods that look like single steps. Four shapes account for nearly all of it.
Membership in a list.
seen = []
for n in nums:
if n in seen: # scans every element of seen
return True
seen.append(n)
n in seen is a loop wearing a keyword. Inside another loop it is quadratic. n in some_set and key in some_dict are not, which is why the fix is usually a one-word change.
Removing from the front of a list.
while queue:
job = queue.pop(0) # shifts every remaining element left by one
pop(0) is linear because a list is a contiguous block. collections.deque has popleft for exactly this.
Building a string by adding to it.
out = ""
for line in lines:
out = out + line + "\n" # allocates a new string of the full length, every time
Strings are immutable, so each round copies everything accumulated so far. Ten thousand lines of eighty characters copies about four billion characters. "".join(parts) copies each character once.
Sorting inside a loop.
for user in users:
ranked = sorted(scores) # the same sort, n times
...
Correct, and it repeats identical work per iteration. Hoisting it above the loop is usually the whole fix.
Count it, do not estimate it
Big O is the right language for a review comment and the wrong tool for convincing yourself. When you want to know whether a body is really quadratic, count the operations exactly for a small input. It takes a minute and it is not arguable.
def scan_steps(nums):
"""How many element comparisons does 'n in seen' actually perform?"""
seen = []
steps = 0
for n in nums:
for candidate in seen: # what 'in' does, written out
steps += 1
if candidate == n:
return steps # 'in' stops at the first match, and so does the outer loop
seen.append(n)
return steps
Writing the hidden loop out by hand is the technique. Once in is a visible for, the cost is not a judgment call, and details you would have hand-waved past become obvious: the scan stops at the first match, and finding a duplicate ends the whole function early. That is why a list full of duplicates is fast and a list of distinct values is the worst case.
Each snippet is the body of a loop over n items. Sort each by the total work it does.
Say it as a review comment
"This is O(n squared)" invites a debate about whether n is ever large. The version that gets fixed names the line, the input size, and the swap.
seenis a list, son in seenon line 6 scans it. On the 200k-row import that is about twenty billion comparisons. Makingseena set is a one-word change and the rest of the function is unaffected.
Interview nuance: when you are asked to review a solution, cost is the dimension most candidates skip entirely, so raising it is disproportionately valuable. The strongest form is not the label but the mechanism: "this reads as one pass, but the membership test on line 6 is itself a scan, so it is quadratic." Anyone can recite complexity classes. Pointing at the line that creates the hidden loop is what shows you read the code.
def scan_steps(nums):
seen = []
steps = 0
for n in nums:
for candidate in seen: # what 'n in seen' really does
steps += 1
if candidate == n:
return steps
seen.append(n)
return steps
print(scan_steps([1, 2, 3])) # 3
print(scan_steps(list(range(50)))) # 1225Apply
Your turn
The task this lesson builds to.
Write a function scan_steps(nums) that returns the exact number of element comparisons
the membership test in has_duplicate performs on nums.
has_duplicate is in the starter. It keeps a list called seen and asks if n in seen. That test
walks seen from the front, comparing one element at a time, and stops as soon as it finds a match.
When it finds one, has_duplicate returns immediately and no further comparisons happen.
Count every comparison the test performs across the whole call. Keep scan_steps as the last
function in the file.
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Your nightly export builds a CSV in memory with report = report + row + "\n" inside a
loop, and it has started missing its window. Before you change anything you want a number to put in
the ticket, because "string concatenation is slow" has already been dismissed once as premature
optimisation.
Write a function rows_within_budget(row_length, budget) that returns the largest number of rows
the loop can append before the total characters it has copied would exceed budget.
Every row has exactly row_length characters, and the loop adds a newline to each, so appending a
row produces a new string one row longer than the previous one. The characters copied by an append
is the length of the string it produces. Count zero rows as costing nothing.
3 hints and 4 automated checks are waiting in the workspace.