Python Arena
Step 1 of 6

Big-O in practice

Two programs can give the same answer while one finishes in a blink and the other runs for an hour. The computer isn't the difference. What matters is how the work grows as the input grows: with 10 values both are instant, with a million values only one of them is.

You could time a program with a clock, but the time changes from computer to computer and with whatever else the computer is doing. Counting steps doesn't. Here three functions count how many times their innermost line runs:

def one_loop(n):
    steps = 0
    for i in range(n):
        steps += 1
    return steps


def every_pair(n):
    steps = 0
    for i in range(n):
        for j in range(i + 1, n):
            steps += 1
    return steps


def halving(n):
    steps = 0
    while n > 1:
        n = n // 2
        steps += 1
    return steps


for n in [10, 100, 1000]:
    print(n, one_loop(n), every_pair(n), halving(n))
Output
10 10 45 3
100 100 4950 6
1000 1000 499500 9

Read the columns down. one_loop grows with n: ten times the input, ten times the work. every_pair meets each pair of values once, n × (n - 1) / 2 steps: ten times the input, about a hundred times the work. halving halves n until 1 is left, so ten times the input adds only about three steps.

Big-O

Big-O notation names that growth and leaves out the details. one_loop is O(n), every_pair is O(n²) and halving is O(log n). Two more come up all the time: O(1), the same work whatever the size, and O(n log n), the cost of sorting.

Big-O n = 1,000 n = 1,000,000 For example
O(1) 1 1 values[i], len(values)
O(log n) 10 20 binary search
O(n) 1,000 1,000,000 one loop over the list
O(n log n) 10,000 20,000,000 sorted(values)
O(n²) 1,000,000 1,000,000,000,000 a loop over every pair

Big-O drops constant factors: every_pair does about n²/2 steps, and that is still O(n²). Two loops one after the other do 2n steps, still O(n). For big inputs, the growth beats any constant. Plain Python does roughly ten to fifty million simple steps a second, so an O(n²) loop over 200,000 values, 20 billion steps, runs for many minutes, while an O(n) loop over them takes a fraction of a second.

Loops you don't see

A line can be a loop in disguise. x in values on a list, values.index(x), values.count(x), values.remove(x), max(values) and sum(values) go through the items one by one: O(n) each. A slice values[a:b] copies its items, and values.pop(0) or values.insert(0, x) moves every other item by one place. Put one of them in a loop over the list and you have O(n²) without a second for. (To take items from the front again and again, use a collections.deque, the queue of the module on trees: it removes its first item in O(1).)

These two functions find the best profit from buying on one day and selling on a later day. Both are right:

def best_profit_slow(prices):
    best = 0
    for i in range(len(prices) - 1):
        best = max(best, max(prices[i + 1:]) - prices[i])
    return best


def best_profit(prices):
    best = 0
    lowest = prices[0]
    for price in prices:
        lowest = min(lowest, price)
        best = max(best, price - lowest)
    return best


prices = [9, 11, 8, 5, 7, 10, 6]
print(best_profit_slow(prices))
print(best_profit(prices))
Output
5
5

best_profit_slow has one for, but max(prices[i + 1:]) copies and scans the rest of the list in every round: O(n²) in all. best_profit remembers the lowest price so far instead, so each round is O(1): the function is O(n) time, with O(1) extra memory (two variables, whatever the length of the list). Remembering something as you go is the most common way to remove a hidden loop.

Sorting first

Sorting costs O(n log n), and it often pays: after sorting, equal and close values sit next to each other. To find out whether a list has a repeated value, comparing every pair is O(n²). In a sorted list, a repeated value sits right next to its twin, so one pass over the neighbors is enough: O(n log n) for the sort plus O(n) for the pass, O(n log n) in all.

def has_repeat(values):
    ordered = sorted(values)
    for i in range(len(ordered) - 1):
        if ordered[i] == ordered[i + 1]:
            return True
    return False


print(has_repeat([31, 7, 19, 7, 2]))
print(has_repeat([31, 7, 19, 8, 2]))
Output
True
False

sorted makes a new sorted list, O(n) extra memory, and leaves the original as it was.

Your task

Challenge 1 of 3

What does this program print? Count the steps of each function by hand, and watch how each count grows when n doubles. Type each line it prints.

Challenge 2

A record is a value greater than every value before it. The first value is always a record. This function counts the records in a list, and it gives the right answers, but max(values[:i]) is a hidden loop: it makes the function O(n²), and the hidden test has 200,000 values. Make it O(n), without changing what it returns.

For example, count_records([3, 1, 4, 1, 5, 9, 2, 6]) returns (the records are 3, 4, 5 and 9):

4

Challenge 3

Write smallest_gap(values): the smallest difference between two values of the list (the larger one minus the smaller one). The list has at least two values. Comparing every pair is O(n²), too slow for the hidden test's 100,000 values: sort first, and the closest values sit next to each other.

For example, smallest_gap([40, 7, 22, 31, 15]) returns (22 - 15):

7