root.system / 0x15 / big-o

Same answer.
317 years apart.

Two programs. Both correct. Both return the right result. One finishes in a second. One finishes after the sun has died. The difference is not the hardware, not the language. It is the shape of the algorithm. Big O is how you describe that shape, and once you can read it you see the skeleton of every system ever built.

Two programmers solve the same problem.

Both are correct. Both return the exact same answer.

One finishes before you blink. The other is still running when the sun burns out.

Same answer. The difference is not the language, not the laptop, not how clever the code looks.

Its the shape.

Every algorithm has a shape: how the work grows as the input grows. Double the data. Does the cost stay flat? Double? Square? Explode?

Big O is the language for that shape. Not seconds. Not megahertz. Growth.

And once you can read it, you start seeing it everywhere youve already been.

The array that hands you element ten million as fast as element zero, that was O(1), page eleven. The search that throws away half the haystack with every guess, O(log n). The hash that finds a key without scanning the rest, O(1) again, page thirteen.

You were doing Big O the whole time. You just didnt have the word for it.

Heres the word.

Beginner// level 01

What Big O actually measures

Big O is not about speed. Most people get this wrong immediately. Big O is not about how fast your code runs; it is about how your code scales. What happens when the input doubles? Does the time double? Quadruple? Barely change? Explode? That question is what Big O answers.

You already know every data structure where this matters: arrays, linked lists, hash maps, recursion, hashing, the blockchain. Big O is the thread that runs through all of them, the lens that makes the curriculum coherent, and the tool that separates programmers who write correct code from programmers who write correct code that actually works at scale.

Big O notation describes how the number of operations in an algorithm grows relative to the size of its input. Not the exact number: the growth rate, the shape of the curve, as input approaches infinity.

The complexity classes, fastest to slowest

O(1) · constant
Input doubles, time unchanged
A fixed number of steps regardless of input size. Array index access, hash map lookup, reading the first element. The most powerful complexity class: achieve it whenever you can.
O(log n) · logarithmic
Input doubles, one more step
Eliminates half the possibilities on each step. Binary search, balanced BST lookup. 1 billion elements is 30 steps; 2 billion is 31. The most underrated complexity in computing.
O(n) · linear
Input doubles, time doubles
Visits each element once. Reading an array, linear search, printing every element. Fair, predictable, often unavoidable.
O(n log n) · linearithmic
Input doubles, slightly more than doubles
Merge sort, quicksort, heap sort. The best possible for comparison-based sorting: this is the sorting ceiling. Fast in practice, elegant in theory.
O(n²) · quadratic
Input doubles, time quadruples
Bubble sort, nested loops, comparing every pair. 1,000 elements is a million operations; 10,000 is a hundred million. A warning sign in production code.
O(2ⁿ) · exponential
Input plus one, time doubles
Naive recursive Fibonacci, brute-force cracking, subset enumeration. 30 elements is a billion operations; 64 is more than the atoms in the universe. The complexity class of doom.
O(n!) · factorial
The boundary of the impossible
Travelling-salesman brute force. 20 cities is 2.4 quintillion routes. Never acceptable at scale; it exists to mark the edge of what is computationally possible.

How they grow

nO(1)O(log n)O(n)O(n log n)O(n²)O(2ⁿ)
1111112
101310331001,024
1001710066410,0001.3 × 10³⁰
1,0001101,0009,9661,000,00010³⁰¹
10,00011310,000132,877100,000,00010³⁰¹⁰

The four rules

rule 1
Drop constants
O(2n) is O(n). O(5n²) is O(n²). We care about growth rate, not multipliers. The constant depends on the machine; the growth rate does not.
rule 2
Drop lower-order terms
O(n² + n) is O(n²). For large n the dominant term swallows the rest. At n = 1,000,000, n² is 10¹² and n is 10⁶: the n is irrelevant.
rule 3
Worst case by default
O(n) for linear search means the worst case: the element is last. Best case it is first, O(1). We design for the worst case, because that is what users eventually hit.
rule 4
Space complexity exists too
Not just time. O(1) space uses fixed memory regardless of input; O(n) space grows with it. Recursive algorithms often use O(n) space for the call stack.

Four complexities, one screen

Rust• • •
// Each function below has a different Big O.
// Same types, same inputs, radically different scales.

