📖 Analogy
Finding a name in a phone book two ways: flipping one page at a time from the front, or opening to the middle and deciding “earlier or later” each time. With a million names, the first way can take a million flips; the second takes about twenty. Big-O is the language for that gap — it tells you how the work grows as the book gets bigger, not how long one flip takes.
Why growth, not a stopwatch
A stopwatch measures one machine, one input, one day. Big-O measures how the work scales as the input size n grows — which is what actually decides whether your code survives 10× the data. Two rules make it portable:
- Drop constants:
O(2n)andO(n)are the same class; hardware changes the constant, not the shape. - Keep the dominant term:
O(n² + n)isO(n²)— for large n, the biggest term wins.
So we care about the shape of the curve, not its exact height.
The growth classes you’ll meet
graph LR A["O(1)<br/>constant"] --> B["O(log n)<br/>logarithmic"] --> C["O(n)<br/>linear"] --> D["O(n log n)<br/>linearithmic"] --> E["O(n²)<br/>quadratic"] --> F["O(2ⁿ)<br/>exponential"] classDef good fill:#0f2a1a,stroke:#22c55e,color:#d7ffe9; classDef bad fill:#2a0f17,stroke:#f43f5e,color:#ffd7e0; class A,B good; class E,F bad;
| Class | Name | Example in Go | n=1,000 |
|---|---|---|---|
| O(1) | constant | m[key], s[i], len(s) | 1 |
| O(log n) | logarithmic | binary search, balanced-tree lookup | ~10 |
| O(n) | linear | one loop over a slice | 1,000 |
| O(n log n) | linearithmic | sort.Slice, merge sort | ~10,000 |
| O(n²) | quadratic | nested loop over the same slice | 1,000,000 |
| O(2ⁿ) | exponential | naive recursive subsets/Fibonacci | astronomically large |
Reading it off real code
Count how many times the work in the innermost statement runs as a function of n:
// O(n) — the loop body runs once per element
func sum(a []int) int {
total := 0
for _, v := range a { // n iterations
total += v
}
return total
}
// O(n²) — a loop inside a loop over the same input
func hasDuplicate(a []int) bool {
for i := range a { // n
for j := i + 1; j < len(a); j++ { // n
if a[i] == a[j] {
return true
}
}
}
return false
}
// O(log n) — the range halves every step
func countHalvings(n int) int {
steps := 0
for n > 1 {
n /= 2 // discard half
steps++
}
return steps
}
The same hasDuplicate becomes O(n) with a hash set — trading O(n) space to drop a whole factor of n off the time. That trade is the heart of most optimization.
See the gap
The same task — find 999 in a sorted slice of 1,000 — scanned linearly vs. halved. Run it:
package main
import "fmt"
// O(n): check every element until found.
func linearSearch(a []int, target int) (idx, steps int) {
for i, v := range a {
steps++
if v == target {
return i, steps
}
}
return -1, steps
}
// O(log n): halve the range each step (input must be sorted).
func binarySearch(a []int, target int) (idx, steps int) {
lo, hi := 0, len(a)-1
for lo <= hi {
steps++
mid := lo + (hi-lo)/2
switch {
case a[mid] == target:
return mid, steps
case a[mid] < target:
lo = mid + 1
default:
hi = mid - 1
}
}
return -1, steps
}
func main() {
a := make([]int, 1000)
for i := range a {
a[i] = i // sorted 0..999
}
_, ls := linearSearch(a, 999)
_, bs := binarySearch(a, 999)
fmt.Printf("n=%d → linear: %d steps, binary: %d steps\n", len(a), ls, bs)
}
Space complexity counts too
The same growth classes describe extra memory. Reversing a slice in place is O(1) space; building a new reversed copy is O(n). Recursion adds O(depth) for the call stack — a recursion n deep costs O(n) space even if it returns one number.
⚠️ Worst, average, and amortized aren't the same
A hash-map lookup is average O(1) but worst-case O(n) when everything collides. A slice append is amortized O(1) — most are instant, but the one that reallocates copies all n elements. State which you mean. And remember Big-O hides constants: an O(n) pass with a huge constant can lose to an O(n log n) one on realistic sizes.
Complexity of common Go operations
A quick reference for the costs you’ll reason about constantly:
| Operation | Complexity |
|---|---|
s[i], len(s), m[k] read | O(1) |
append(s, x) | O(1) amortized |
| insert/delete in the middle of a slice | O(n) (shift) |
m[k] = v, delete(m, k) | O(1) average |
sort.Slice(s, …) | O(n log n) |
binary search (sort.Search) | O(log n) |
linear scan / range | O(n) |
| nested loop over the same slice | O(n²) |
| balanced-tree / heap insert | O(log n) |
| naive recursive Fibonacci | O(2ⁿ) |
See also
- arrays & slices — where amortized-O(1) append comes from.
- binary search — the canonical O(log n).
- hash tables — O(1) average, and the trade that buys it.
- sorting — the O(n log n) vs O(n²) landscape.
- the cheat sheet — every structure and algorithm’s complexity in one table.
Next: the structure those costs live on — arrays and slices.
Related topics
A Go slice is a view over a backing array — how the (ptr, len, cap) header, append, growth, copy and re-slicing actually behave, and the aliasing traps.
algorithmsBinary SearchFind a value in a sorted slice in O(log n) by repeatedly halving the range — the correct loop, the off-by-one traps, and the stdlib helpers.
linearHash TablesAverage O(1) lookup via hashing — how Go's built-in map works, collisions and load factor, the comma-ok idiom, and the ordering and nil-map gotchas.
Check your understanding
Score: 0 / 51. What does Big-O notation describe?
Big-O describes the growth rate of time (or space) as input size n → ∞. Constant factors and lower-order terms are dropped, so O(2n + 5) is just O(n).
2. What is the time complexity of binary search on a sorted slice of n elements?
Each comparison discards half the remaining range, so it takes about log₂(n) steps — 10 steps for 1000 elements, 20 for a million.
3. Why is appending to a Go slice called amortized O(1)?
When cap is exceeded, Go allocates a bigger backing array (roughly doubling) and copies. Doubling means those O(n) copies happen rarely enough that the average over n appends is O(1).
4. Simplify O(n² + 3n + 7) to a single Big-O class.
As n grows, n² dwarfs 3n and 7, so the lower-order terms vanish and constant factors drop. The class is O(n²) — Big-O captures the shape of the dominant term only.
5. A recursive function calls itself to a depth of n before returning. What's its space complexity, even if it returns a single int?
Space complexity counts the call stack too. n nested calls hold n frames simultaneously before any unwinds, so the recursion costs O(n) space regardless of how small the returned value is. Converting to iteration (or tail-style accumulation) can drop it to O(1).
Comments
Sign in with GitHub to join the discussion.