Big-O Notation
Big-O describes an upper bound on growth. Saying an algorithm is claims that beyond some input size, its cost is at most a constant multiple of — never that it is , and never anything at all about small inputs.
That "beyond some input size" clause is the part most explanations skip, and it is the part that makes the notation work. It is what licenses throwing away constants and lower-order terms, because for large enough n those genuinely stop mattering.
Core Concepts
| Term | Meaning | Everyday reading |
|---|---|---|
| Grows no faster than f | Upper bound — "at worst this" | |
| Grows no slower than f | Lower bound — "at best this" | |
| Bounded above and below by f | Tight bound — "exactly this rate" | |
| Grows strictly slower than f | Strict upper bound — never touches f itself | |
| Grows strictly faster than f | Strict lower bound — never touches f itself |
and allow equality with in the limit; and forbid it — is true, is false. is the conjunction : a function is exactly when it is both and for the same g.
Informal usage almost always says "O" where "Θ" is meant. Saying mergesort is is true but weak — it is also , since that is a valid upper bound too. Saying mergesort is is the stronger, more useful claim. In practice, when someone says "quicksort is on average", read Θ.
Mechanism
The formal definition, and what it is doing
means: there exist positive constants c and such that for all ,
f(n) ≤ c · g(n)

The two knobs are what make the abstraction useful. Because you may pick any constant c, a
factor of 100 in speed cannot change the classification. Because you may pick any starting point
, behaviour on small inputs cannot change it either.
Why constants and lower-order terms vanish
Given :
| n | 3 | 500n | 90000 | Which dominates |
|---|---|---|---|---|
| 10 | 300 | 5,000 | 90,000 | the constant |
| 100 | 30,000 | 50,000 | 90,000 | still the constant |
| 1,000 | 3,000,000 | 500,000 | 90,000 | |
| 100,000 | 90,000 | , overwhelmingly |
So . The other terms are not wrong, they simply stop being the story. Note also that at n = 100 this function is dominated by a constant — a real reminder that asymptotic claims say nothing about the range you might actually be operating in.
There are two situations where dropping the constant genuinely misleads:
- Small n. The table above shows it directly — up to n ≈ 500 the constant term
90000outweighs the quadratic term, so an "" label predicts nothing useful about behaviour in that range. - A huge constant hidden inside a low-order term. An step that is implemented as a lookup into a 10 MB table pays for a cache miss on essentially every call — the asymptotic class says "constant", but the constant is a full round trip to main memory, not a register read. Two algorithms with wildly different real constants are not interchangeable just because the notation puts them in the same class.
Verifying a bound directly
Proving 3n² + 5n + 2 = O(n²) means exhibiting a c and an n₀ that make the formal definition hold:
claim: 3n² + 5n + 2 ≤ c · n² for all n ≥ n₀
try n₀ = 1, and bound each term by n² for n ≥ 1:
5n ≤ 5n² (since n ≤ n² when n ≥ 1)
2 ≤ 2n² (since 1 ≤ n² when n ≥ 1)
so 3n² + 5n + 2 ≤ 3n² + 5n² + 2n² = 10n² for all n ≥ 1
check n = 1: 3 + 5 + 2 = 10 ≤ 10 · 1 = 10 ✓ (equality, the tightest case)
check n = 5: 75 + 25 + 2 = 102 ≤ 10 · 25 = 250 ✓
c = 10, n₀ = 1 satisfy the definition, so 3n² + 5n + 2 = O(n²).
The choice of c = 10, n₀ = 1 is not unique — a smaller c also works provided n₀ moves out to
compensate: c = 4, n₀ = 6 clears every n from 6 onward (3·36 + 5·6 + 2 = 140 ≤ 4·36 = 144), even
though it fails at n = 3. Any pair that clears every n from n₀ onward is a valid proof; the
definition asks for existence, not for the tightest possible constants.
Reading complexity off code
The mechanical rules cover most cases:
- Python
- C++
# O(1) — the work does not depend on n
def first(items):
return items[0]
# O(n) — one pass
def total(items):
acc = 0
for x in items: # n iterations
acc += x # O(1) each
return acc
# O(n²) — nested loops over the same input
def has_duplicate(items):
for i in range(len(items)): # n
for j in range(i + 1, len(items)): # up to n
if items[i] == items[j]:
return True
return False
# O(log n) — the search space halves each step
def count_halvings(n):
steps = 0
while n > 1:
n //= 2
steps += 1
return steps
assert first([5, 1, 8, 3]) == 5
assert total([5, 1, 8, 3]) == 17
assert has_duplicate([5, 1, 8, 3]) is False
assert has_duplicate([5, 1, 8, 5]) is True
assert count_halvings(1_000_000) == 19 # floor(log2(1_000_000)) divisions to reach 1
#include <cstddef>
#include <vector>
// O(1) — the work does not depend on n
int first(const std::vector<int>& items) {
return items[0];
}
// O(n) — one pass
long long total(const std::vector<int>& items) {
long long acc = 0;
for (int x : items) // n iterations
acc += x; // O(1) each
return acc;
}
// O(n²) — nested loops over the same input
bool has_duplicate(const std::vector<int>& items) {
for (std::size_t i = 0; i < items.size(); ++i) // n
for (std::size_t j = i + 1; j < items.size(); ++j) // up to n
if (items[i] == items[j]) return true;
return false;
}
// O(log n) — the search space halves each step
int count_halvings(int n) {
int steps = 0;
while (n > 1) {
n /= 2;
++steps;
}
return steps;
}
- Sequential blocks add, and the larger wins: .
- Nested loops multiply: a loop of n containing a loop of m is .
- Halving (or doubling) the problem each step is logarithmic — that is what a logarithm counts.
Edge Cases & Pitfalls
is meaningless until you say what n counts. Two common traps:
- Two different inputs. Comparing every element of one list against another is , not — and if m is tiny and fixed, it is effectively .
- Cost per operation. Summing a list of integers is . Concatenating a list of strings
with
+=in a loop is , because each concatenation copies everything accumulated so far. The loop looks identical; the per-iteration cost is not .
- is not always worse than . With a large constant hidden inside, the "better" algorithm can lose on real input sizes. This is exactly why production sorts switch to insertion sort below a threshold of ~16 elements.
- Best/average/worst are separate questions from O/Ω/Θ, though the two are constantly conflated. You can state a Θ bound on the worst case, or an O bound on the average case; the notation and the case being analysed are independent choices.
- Amortized is not average. See Amortized Analysis — an amortized bound is a guarantee over any sequence of operations, not a probabilistic statement.
Comparisons
| Claim | Says | Does not say |
|---|---|---|
| "Quicksort is " | Its worst case is quadratic | Anything about the typical case, which is n log n |
| "Lookup is " | Cost does not grow with the collection | That it is fast — a hash may be expensive |
| "This is faster, it's not " | It scales better | That it wins at your n |
Recall
References
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, Ch. 3 — "Characterizing Running Times", the formal treatment of O, Ω and Θ.
- Knuth, The Art of Computer Programming, Vol. 1, §1.2.11 — the origin of the notation's use in this field.
Books & Videos
- Big-O Cheat Sheet — complexity tables for the common structures and sorts, useful as a lookup rather than a lesson.
Related Pages
- Common Complexities — a named algorithm for each growth class, and what a given n costs at realistic hardware speeds.
- Sorting Algorithms — where these bounds get their most familiar workout.