// O(1): constant time. 10 or 10 billion elements, same cost.
fn first_element(arr: &[i32]) -> Option<i32> {
    arr.first().copied()
    // one operation; the array size is irrelevant
}

// O(n): linear time. Every element visited once.
fn linear_search(arr: &[i32], target: i32) -> Option<usize> {
    for (i, &val) in arr.iter().enumerate() {
        if val == target { return Some(i); }
    }
    None
    // worst case: target is last or absent, n comparisons
}

// O(n²): quadratic. For every element, visit every other.
fn has_duplicate(arr: &[i32]) -> bool {
    for i in 0..arr.len() {
        for j in (i + 1)..arr.len() {
            if arr[i] == arr[j] { return true; }
        }
    }
    false
    // n × n comparisons; 10,000 elements = 50 million
}

// O(log n): binary search. Sorted input, halve each step.
fn binary_search(arr: &[i32], target: i32) -> Option<usize> {
    let (mut lo, mut hi) = (0, arr.len());
    while lo < hi {
        let mid = lo + (hi - lo) / 2;
        match arr[mid].cmp(&target) {
            std::cmp::Ordering::Equal   => return Some(mid),
            std::cmp::Ordering::Less    => lo = mid + 1,
            std::cmp::Ordering::Greater => hi = mid,
        }
    }
    None
    // 1 billion elements: at most 30 iterations
}
C• • •
#include <stddef.h>
#include <stdint.h>

/* O(1): constant */
int first_element(const int *arr, size_t n) {
    if (n == 0) return -1;
    return arr[0];               /* one read, regardless of n */
}

/* O(n): linear */
int linear_search(const int *arr, size_t n, int target) {
    for (size_t i = 0; i < n; i++)
        if (arr[i] == target) return (int)i;
    return -1;                   /* worst case: n comparisons */
}

/* O(n²): quadratic */
int has_duplicate(const int *arr, size_t n) {
    for (size_t i = 0; i < n; i++)
        for (size_t j = i + 1; j < n; j++)
            if (arr[i] == arr[j]) return 1;
    return 0;                    /* n × n/2 comparisons */
}

/* O(log n): binary search (array must be sorted) */
int binary_search(const int *arr, size_t n, int target) {
    size_t lo = 0, hi = n;
    while (lo < hi) {
        size_t mid = lo + (hi - lo) / 2;
        if      (arr[mid] == target) return (int)mid;
        else if (arr[mid] <  target) lo = mid + 1;
        else                          hi = mid;
    }
    return -1;                   /* log2(1e9) is about 30 steps */
}

Race the algorithms

Drag n and watch the complexity classes pull apart. The bars are log-scaled so they all stay visible, but the operation counts and time estimates are real. Then flip on Bitcoin mode to see the one gap that secures the entire network.

// race the algorithms

O(2ⁿ) is hidden above n = 30. At n = 64 it already exceeds the number of atoms in the universe.

O(1) · array access1 ops
Hash map lookup in Bitcoin's UTXO set1 ns
O(log n) · binary search10 ops
Binary search in a sorted array10 ns
O(n) · linear search1,000 ops
Reading every element once1.0 µs
O(n log n) · merge sort9,966 ops
Sorting with merge sort10.0 µs
O(n²) · bubble sort1,000,000 ops
Bubble sort on this input1.0 ms

drag n and watch the bars diverge (widths are log-scaled so every class stays visible). times assume a billion operations per second. flip bitcoin mode to add SHA-256: O(1) to compute, O(2²⁵⁶) to reverse. that gap is the entire security model.

Intermediate// level 02

Big O in the data structures you know

You have already built every data structure on this site. Now see the exact Big O of every operation on each one. This is the lookup table you will use for the rest of your career.

Arrays

operationcomplexitywhy
access by indexO(1)base + i × size is one CPU instruction
search (unsorted)O(n)must check every element
search (sorted, binary)O(log n)eliminates half each step
insert at endO(1) amortisedappend is one write (plus rare regrowth)
insert at beginningO(n)must shift every element right
delete at indexO(n)must shift elements to fill the gap
// why arrays dominate hot paths
O(1) access plus a cache-friendly contiguous layout. Two properties nothing else matches simultaneously. The arrays page is where that layout came from.

Linked lists

