Programming

Big O Notation Explained Simply

What O(1), O(log n), O(n) and O(n²) actually mean, how to read them straight from code, and real measurements that show why they matter.

Modern Age Coders Team
Modern Age Coders Team September 28, 2026
10 min read
Big O notation explained: growth curves for O(1), O(log n), O(n), O(n log n) and O(n squared)

Big O notation sounds like advanced maths, and most explanations make it look that way. It is actually one simple idea: as the amount of data grows, how much more work does your code have to do? A program that handles 100 items instantly might take a second with 100,000 items, or an hour, and Big O is the language programmers use to predict which.

This guide explains Big O without the heavy maths. Every claim is backed by code you can run: step counts measured by actually running the algorithms, and timings measured on a real machine. By the end you will be able to look at a loop and say what its Big O is, and know why it matters for exams, interviews and real programs.

What Big O actually measures

Big O does not tell you how many seconds a program takes. That depends on the computer, the language, and what else is running. Instead, it tells you the shape of the growth. If you double the input, does the work stay the same, double, or quadruple?

That is why it is written with n, the size of the input. O(n) reads as "order n" and means the work grows roughly in proportion to n. O(n²) means it grows with n times n. The O is just a label saying "we are describing the growth, not the exact count".

  • O(1), constant. Looking up the first item in a list, or a value in a dictionary. The same work whether there are 10 items or 10 million.
  • O(log n), logarithmic. Binary search in a sorted list. Each step throws away half of what is left, so a million items need only about 20 steps.
  • O(n), linear. Checking every item once, like finding the largest number in an unsorted list.
  • O(n log n). Good sorting algorithms, including the one Python's sorted() uses.
  • O(n²), quadratic. Comparing every item with every other item. Fine for 100 items, painful for 100,000.

Watching the growth: steps counted by running the code

Here are three real algorithms, each with a counter that adds one for every basic step. The program runs them on lists of increasing size and prints the counts. Nothing here is estimated.

count_steps.py
def linear_search(items, target):
    steps = 0
    for item in items:
        steps += 1
        if item == target:
            break
    return steps

