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.
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
How they grow
| n | O(1) | O(log n) | O(n) | O(n log n) | O(n²) | O(2ⁿ) |
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 1 | 1 | 2 |
| 10 | 1 | 3 | 10 | 33 | 100 | 1,024 |
| 100 | 1 | 7 | 100 | 664 | 10,000 | 1.3 × 10³⁰ |
| 1,000 | 1 | 10 | 1,000 | 9,966 | 1,000,000 | 10³⁰¹ |
| 10,000 | 1 | 13 | 10,000 | 132,877 | 100,000,000 | 10³⁰¹⁰ |
The four rules
Four complexities, one screen
// 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
}#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.
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
| operation | complexity | why |
|---|---|---|
| access by index | O(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 end | O(1) amortised | append is one write (plus rare regrowth) |
| insert at beginning | O(n) | must shift every element right |
| delete at index | O(n) | must shift elements to fill the gap |
Linked lists
| operation | complexity | why |
|---|---|---|
| access by index | O(n) | follow the pointer chain from the head |
| search | O(n) | traverse from the head |
| insert at head | O(1) | just update the head pointer |
| insert at tail | O(1) / O(n) | O(1) with a tail pointer, O(n) without |
| insert at middle | O(n) | O(n) to find, O(1) to splice |
| delete at head | O(1) | update the head pointer |
Hash maps
| operation | complexity | why |
|---|---|---|
| lookup | O(1) average | hash the key, jump to the slot |
| insert | O(1) average | hash the key, write the slot |
| delete | O(1) average | hash the key, clear the slot |
| worst case (all collide) | O(n) | degenerates to a chain walk; good hash functions prevent it |
Recursion
| algorithm | time | space |
|---|---|---|
| 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 sort | O(n log n) | O(n) |
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.
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
}#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 */
}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.
// 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
}#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:
| approach | time | space |
|---|---|---|
| nested loops | O(n²) | O(1) |
| sort then scan | O(n log n) | O(n) |
| hash set | O(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.
| operation | complexity | who can do it |
|---|---|---|
| compute a SHA-256 hash | O(1) | everyone, in nanoseconds |
| verify a hash | O(1) | everyone, in nanoseconds |
| find a valid nonce | O(2⁷⁰) | only miners, with electricity |
| reverse SHA-256 | O(2²⁵⁶) | nobody, ever |
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
}
}#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 */
}
}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 across BitRoot
This page is the backbone the whole curriculum hangs from. Every prior topic has a Big O story: