Index Ai Academy
Phase 7 · Software engineering · How computers run code

Algorithm complexity

Count the work code does, and see why some programs stop scaling.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

AI work means big inputs: a million customer rows, a hundred thousand documents to search, a context window of a million tokens. Code that feels instant on a test file can freeze on the real thing. Big O is the tool engineers use to see that coming before it happens, by asking one question: when the input doubles, what happens to the work?

What you'll be able to do

  • Count how the work a function does grows as its input grows
  • Name the common growth rates, from O(1) to O(n²), and spot each in code
  • Pick a data structure that turns slow code into fast code
  • Explain why attention's cost limits how long a model's context can be

What it covers

  1. Count steps, not seconds
  2. The growth rates you'll meet
  3. The right data structure changes everything
  4. From quadratic to linear
  5. Big O in AI systems
  6. What Big O doesn't tell you

Count steps, not seconds

How long does a recipe take? That depends on who's cooking. How much more work is it for 100 guests than for 10? That's a better question, because the answer holds in any kitchen.

Timing code with a stopwatch tells you about one machine on one day. A faster laptop changes the answer. So engineers count steps instead, and ask how the count grows with the size of the input, called n.

Here's a search through a list that counts its comparisons:

def find(items, target):
    steps = 0
    for item in items:
        steps += 1
        if item == target:
            return steps
    return steps

for n in [10, 100, 1000, 10000]:
    items = list(range(n))
    print(n, "items ->", find(items, -1), "comparisons (worst case: not found)")
It runs right here in your browser, with nothing to install.

Ten times the items means ten times the comparisons. The work grows in a straight line with n. We write that as O(n), said "order n" or "big O of n".

The growth rates you'll meet

Big O describes how the work grows, not the exact count. It keeps only the biggest term and drops constant factors. Code that does 3n + 7 steps is O(n): when n is a million, the 7 doesn't matter and the 3 doesn't change the shape.

Picture a party. Calling out one announcement is O(1), however many guests there are. Greeting each guest once is O(n). Everyone shaking hands with everyone else is O(n²), and it gets out of hand fast.

Big Onameexamplesteps for n = 1,000for n = 1,000,000
O(1)constantlook up a key in a dict11
O(log n)logarithmicbinary search in a sorted list1020
O(n)linearone loop over the items1,0001,000,000
O(n log n)"n log n"a good sort10,00020,000,000
O(n²)quadraticcompare every item with every other1,000,0001,000,000,000,000

Look at the last row. A quadratic step that takes a second on 1,000 items takes about twelve days on a million. That's the bug Big O helps you catch: code that works on your sample and dies on the full data.

In plain words: O(1) doesn't grow at all. O(n) doubles when the input doubles. O(n²) quadruples when the input doubles.

How do you tell from the code? Roughly:

  • one loop over the input: O(n);
  • a loop inside a loop, both over the input: O(n²);
  • halving the problem each step: O(log n);
  • a fixed number of steps, whatever the input: O(1).
Try it now

Slide to a million: O(n²) takes about 11 days at that speed, and O(n log n) about 20 seconds.

Quick check
What is the Big O of this function?
def all_pairs(names):
    pairs = []
    for a in names:
        for b in names:
            if a < b:
                pairs.append((a, b))
    return pairs
  1. O(n)
  2. O(log n)
  3. O(n²)
  4. O(1)
Show the answer

O(n²)

A loop over the names inside another loop over the names: n × n checks, which is O(n²). The if doesn't change the count of checks, only how many pairs are kept.

The right data structure changes everything

Finding one letter in an unsorted pile means going through the pile. Finding it in a wall of labeled pigeonholes means walking straight to the right one. Checking x in some_list is the pile: it walks the list item by item, which is O(n). Checking x in some_set is the wall: it jumps straight to the answer, O(1) on average. A set and a dict use a hash table, which the next lesson builds from scratch. For now, feel the difference:

import time

n = 20000
as_list = list(range(n))
as_set = set(as_list)
lookups = range(n - 500, n)          # 500 values near the end of the list

start = time.perf_counter()
hits = sum(1 for x in lookups if x in as_list)
list_time = time.perf_counter() - start

