Big O Complexity
How to read it, how to derive it from code, and how Java's built-in collections perform across every operation you care about.
What Big O Measures
Big O notation describes the growth rate of an algorithm’s resource usage β time or space β as a function of its input size n. It does not measure wall-clock time in milliseconds, because that depends on hardware, language, and compiler. It measures the rate of growth: if you double the input, does the algorithm take twice as long, four times as long, or the same amount of time? That growth rate is what determines whether an algorithm scales or breaks under real-world load.
Big O describes the upper bound β the worst the algorithm can do on any input of size n. This is deliberately pessimistic: an algorithm that runs in O(n) time might finish much faster on a sorted input, but the guarantee is that it will never be worse than linear. When comparing algorithms, upper bounds are more useful than averages because they tell you the worst case you will ever experience in production.
- O(3n + 100) simplifies to O(n). O(nΒ² + n) simplifies to O(nΒ²).
- Big O cares only about the dominant term as n grows large β constants and smaller terms become irrelevant at scale.
Reading Big O Notation
- O(1) means the time does not grow with input size β it could still be slow in absolute terms.
- A HashMap.get() is O(1) but performs a hash computation, an array index, and possibly an equals() check β it is always the same cost regardless of map size.
- A sorted array of one million elements needs at most 20 comparisons with binary search.
- That is why O(log n) is called “essentially constant” at practical scales β TreeMap.get() on a map with a billion keys still finishes in ~30 comparisons.
- If the data is unsorted and you need to find an element, you must look at every element in the worst case β O(n) is optimal.
- Any claim of faster-than-linear search on unsorted data requires either preprocessing (sorting, hashing) or a structural guarantee.
- Any algorithm that sorts by comparing elements cannot do better than O(n log n) β this is a mathematical proof, not a limitation of existing implementations.
- The only way to sort faster is to use non-comparison methods like counting sort or radix sort, which require constraints on the input values.
- At n=10,000 a quadratic algorithm performs 100 million operations.
- At n=100,000 it performs 10 billion β seconds become minutes.
- Whenever you see a nested loop over the same collection, ask whether a HashMap can reduce the inner loop to O(1).
- Storing previously computed results eliminates redundant recursive calls. fib(n) with a cache (dynamic programming) runs in O(n) time and O(n) space β the same answer, exponentially faster.
- Recognising exponential growth is the first step to knowing when dynamic programming applies.
| Notation | Name | n=10 | n=100 | n=1,000 | n=1,000,000 |
|---|---|---|---|---|---|
| O(1) | Constant | 1 | 1 | 1 | 1 |
| O(log n) | Logarithmic | 3 | 7 | 10 | 20 |
| O(n) | Linear | 10 | 100 | 1,000 | 1,000,000 |
| O(n log n) | Linearithmic | 33 | 664 | 9,966 | 19,931,568 |
| O(nΒ²) | Quadratic | 100 | 10,000 | 1,000,000 | 10ΒΉΒ² (unusable) |
| O(2βΏ) | Exponential | 1,024 | 10Β³β° (β) | β | β |
Deriving Complexity From Code
Reading Big O from existing analysis is one skill; deriving it from code you are looking at is another. The rules are mechanical β you do not need to count every operation, only identify the dominant pattern. Four patterns cover 95% of what you will encounter.
- If the loop body itself is O(n) β for example, calling list.contains() inside a for loop β the total becomes O(n) Γ O(n) = O(nΒ²).
- Always analyse the body before multiplying.
- If the inner loop is searching for a match, a HashMap built in O(n) reduces the inner search to O(1), converting the entire algorithm from O(nΒ²) to O(n).
- This is the most common optimisation pattern in array interview problems.
- Any time you see a loop where the search space is cut in half β whether by lo/hi pointers, a balanced tree, or a divide-and-conquer recursion β the loop runs at most logβ(n) times.
- Identify the shrinking variable to confirm.
- For any recursive function, draw a tree where each node is one call and each branch is a recursive invocation.
- The total number of nodes is the total number of calls.
- Multiply by the work per call (excluding recursion) to get the total complexity.
Best, Average, and Worst Case
Big O typically describes worst-case β the maximum number of steps on any input of size n. But algorithms can behave very differently depending on the input, so three cases matter: best, average, and worst. Understanding all three prevents false confidence from benchmarks on lucky inputs.
The ideal input
The input that makes the algorithm finish as fast as possible. Useful as a lower bound but rarely the number you plan for.
- Linear search finds target at index 0 β Ξ©(1)
- QuickSort with perfect median pivot β Ξ©(n log n)
The hardest input
The input that forces the maximum number of steps. This is what Big O describes. Plan for this β production systems encounter worst-case inputs regularly.
- Linear search: target not in array β O(n)
- QuickSort: always picking smallest pivot β O(nΒ²)
The expected input
The expected behaviour over all possible inputs of size n, weighted by probability. Harder to derive but most representative of real-world performance.
- HashMap lookup: O(1) average, O(n) worst (all keys collide)
- QuickSort: O(n log n) average with random pivot selection
Space Complexity
Time complexity measures how runtime grows with input size β space complexity measures how memory usage grows. The two are often in tension: using extra memory (a HashMap, a cache, a second array) frequently reduces time complexity, and the trade-off is worth making explicit. When someone says an algorithm runs in O(1) space, they mean it uses a constant amount of extra memory regardless of input size β not that it uses zero memory.
| Algorithm | Time | Space | Notes |
|---|---|---|---|
| Array reversal (in-place) | O(n) | O(1) | Swap with two pointers |
| Array reversal (new array) | O(n) | O(n) | Allocates n-element copy |
| Recursive fibonacci | O(2βΏ) | O(n) | n stack frames at deepest path |
| Memoized fibonacci | O(n) | O(n) | Cache stores n results |
| Iterative fibonacci | O(n) | O(1) | Two variables, no cache |
| BFS on a graph | O(V+E) | O(V) | Queue holds up to V nodes |
| Merge sort | O(n log n) | O(n) | Merge step needs temp array |
| QuickSort (in-place) | O(n log n) | O(log n) | Stack frames = tree height |
- When you add a HashMap to reduce O(nΒ²) to O(n), you are trading O(n) space for O(n) time savings.
- Name the trade-off when you make it β in code comments, in interviews, and in design discussions.
Amortized Analysis
Amortized analysis describes the average cost of an operation over a sequence of operations β not the worst single case, but the cost spread across many calls. It matters because some operations are occasionally expensive but cheaply distributed over time. ArrayList’s add() is the canonical example: most appends are O(1), but one in every n appends triggers a resize that costs O(n) β yet the average cost per append over n operations is still O(1).
Start with capacity 1. Each time the array fills, double it and copy all elements. Track the total copy operations across 16 appends.
| Append # | Capacity before | Resize? | Copy cost | Running total |
|---|---|---|---|---|
| 1 | 1 | No | 0 | 0 |
| 2 | 1 | Yes β 2 | 1 | 1 |
| 3 | 2 | Yes β 4 | 2 | 3 |
| 4 | 4 | No | 0 | 3 |
| 5 | 4 | Yes β 8 | 4 | 7 |
| 6β8 | 8 | No | 0 | 7 |
| 9 | 8 | Yes β 16 | 8 | 15 |
| 10β16 | 16 | No | 0 | 15 |
| Average copy cost per append: 15/16 β 1 β O(1) amortized | ||||
- Amortized O(1) means the total cost of n operations is O(n), so each individual operation costs O(1) on average β but some individual calls may be O(n).
- Average case is about input distribution; amortized is about spreading one expensive operation across many cheap ones.
Java Collections Cheatsheet
Every Java collection has a defined complexity for its core operations β knowing these cold is the difference between a confident interview answer and an uncertain guess. This is the reference to bookmark: the complete complexity profile of every collection you will reach for.
| Operation | ArrayList | LinkedList | Notes |
|---|---|---|---|
get(index) | O(1) | O(n) | LinkedList must traverse from head/tail |
add() at end | O(1)* | O(1) | *ArrayList amortized; LinkedList has tail ref |
add(i, val) | O(n) | O(n) | ArrayList: shift; LinkedList: traverse to i |
remove(index) | O(n) | O(n) | ArrayList: shift; LinkedList: traverse to i |
contains() | O(n) | O(n) | Linear scan in both |
size() | O(1) | O(1) | Both store as field |
iterator.next() | O(1) | O(1) | ArrayList: index++; LinkedList: pointer follow |
* LinkedList.get(i) is O(n) β never call it in a loop
| Operation | ArrayDeque | LinkedList | PriorityQueue | Notes |
|---|---|---|---|---|
addFirst / push | O(1) | O(1) | β | ArrayDeque: array index |
addLast / offer | O(1) | O(1) | O(log n) | PQ: heap sift-up |
removeFirst / pop | O(1) | O(1) | O(log n) | PQ: heap sift-down |
peekFirst | O(1) | O(1) | O(1) | PQ: root is always min/max |
size | O(1) | O(1) | O(1) | All store as field |
| Operation | HashMap | LinkedHashMap | TreeMap | Notes |
|---|---|---|---|---|
get(key) | O(1)* | O(1)* | O(log n) | *avg β O(n) worst (all collide) |
put(key, val) | O(1)* | O(1)* | O(log n) | TreeMap maintains sorted order |
remove(key) | O(1)* | O(1)* | O(log n) | β |
containsKey | O(1)* | O(1)* | O(log n) | β |
| iteration | O(n) | O(n) | O(n) | LinkedHashMap: insertion order |
firstKey / lastKey | β | β | O(log n) | TreeMap only β BST min/max |
* HashMap O(1) average assumes a good hash function
| Operation | HashSet | LinkedHashSet | TreeSet | Notes |
|---|---|---|---|---|
add(value) | O(1)* | O(1)* | O(log n) | β |
contains() | O(1)* | O(1)* | O(log n) | β |
remove() | O(1)* | O(1)* | O(log n) | β |
first() / last() | β | β | O(log n) | TreeSet only |
| iteration | O(n) | O(n) | O(n) | TreeSet: sorted order |
- HashMap gives O(1) get/put for the most common need β checking membership or counting frequency.
- ArrayDeque gives O(1) push/pop/enqueue/dequeue for the second most common need β processing items in order.