def binary_search(items, target):
    steps, low, high = 0, 0, len(items) - 1
    while low <= high:
        steps += 1
        mid = (low + high) // 2
        if items[mid] == target:
            break
        if items[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return steps

def compare_all_pairs(items):
    steps = 0
    for i in range(len(items)):
        for j in range(i + 1, len(items)):
            steps += 1
    return steps

print(f"{'n':>9} {'linear':>9} {'binary':>7} {'all pairs':>14}")
for n in (10, 100, 1_000, 10_000):
    data = list(range(n))
    worst = n - 1                      # the last item: the worst case for a linear search
    print(f"{n:>9,} {linear_search(data, worst):>9,} {binary_search(data, worst):>7,} {compare_all_pairs(data):>14,}")
Output
        n    linear  binary      all pairs
       10        10       4             45
      100       100       7          4,950
    1,000     1,000      10        499,500
   10,000    10,000      14     49,995,000
Table of worst-case steps for linear search, binary search and comparing all pairs at 10, 100, 1,000 and 10,000 items
Linear grows with n. Binary barely moves. All pairs explodes.

Read down each column. Every time n gets ten times bigger, linear search does ten times more work. Binary search adds only three or four steps. Comparing all pairs does about a hundred times more work, because ten times more items means ten times more items to compare, each against ten times more others. At 10,000 items that is nearly fifty million comparisons, for a list that fits comfortably in memory.

💡

Why binary search is so fast

Binary search only works on sorted data. It looks at the middle item, decides which half the target must be in, and throws the other half away. Halving 10,000 repeatedly reaches 1 in about 14 steps, which is exactly what the counter shows. That halving is where every O(log n) comes from.

How to read Big O from code

You do not need to count steps every time. A few rules of thumb cover most code you will see in school, college and interviews.

Four rules of thumb for reading Big O from code: one loop is O(n), a nested loop is O(n squared), halving is O(log n), no loop is O(1)
Most code you will meet can be classified with these four patterns.
  1. Find the loops that depend on the input. A loop that runs once per item is O(n). A loop that always runs 10 times, whatever the input, is O(1).
  2. Nested loops multiply. A loop over n inside another loop over n is O(n × n) = O(n²). Three levels deep is O(n³).
  3. Loops one after another add. Two separate loops over the input are O(n + n) = O(2n), which is still O(n).
  4. Drop constants and smaller terms. Big O cares about what dominates when n is huge. 3n + 5 becomes O(n). n² + 100n becomes O(n²), because at large n the n² part swamps everything else.
  5. Watch for hidden loops. x in my_list looks like one step but is a loop inside Python. Put it inside your own loop and you have written O(n²) without noticing.

A real example: list or set?

That last rule causes more slow beginner programs than anything else, so it is worth seeing properly. Checking whether a value is in a Python list means looking at items one by one: O(n). Checking whether it is in a set uses a hash table to jump straight to where the value would be: O(1) on average. This program measures both, with the same data and the same thousand lookups:

list_vs_set.py
import random
from time import perf_counter

random.seed(1)
for n in (10_000, 100_000, 1_000_000):
    numbers = list(range(n))
    as_set = set(numbers)
    lookups = [random.randrange(n) for _ in range(1_000)]

    start = perf_counter()
    for x in lookups:
        x in numbers              # a list checks items one by one: O(n)
    list_time = perf_counter() - start

    start = perf_counter()
    for x in lookups:
        x in as_set               # a set jumps straight to the item: O(1) on average
    set_time = perf_counter() - start

    print(f"n = {n:>9,}   list {list_time*1000:9.2f} ms   set {set_time*1000:6.3f} ms")
Output (timings from the machine this was run on)
n =    10,000   list     67.94 ms   set  0.536 ms
n =   100,000   list   1115.18 ms   set  0.607 ms
n = 1,000,000   list  11211.89 ms   set  3.188 ms
Measured time for 1,000 lookups in a list compared with a set, at 10,000, 100,000 and 1,000,000 items
The list's time grows with n. The set's stays almost flat.

At a million items the list took about 11,212 milliseconds for a thousand lookups, and the set took about 3.2. Your exact numbers will differ, but the shape will not: the list's time grows with the size of the list, and the set's does not. That is exactly what O(n) versus O(1) predicts.

The same idea, fixing a slow program

A classic beginner task is checking whether a list contains any duplicate. The obvious solution compares every pair, which is O(n²). The better one remembers what it has already seen in a set, which is O(n). Both give the same answer:

duplicates.py
import random
from time import perf_counter

def has_duplicate_slow(items):          # O(n^2): compare every pair
    for i in range(len(items)):
        for j in range(i + 1, len(items)):
            if items[i] == items[j]:
                return True
    return False

def has_duplicate_fast(items):          # O(n): remember what you have seen
    seen = set()
    for item in items:
        if item in seen:
            return True
        seen.add(item)
    return False

random.seed(2)
data = random.sample(range(10_000_000), 5_000)    # 5,000 different numbers, so no duplicate

start = perf_counter(); slow = has_duplicate_slow(data); t_slow = perf_counter() - start
start = perf_counter(); fast = has_duplicate_fast(data); t_fast = perf_counter() - start
print("same answer:", slow == fast)
print(f"compare every pair: {t_slow:.3f} s")
print(f"use a set:          {t_fast:.4f} s")
print(f"about {t_slow / t_fast:,.0f} times faster")
Output
same answer: True
compare every pair: 1.137 s
use a set:          0.0022 s
about 513 times faster

Nothing clever happened. The fast version simply avoided doing the same work over and over. That is what most Big O improvements look like in practice: not a genius algorithm, but noticing that a loop inside a loop can become a lookup.

Big O of everyday Python operations

Cheat sheet of Big O for common Python operations: list append, membership tests, insert at the front, indexing, set and dictionary lookups, sorting, min and sum
Worth memorising. The two in orange cause most slow programs.
Operation Big O Note
list.append(x) O(1) on average Occasionally the list resizes, but averaged over many appends it is constant
list[i] O(1) Jumping to a position is instant
x in list O(n) Checks items one by one
list.insert(0, x) O(n) Every other item has to shift along
x in set, dict[key] O(1) on average Hash tables jump straight to the right place
sorted(list) O(n log n) About as fast as a general sort can be
min(), max(), sum() O(n) Each has to look at every item once

Best, worst and average case

You will sometimes see Big O described as the worst case. Linear search is O(n) because in the worst case the item is at the end, or missing, and every item gets checked. If the item happens to be first, it takes one step, but you cannot plan around luck. Interviewers and exam questions almost always mean the worst case unless they say otherwise.

Dictionaries and sets are the main place where the average case matters. Their lookups are O(1) on average, which is what you get in practice, though a deliberately badly-behaved input can make them slower. For everyday programming you can treat them as constant time.

Why it matters for exams, interviews and real code

  • School and college exams. AP Computer Science, A level and university data structures courses all expect you to compare algorithms like linear and binary search, and bubble sort against merge sort, by their growth.
  • Competitive programming. Contest problems are designed so the obvious O(n²) solution runs out of time. Our post on moving from USACO Bronze to Silver shows exactly this wall, measured.
  • Technical interviews. After you solve a problem, the next question is almost always "what is the time complexity, and can you do better?"
  • Real software. Code that works on your test file of 50 rows can fall over on a real file of 5 million. Knowing Big O is how you spot that before your users do.

Big O is not about making fast code faster. It is about not writing code that falls over when the data gets real.

How we teach it

Our data structures and algorithms course is taught live in Python, Java and C++, one to one or in small groups of 5 to 10. It follows the principles on our how we teach page, including tracing code line by line until a student can predict every step, which is the same skill that makes Big O intuitive rather than memorised.

Frequently asked questions

Big O notation describes how much more work an algorithm has to do as its input grows. O(n) means the work grows in proportion to the input, O(n squared) means it grows with the square of the input, and O(1) means it stays the same no matter how big the input gets.

O(n) means linear time: the work grows in direct proportion to the size of the input. If you double the number of items, the work roughly doubles. Looking at every item in a list once, such as finding the largest number, is O(n).

O(log n) means the work grows very slowly, because each step cuts the remaining problem in half. Binary search is the classic example: searching a sorted list of a million items takes only about 20 steps.

Not for every input size. O(1) means the time does not grow with the input, but that constant time could still be large. For big inputs, O(1) wins, which is why Big O focuses on growth rather than exact speed.

Look at the loops that depend on the input size. One loop over the input is O(n), a loop inside a loop is O(n squared), and repeatedly halving the problem is O(log n). Add loops that run one after another, multiply loops that are nested, then drop constants and smaller terms.

Because Big O describes what happens as the input becomes very large. At that scale, doing 3n steps instead of n steps matters far less than whether the work grows like n or like n squared. Constants depend on the computer and language, while the growth rate does not.

Yes. Almost every technical interview asks for the time complexity of your solution and whether it can be improved. Knowing the common patterns, such as replacing a nested loop with a set or dictionary lookup, is one of the most useful interview skills.

Modern Age Coders Team

About Modern Age Coders Team

Expert educators making coding and maths clear for ages 6 to 67.

Keep exploring Modern Age Coders

More from the blog

Free resources

From the blog

Start here

Ask Misti AI
Chat with us
Enroll Watch Class Priority Demo Enrol Book a Demo Watch Class WhatsApp Book demo today