operationcomplexitywhy
access by indexO(n)follow the pointer chain from the head
searchO(n)traverse from the head
insert at headO(1)just update the head pointer
insert at tailO(1) / O(n)O(1) with a tail pointer, O(n) without
insert at middleO(n)O(n) to find, O(1) to splice
delete at headO(1)update the head pointer
// the hidden truth
Traversal is the same O(n) as an array, but 10 to 100 times slower in practice. Not because of Big O, because of cache misses. The memory page explains why. Big O measures operations; it does not measure where those operations land in the cache hierarchy.

Hash maps

operationcomplexitywhy
lookupO(1) averagehash the key, jump to the slot
insertO(1) averagehash the key, write the slot
deleteO(1) averagehash the key, clear the slot
worst case (all collide)O(n)degenerates to a chain walk; good hash functions prevent it
// the O(1) miracle
Every Python dict, every JS object, every Rust HashMap, Bitcoin's UTXO set with 85 million entries: all O(1) lookup. The hashing page explained the mechanism; Big O explains why it matters. O(1) versus O(n) at 85 million entries is the difference between a working node and an unusable one.

Recursion

algorithmtimespace
factorial (recursive)O(n)O(n) stack frames
fibonacci (naive)O(2ⁿ)O(n)
fibonacci (memoised)O(n)O(n)
binary search (recursive)O(log n)O(log n)
merge sortO(n log n)O(n)
// the warning was a complexity class
The recursion page showed why naive Fibonacci kills your program. Big O is the reason: O(2ⁿ) means 30 elements is a billion calls. The complexity class was the warning sign.

One problem, three complexities

Duplicate detection, three ways: the nested-loop O(n²), the sort-then-scan O(n log n), and the hash-set O(n). Same answer every time; the only difference is the shape, and at ten million elements that shape is the difference between instant and never.

Rust• • •
use std::collections::HashSet;

// O(n²): check every pair. Simple, slow at scale.
fn has_dup_quadratic(arr: &[i32]) -> bool {
    for i in 0..arr.len() {
        for j in (i + 1)..arr.len() {
            if arr[i] == arr[j] { return true; }
        }
    }
    false
    // n = 10,000 -> 50 million comparisons
}

// O(n log n): sort first, then scan neighbours.
fn has_dup_sort(arr: &[i32]) -> bool {
    let mut sorted = arr.to_vec();
    sorted.sort_unstable();                  // O(n log n)
    sorted.windows(2).any(|w| w[0] == w[1])  // O(n)
    // total: O(n log n) time, O(n) space for the copy
}

// O(n): a hash set. Spend memory to buy speed.
fn has_dup_hash(arr: &[i32]) -> bool {
    let mut seen = HashSet::with_capacity(arr.len());
    arr.iter().any(|x| !seen.insert(x))
    // O(1) insert per element, O(n) total
    // n = 10,000,000 -> 10 million ops, not 50 trillion
}
C• • •
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

/* O(n²): nested loops */
int has_dup_quadratic(const int *arr, size_t n) {
    for (size_t i = 0; i < n; i++)
        for (size_t j = i + 1; j < n; j++)
            if (arr[i] == arr[j]) return 1;
    return 0;
}

/* O(n log n): sort then scan */
static int cmp_int(const void *a, const void *b) {
    return (*(const int*)a > *(const int*)b)
         - (*(const int*)a < *(const int*)b);
}
int has_dup_sort(const int *arr, size_t n) {
    int *copy = malloc(n * sizeof *copy);
    if (!copy) return -1;
    memcpy(copy, arr, n * sizeof *copy);
    qsort(copy, n, sizeof *copy, cmp_int);   /* O(n log n) */
    int found = 0;
    for (size_t i = 1; i < n; i++)
        if (copy[i] == copy[i - 1]) { found = 1; break; }
    free(copy);
    return found;
}

/* O(n) average: open-addressing hash set */
#define HASH_SIZE (1 << 20)   /* power of two for fast modulo */
int has_dup_hash(const int *arr, size_t n) {
    int *table = malloc(HASH_SIZE * sizeof *table);
    if (!table) return -1;
    for (size_t i = 0; i < HASH_SIZE; i++) table[i] = INT_MIN;

    int found = 0;
    for (size_t i = 0; i < n && !found; i++) {
        unsigned slot = (unsigned)arr[i] & (HASH_SIZE - 1);
        while (table[slot] != INT_MIN) {
            if (table[slot] == arr[i]) { found = 1; break; }
            slot = (slot + 1) & (HASH_SIZE - 1);   /* probe */
        }
        if (!found) table[slot] = arr[i];
    }
    free(table);
    return found;                /* O(1) per element average */
}
Advanced// level 03