start = time.perf_counter()
hits = sum(1 for x in lookups if x in as_set)
set_time = time.perf_counter() - start

print("list:", round(list_time * 1000, 1), "ms")
print("set: ", round(set_time * 1000, 2), "ms")
print("set is about", round(list_time / set_time), "times faster here")

The exact numbers depend on your machine. The gap is the point, and it widens as n grows.

Halving: where log n comes from

Guess a number between 1 and 1,000,000, and after each guess you're told "higher" or "lower". Always guess the middle and you halve what's left each time. It takes at most 20 guesses, because halving a million 20 times gets you to one. That count is the logarithm base 2 of n, and it grows very slowly: a billion items need only 30 halvings.

n = 1_000_000
halvings = 0
while n > 1:
    n = n // 2
    halvings += 1
print(halvings, "halvings to go from a million to one")
Quick check
How many times can you halve a million before reaching 1? What does this print?
n, steps = 1_000_000, 0
while n > 1:
    n //= 2
    steps += 1
print(steps)
  1. 1000
  2. 19
  3. 500000
  4. 6
Show the answer

19

Halving a million with // takes 19 steps to reach 1, close to log₂(1,000,000) ≈ 20. That's why binary search finds one item in a million in about 20 comparisons.

Worked example

From quadratic to linear

Two ways to find whether any email appears twice in a list. Both are correct. Count how many comparisons each makes:

def has_dupes_slow(emails):
    checks = 0
    for i in range(len(emails)):
        for j in range(i + 1, len(emails)):
            checks += 1
            if emails[i] == emails[j]:
                return True, checks
    return False, checks

def has_dupes_fast(emails):
    seen = set()
    checks = 0
    for e in emails:
        checks += 1
        if e in seen:
            return True, checks
        seen.add(e)
    return False, checks

emails = [f"user{i}@shop.test" for i in range(2000)]
print("slow:", has_dupes_slow(emails))
print("fast:", has_dupes_fast(emails))
In the lesson you can change this and run it.

The slow version compares every pair: about 2,000 × 2,000 / 2, which is nearly two million checks. The fast version looks at each email once and remembers what it has seen, so it makes 2,000 checks. It does use more memory: the set holds up to n items. Trading memory for time like this is one of the most common moves in programming. Big O for memory is called space complexity, and here it's O(n).

Big O in AI systems

The same thinking explains limits you met earlier in the course:

  • Attention compares every token with every other token, the handshakes at the party again, so its cost is O(n²) in the length of the context. Doubling the context roughly quadruples that part of the work. That's why long context costs more, and why researchers work on cheaper attention.
  • Similarity search that compares a question with every stored embedding is O(n). That's fine for ten thousand documents and too slow for a billion, which is why vector databases build an index that skips most of them.
  • Generating text takes one model step per output token, so it's O(n) in the reply's length. Long answers take longer.
Quick check
A model's attention cost is O(n²) in context length. You go from 10,000 tokens to 40,000. Roughly how much more attention work is that?
  1. 4 times
  2. The same
  3. 16 times
  4. 2 times
Show the answer

16 times

The context is 4 times longer, and quadratic cost grows with the square: 4 × 4 = 16.

What Big O doesn't tell you

It isn't seconds. Big O describes how the work grows as the input grows, on any machine. A faster laptop changes the seconds and leaves the Big O alone. That's why it's useful: it predicts what happens at ten times the data, before you get there.

Constants get dropped. 3n + 7 and n are both O(n), because at a large size the shape of the curve matters more than the multiplier. So O(n) code can lose to O(n²) code on small inputs, and you shouldn't care. It's the large inputs that hurt.

A quick test on a small sample proves little. A dinner for four tells you nothing about catering a wedding. Quadratic code looks fine on 100 rows. On a million rows it takes about 100 million times as long. This is the classic way software works on a laptop and falls over in production, so try your code on realistic sizes before you trust it.

in isn't one thing. x in my_list checks items one at a time, which is O(n). x in my_set goes almost straight there, which is O(1). Inside a loop, that one word decides whether your program takes seconds or hours.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

A function does 5n + 100 steps for an input of size n. What is its Big O?

  1. O(n)
  2. O(5n + 100)
  3. O(1)
  4. O(n²)

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson