Chapter 11

Algorithms & Big-O Complexity

High School
At a glance
Core ideaBig-O captures how cost grows with input size, not stopwatch time.
Key termO(log n) — halve the search space each step, like binary search.
You can…Compare algorithms by growth class and read their trade-offs.
Watch outConstants are hidden — for small n a "worse" algorithm may win.
Theory

An algorithm is a finite, unambiguous procedure that solves a class of problems. Two algorithms may both be correct yet differ enormously in cost. Big-O notation describes how an algorithm's running time (or memory) grows as the input size n grows, ignoring constant factors and lower-order terms — it captures the shape of growth, not the stopwatch value.

Formally, f(n) = O(g(n)) means there exist constants c and n₀ such that f(n) ≤ c·g(n) for all n ≥ n₀. The common growth classes, from best to worst:

Big-ONameExample
O(1)ConstantArray index; hash lookup (avg)
O(log n)LogarithmicBinary search
O(n)LinearScan a list once
O(n log n)LinearithmicMerge sort, heap sort
O(n²)QuadraticBubble / insertion sort
O(2ⁿ)ExponentialNaive subset enumeration
input size (n) → time → O(1) O(log n) O(n) O(n log n) O(n²)
Growth rate is what matters at scale: O(1) barely rises, while O(n²) shoots upward far faster than O(n log n) or O(n).
1
O(1) steps at n = 1,000,000
~20
O(log n) steps at n = 1,000,000
1 million
O(n) steps at n = 1,000,000
~20 million
O(n log n) steps
10¹²
O(n²) steps — a trillion
2¹⁰⁰⁰⁰⁰⁰
O(2ⁿ) — physically impossible
Explanation

Why ignore constants? Because for large n, growth rate dominates everything. An O(n²) algorithm that is heavily optimised will still lose to a sloppy O(n log n) one once n is big enough — and in the real world, n is always getting bigger. Big-O lets you predict scalability without benchmarking every machine.

The star example is binary search. To find a name in a sorted phone book of a million entries, linear search may take a million comparisons; binary search — repeatedly halving the range — takes at most twenty (since 2²⁰ > 10⁶). That is the practical power of O(log n): doubling the data adds just one more step.

target = 7 STEP 1 1 2 3 4 5 6 7 8 9 mid=5 < 7 → keep right half STEP 2 6 7 8 9 mid=7 → found! 9 items found in ~log₂9 ≈ 4 comparisons; each step throws away half the array.
Binary search halves the remaining range at every step — logarithmic growth in action.

Caution. Big-O hides constants, so for small inputs a "worse" algorithm can win. It is a statement about the limit as n → ∞, not a promise about your particular case. Measure when it matters.

Practical

We contrast linear and binary search, then implement merge sort — an O(n log n) divide-and-conquer sort — and count operations to feel the difference.

Step 1SplitDivide the list into two halves.
Step 2RecurseSort each half the same way, down to single items.
Step 3MergeWeave two sorted halves into one, in linear time.
Step 4Sortedlog n levels × O(n) merging = O(n log n) total.
def linear_search(arr, target):       # O(n)
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

def binary_search(sorted_arr, target):    # O(log n) -- requires sorted input
    lo, hi = 0, len(sorted_arr) - 1
    while lo <= hi:
        mid = (lo + hi) // 2              # look at the middle
        if sorted_arr[mid] == target:
            return mid
        elif sorted_arr[mid] < target:
            lo = mid + 1                 # discard the lower half
        else:
            hi = mid - 1                 # discard the upper half
    return -1

def merge_sort(a):                        # O(n log n)
    if len(a) <= 1:
        return a                          # base case: already sorted
    mid = len(a) // 2
    left  = merge_sort(a[:mid])           # sort each half (divide)
    right = merge_sort(a[mid:])
    # merge two sorted halves in linear time (conquer):
    result, i, j = [], 0, 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i]); i += 1
        else:
            result.append(right[j]); j += 1
    result.extend(left[i:]); result.extend(right[j:])
    return result

data = [9, 3, 7, 1, 8, 2, 6, 5, 4]
ordered = merge_sort(data)
print(ordered)                            # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(binary_search(ordered, 7))          # 6  (found in ~log2(9) ≈ 4 steps)

Merge sort splits the list in half (log n levels of splitting) and merges each level in O(n) work — multiplying to O(n log n). It beats bubble sort's O(n²) by a factor of n/log n, which for a million items is about 50,000×.

Q&A
Q1 Why do we drop constants and lower-order terms in Big-O?

Because Big-O describes asymptotic growth — behaviour as n becomes large. There, the fastest-growing term swamps the rest: 3n² + 100n + 5000 is O(n²), because for big n the term dwarfs the others, and the constant 3 doesn't change the shape of the curve. Big-O compares scalability, not exact timings.

Q2 What is the catch with binary search?

It only works on sorted data. If you must first sort (O(n log n)) to then search once, linear search (O(n)) may be cheaper. Binary search pays off when you sort once and search many times, amortising the sorting cost across all the fast lookups.

Q3 Can every problem be solved efficiently if we are clever enough?

No — and this is one of the deepest open questions in science. Some problems (the NP-complete class, like the travelling salesman) have no known polynomial-time algorithm, and the famous P vs NP question asks whether one can exist. Most researchers believe P ≠ NP: some problems are inherently hard, not merely awaiting a clever trick.

Q4 Does O(n log n) beat O(n²) for every input?

Only for large enough n. For a handful of elements, a simple O(n²) insertion sort can outrun merge sort because it has tiny constant overhead and good cache behaviour. Real library sorts exploit this by switching to insertion sort on small sub-arrays — a hybrid called Timsort or introsort.

Concept mind map

How the ideas connect

Every key idea in this chapter, branching from the core concept — use it to see the whole picture at a glance.

O(1)O(log n)O(n)O(n log n)O(n^2)growth not secondsBig-O complexity
Infographic

The key facts, visualised

O(1)
Constant: cost does not grow with input size.
O(log n)
Halve the search space each step, like binary search.
O(n)
Linear: one pass over the input.
O(n^2)
Quadratic: a loop inside a loop over the input.
Solved examples

Worked problems, step by step

Follow each solution line by line, then try to reproduce it on paper before moving on.

Example 1Give the big-O of a single loop: for i in 1..n: do one step.

  1. The body runs once per element.
  2. That is n steps.

Example 2Give the big-O of nested loops: for i in 1..n: for j in 1..n: step.

  1. Inner loop runs n times.
  2. Outer loop runs it n times, so n * n.
Practice problem set

Now you try

Work each one out first, then tap to reveal the worked answer.

1What does big-O measure?
How an algorithm cost grows with input size, not the stopwatch time on a particular machine.
2What is O(log n) and where does it appear?
Cost that grows by one step each time the input doubles; binary search on sorted data is O(log n).
3Which is faster for large n, O(n) or O(n^2)?
O(n) grows far slower than O(n^2), so it is faster for large inputs.
4What is the big-O of accessing an array element by index?
O(1), constant time.
5What complexity do good comparison sorts like mergesort have?
O(n log n).
6Why ignore constants in big-O?
Big-O captures growth class; constants and lower-order terms do not change how cost scales as n grows large.