Amortised cost, space, and where Big O meets security

Worst case is not always the right measure. Sometimes an operation is occasionally expensive but cheap on average. Amortised analysis accounts for that. And space complexity is the dimension most tutorials skip. Both matter in production.

Amortised analysis: the growing array

When a Vec or ArrayList runs out of capacity it allocates a new buffer (twice the size), copies all elements over, and frees the old one. That copy is O(n), expensive, but it happens rarely. Between copies, every append is O(1). Over n appends the total copy work is 1 + 2 + 4 + ... + n = 2n, so total work is 3n = O(n) and the average per append is O(1) amortised, despite the occasional O(n) spike.

Rust• • •
// Dynamic array growth: the canonical amortised O(1).
fn amortised_example() {
    let mut v: Vec<i32> = Vec::new(); // capacity starts at 0

    for i in 0..16 {
        v.push(i);
        // capacity doubles when full: 0 -> 1 -> 2 -> 4 -> 8 -> 16
        // each doubling copies all elements (an O(n) spike)
        // but spread over all pushes, it averages to O(1)
        println!("len={:2} cap={:2}", v.len(), v.capacity());
    }
    // total copies across 16 pushes: 1+2+4+8 = 15
    // average extra work per push: about 1 copy
    // O(1) amortised
}
C• • •
#include <stdlib.h>

typedef struct {
    int    *data;
    size_t  len;
    size_t  cap;
} DynArray;

void push(DynArray *a, int val) {
    if (a->len == a->cap) {
        /* capacity exhausted: double it */
        size_t new_cap = a->cap ? a->cap * 2 : 1;
        a->data = realloc(a->data, new_cap * sizeof *a->data);
        /* this realloc is O(n), but it happens rarely;
         * amortised over all pushes it is O(1) */
        a->cap = new_cap;
    }
    a->data[a->len++] = val;
}

Space complexity and the time-space tradeoff

Space complexity uses the same notation. O(1) space means fixed memory regardless of input (in-place sorting with swaps). O(n) space means it grows with the input (a copy of the array, a hash set of n entries, a recursion stack n deep). O(n²) space is rare and almost always a bug.

The three duplicate-detection functions are the tradeoff in miniature:

approachtimespace
nested loopsO(n²)O(1)
sort then scanO(n log n)O(n)
hash setO(n)O(n)

Spending memory to buy time is one of the fundamental engineering tradeoffs. Bitcoin's UTXO set is the most expensive example in existence: roughly 8GB of RAM to achieve O(1) validation. The alternative would be to scan every past transaction for every new one, O(n) per validation across millions of validations. That is not Bitcoin; that is a database that cannot scale.

Big O in Bitcoin's security

The single most important Big O relationship in applied computing is an asymmetry, and it is deliberate. It is the security model.

operationcomplexitywho can do it
compute a SHA-256 hashO(1)everyone, in nanoseconds
verify a hashO(1)everyone, in nanoseconds
find a valid nonceO(2⁷⁰)only miners, with electricity
reverse SHA-256O(2²⁵⁶)nobody, ever
Rust• • •
use sha2::{Sha256, Digest};

// O(1): compute SHA-256. Always 64 rounds, any input.
fn hash_block(data: &[u8; 32]) -> [u8; 32] {
    let mut h = Sha256::new();
    h.update(data);
    h.finalize().into()
    // fixed operations regardless of content
}

// O(1): verify. Same cost as computing, then compare.
fn verify_hash(data: &[u8; 32], expected: &[u8; 32]) -> bool {
    hash_block(data) == *expected
}

// O(2^difficulty): mining. No algorithm, only brute force.
fn mine(mut header: [u8; 32], difficulty: u32) -> ([u8; 32], u64) {
    let mut nonce: u64 = 0;
    loop {
        header[24..32].copy_from_slice(&nonce.to_le_bytes());
        let hash = hash_block(&header);
        if hash[0..(difficulty / 8) as usize].iter().all(|&b| b == 0) {
            return (hash, nonce);
        }
        nonce += 1;
        // average iterations: 2^difficulty
        // Bitcoin's current difficulty is around 2^70
    }
}
C• • •
#include <stdint.h>
#include <string.h>
#include <openssl/sha.h>

