01. How to Think About Complexity
📋 Jump to TakeawaysEvery algorithm has a cost. Big-O notation tells you how that cost grows as input gets bigger. It's the single most important concept in DSA — every data structure choice and every algorithm decision comes back to "what's the complexity?"
The Core Idea
Big-O measures growth rate, not exact speed. An O(n) algorithm isn't "fast" — it means the work scales linearly with input size. Double the input, double the work.
// O(n) — one loop through the data
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}Constants don't matter. O(2n) is O(n). O(n/2) is O(n). Big-O cares about the shape of the growth curve, not the exact multiplier.
The Five Common Classes
Almost every algorithm you'll encounter falls into one of these:
| Class | Name | Example |
|---|---|---|
| O(1) | Constant | Array access by index, hash map lookup |
| O(log n) | Logarithmic | Binary search, balanced tree operations |
| O(n) | Linear | Single loop, linear search |
| O(n log n) | Linearithmic | Merge sort, heap sort |
| O(n²) | Quadratic | Nested loops, bubble sort |
With 1 million elements:
- O(1) → 1 operation
- O(log n) → ~20 operations
- O(n) → 1,000,000 operations
- O(n log n) → ~20,000,000 operations
- O(n²) → 1,000,000,000,000 operations
The jump from O(n) to O(n²) is where algorithms go from "instant" to "won't finish before the heat death of the universe."
Beyond Quadratic
These classes appear in brute-force solutions and exhaustive search. They're usually too slow for large inputs but acceptable when n is small.
| Class | Name | Example |
|---|---|---|
| O(n³) | Cubic | Three nested loops, naive matrix multiplication |
| O(2ⁿ) | Exponential | Recursive with two branches per call, subsets |
| O(n!) | Factorial | Generating all permutations |
Time vs Space
Every algorithm has two costs:
Time complexity — how many operations it performs.
Space complexity — how much extra memory it needs.
// O(n) time, O(n) space — builds a new slice
func double(nums []int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = n * 2
}
return result
}
// O(n) time, O(1) space — modifies in place
func doubleInPlace(nums []int) {
for i := range nums {
nums[i] *= 2
}
}Often you trade one for the other. Use extra memory to save time (caching, hash maps). Use extra time to save memory (recompute instead of store).
How to Analyze Code
Count the loops:
- No loops → O(1)
- One loop over n → O(n)
- Nested loops over n → O(n²)
- Loop that halves the problem each step → O(log n)
- One loop with a sort inside → O(n log n)
// O(n²) — nested loops, both depend on n
func allPairs(nums []int) {
for i := 0; i < len(nums); i++ {
for j := i + 1; j < len(nums); j++ {
fmt.Println(nums[i], nums[j])
}
}
}
// O(n log n) — sort dominates
func sortedUnique(nums []int) []int {
sort.Ints(nums) // O(n log n)
result := []int{nums[0]}
for i := 1; i < len(nums); i++ { // O(n)
if nums[i] != nums[i-1] {
result = append(result, nums[i])
}
}
return result // Total: O(n log n) + O(n) = O(n log n)
}When you have sequential steps, take the largest. When you have nested steps, multiply.
Amortized Complexity
Some operations are usually cheap but occasionally expensive. Go's append is O(1) amortized — most calls just write to the next slot, but occasionally the slice doubles its capacity and copies everything.
s := make([]int, 0, 4)
s = append(s, 1) // O(1) — fits in capacity
s = append(s, 2) // O(1)
s = append(s, 3) // O(1)
s = append(s, 4) // O(1)
s = append(s, 5) // O(n) — capacity exceeded, allocate and copyOver n appends, the total cost is O(n), so each append is O(1) amortized. You'll see this pattern in dynamic arrays, hash maps, and union-find.
Best, Worst, Average
Big-O usually describes the worst case. But sometimes the distinction matters:
- Binary search: O(log n) worst, O(1) best (target is the middle element)
- Quick sort: O(n²) worst, O(n log n) average
- Hash map lookup: O(n) worst (all keys collide), O(1) average
In interviews, state the worst case unless asked otherwise. In practice, average case often matters more — quick sort is faster than merge sort on real data despite the worse worst case.
The Practical Rule
For competitive programming and interviews, these are the rough limits for n:
| n | Max acceptable complexity |
|---|---|
| ≤ 20 | O(2ⁿ), O(n!) |
| ≤ 500 | O(n³) |
| ≤ 10,000 | O(n²) |
| ≤ 1,000,000 | O(n log n) |
| ≤ 100,000,000 | O(n) |
| Any | O(log n), O(1) |
If n is 10⁶ and your solution is O(n²), it won't pass. This table tells you which algorithmic class you need before you start coding.
Practice Problems
These problems have brute-force solutions that are too slow. The challenge is recognizing which complexity class you need and picking the right approach.
| Problem | Difficulty | Why it fits |
|---|---|---|
| Contains Duplicate | Easy | O(n²) brute force vs O(n) with a hash set |
| Two Sum | Easy | O(n²) nested loops vs O(n) with a hash map |
| 3Sum | Medium | O(n³) brute force vs O(n²) with sorting + two pointers |
| Search in Rotated Sorted Array | Medium | Must be O(log n) — linear scan won't pass |
| Kth Largest Element in an Array | Medium | O(n log n) sort vs O(n) quickselect — input size determines which matters |
Key Takeaways
- Big-O measures growth rate, not absolute speed — constants are dropped
- Five classes cover almost everything: O(1), O(log n), O(n), O(n log n), O(n²)
- Time and space are separate costs — you often trade one for the other
- Count loops to analyze: one loop = O(n), nested = O(n²), halving = O(log n)
- Amortized O(1) means "usually O(1), occasionally O(n), averages out"
- Use the input size to determine which complexity class your solution needs