/* O(1): fixed 64-round SHA-256 */
void hash_block(const uint8_t in[32], uint8_t out[32]) {
    SHA256(in, 32, out);
}

/* O(1): verify */
int verify_hash(const uint8_t data[32], const uint8_t expected[32]) {
    uint8_t computed[32];
    hash_block(data, computed);
    return memcmp(computed, expected, 32) == 0;
}

/* O(2^difficulty): no algorithm, only the loop */
uint64_t mine(uint8_t header[32], uint32_t difficulty) {
    uint64_t nonce = 0;
    uint8_t  hash[32];
    uint32_t zero_bytes = difficulty / 8;

    for (;;) {
        memcpy(header + 24, &nonce, 8);
        hash_block(header, hash);

        int valid = 1;
        for (uint32_t i = 0; i < zero_bytes; i++)
            if (hash[i] != 0) { valid = 0; break; }
        if (valid) return nonce;
        nonce++;
        /* about 2^70 iterations on average */
    }
}
// proof of work is Big O weaponised
The gap between O(1) and O(2²⁵⁶) is Bitcoin's moat. Not cryptographic magic: asymmetric computational complexity. Compute is O(1) so everyone can do it; verify is O(1) so everyone can check it; finding a nonce is O(2⁷⁰) so only miners with real electricity can; reversing the hash is O(2²⁵⁶) so nobody can. The whole system is one carefully chosen complexity gap.

The sorting ceiling

Big O also defines what is possible. Any algorithm that sorts by comparing elements cannot do better than O(n log n). This is proven, not a limitation of current cleverness: there are n! possible orderings of n elements, each comparison eliminates at most half of them, so the minimum number of comparisons is log₂(n!) ≈ n log n.

O(n log n) is the sorting ceiling. Merge sort hits it; quicksort hits it on average; bubble sort and insertion sort do not. The next page proves all of this in code.

Big O Notation across ScrapyBytes

The same ideas surface all over ScrapyBytes. Here is where this page connects to the rest of the curriculum, and how to follow each thread.

Arrays

Array indexing is O(1), searching an unsorted array is O(n), and push is amortised O(1). The arrays page is full of the costs this page names.

scrapybytes.vercel.app/arrays
Linked List

A linked list trades the array's O(1) indexing for O(1) front inserts and O(n) search. The linked-list page is one row in this page's table.

scrapybytes.vercel.app/linked-list
Hashing

Hash map lookup is O(1) average and O(n) worst case when every key collides. The hashing page is where the hidden caveat in Big O bites hardest.

scrapybytes.vercel.app/hashing
Sorting Algorithms

Bubble sort is O(n squared), merge sort is O(n log n), and that gap is the whole point of this page. The sorting page is Big O made visible.

scrapybytes.vercel.app/sorting
Recursion

A recursive algorithm's cost is a recurrence you solve to get its Big O. The recursion page supplies the algorithms; this page measures them.

scrapybytes.vercel.app/recursion
Memory

Big O counts operations, but cache misses and allocations have costs too. The memory page is why two O(n) algorithms can run at very different speeds.

scrapybytes.vercel.app/memory
CPU

Big O ignores constant factors, but the CPU does not. The CPU page is the reason a cache-friendly O(n) can beat a cache-hostile O(n).

scrapybytes.vercel.app/cpu
Binary

Binary search is O(log n) precisely because binary is ordered: every comparison eliminates half the search space. The binary page is where that halving comes from.

scrapybytes.vercel.app/binary
Networking

Routing is a Big O problem: Dijkstra's shortest path is O((V+E) log V). The networking page is why your packets arrive in milliseconds instead of an O(n squared) crawl.

scrapybytes.vercel.app/networking
Distributed Systems

Eventually-consistent reads are O(1); strongly-consistent reads cost O(network round-trips). The distributed-systems page showed the tradeoff; this page names its cost.

scrapybytes.vercel.app/distributed-systems
Blockchain

Bitcoin's security is a Big O argument: forging the chain is exponential work, 2 to the N hash attempts. The blockchain page is Big O used as a weapon.

scrapybytes.vercel.app/blockchain
next up / 0x16
The ceiling proven in code: sorting algorithms.
sorting algorithms