Why should you care?
You can write a program that works perfectly and still have a serious problem: it might be too slow when the input becomes large.
Consider two algorithms that search for an element.
Algorithm A takes:
10 operations for 10 elements
100 operations for 100 elements
1,000 operations for 1,000 elements
Algorithm B takes:
3 operations for 10 elements
7 operations for 100 elements
10 operations for 1,000 elements
Both might work perfectly for small inputs.
But as the input grows, their performance becomes dramatically different.
This is where Big-O notation becomes important.
Big-O helps us understand how the running time or memory requirements of an algorithm grow as the input size increases.
It is one of the most important concepts in computer science because it allows us to answer:
"How will this algorithm behave when my input becomes really large?"
The Problem
Suppose you have an array:
[10, 20, 30, 40, 50]
and want to find:
40
You could check every element:
10 → 20 → 30 → 40
For five elements, this is trivial.
But imagine:
10,000 elements
or:
1,000,000 elements
or:
1,000,000,000 elements
Now the algorithm's growth rate matters.
Another algorithm might be able to find the same element much faster.
For example, binary search can repeatedly divide the search space in half.
So we need a mathematical way to describe algorithmic growth.
That's what Big-O notation provides.
The Concept
Big-O notation describes the asymptotic growth of an algorithm.
In simple terms:
Big-O tells us how the amount of work grows as the input size grows.
We usually represent input size using:
n
For example:
n = number of elements in an array
If an algorithm examines every element:
n operations
we write:
O(n)
If it repeatedly divides the input in half:
O(log n)
If it compares every element with every other element:
O(n²)
Simple Explanation
Imagine you are looking for a person in a line.
O(1) — Constant
The person is always standing at a known position.
You immediately access them.
1 operation
It doesn't matter whether there are:
10 people
or:
1,000,000 people
The work remains approximately constant.
Therefore:
O(1)
O(n) — Linear
You check people one by one.
Person 1
Person 2
Person 3
...
Person n
If the number of people doubles, the maximum amount of work approximately doubles.
Therefore:
O(n)
O(log n) — Logarithmic
Imagine the people are sorted and you repeatedly eliminate half of them.
1,000,000
↓
500,000
↓
250,000
↓
125,000
↓
...
You don't need to inspect everyone.
The search space shrinks exponentially.
Therefore:
O(log n)
Binary search is the classic example.
O(n²) — Quadratic
Suppose every person needs to compare themselves with every other person.
For:
n people
you can end up with approximately:
n × n
comparisons.
Therefore:
O(n²)
Nested loops are a common source of quadratic complexity.
Real-world Analogy
Imagine finding a book in a library.
O(n)
You start at the first book and check every book one by one.
Book 1
Book 2
Book 3
...
Book n
O(log n)
The books are sorted alphabetically.
You open the middle of the library.
If your book should come before that point, ignore the second half.
Then repeat.
1,000,000 books
↓
500,000
↓
250,000
↓
125,000
↓
...
This is logarithmic behavior.
O(1)
The library system tells you:
Computer Science → Shelf 42 → Position 17
You directly access the location.
That's constant-time access.
Code Example
Let's look at some common examples.
O(1)
int getFirst(int[] arr) {
return arr[0];
}
Regardless of the array size:
10 elements
100 elements
1,000,000 elements
we access one position.
Therefore:
O(1)
O(n)
void printAll(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
If there are n elements, the loop executes n times.
Therefore:
O(n)
O(n²)
void printPairs(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length; j++) {
System.out.println(arr[i] + ", " + arr[j]);
}
}
}
The outer loop executes:
n times
The inner loop also executes:
n times
Total:
n × n = n²
Therefore:
O(n²)
O(log n)
Binary search is a classic example:
int binarySearch(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
}
if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
Every iteration eliminates approximately half of the remaining elements.
Therefore:
O(log n)
Common Big-O Complexities
From generally more scalable to less scalable:
O(1)
↓
O(log n)
↓
O(n)
↓
O(n log n)
↓
O(n²)
↓
O(n³)
↓
O(2ⁿ)
↓
O(n!)
A rough intuition:
| Complexity | Common Example |
|---|---|
| O(1) | Array access |
| O(log n) | Binary search |
| O(n) | Linear search |
| O(n log n) | Merge sort |
| O(n²) | Nested loops |
| O(n³) | Three nested loops |
| O(2ⁿ) | Some recursive subset algorithms |
| O(n!) | Brute-force permutations |
The exact performance depends on the algorithm and implementation, but this hierarchy is extremely useful.
Dropping Constants
Consider:
void example(int[] arr) {
for (int x : arr) {
System.out.println(x);
}
for (int x : arr) {
System.out.println(x);
}
}
The first loop takes:
n
operations.
The second takes:
n
operations.
Total:
2n
Technically:
O(2n)
But Big-O focuses on the growth rate, so we simplify:
O(n)
We drop constant factors.
Similarly:
O(5n) → O(n)
O(100n) → O(n)
Dropping Lower-Order Terms
Consider:
O(n² + n)
As n becomes very large, n² grows much faster than n.
Therefore:
O(n² + n)
becomes:
O(n²)
Similarly:
O(n³ + n² + n)
becomes:
O(n³)
The dominant term determines the asymptotic growth.
Time Complexity vs Space Complexity
Big-O isn't only about execution time.
We can also analyze memory usage.
Time Complexity
How does the amount of computation grow?
Example:
for (int i = 0; i < n; i++) {
System.out.println(i);
}
Time:
O(n)
Space Complexity
How much additional memory does the algorithm need?
Example:
int[] copy = new int[n];
The new array grows with n.
Therefore:
O(n)
space.
An algorithm can therefore have:
Time: O(n)
Space: O(1)
or:
Time: O(n)
Space: O(n)
These are separate measurements.
Common Mistakes
Mistake 1: Thinking Big-O gives the exact execution time
If an algorithm is:
O(n)
that does not mean it takes exactly n milliseconds.
Big-O describes growth, not a stopwatch measurement.
Two O(n) algorithms can have very different real-world performance.
Mistake 2: Assuming O(1) means instantaneous
O(1) means the operation does not grow with input size.
It doesn't mean:
0 seconds
or:
always extremely fast
A constant-time operation could still perform a relatively expensive fixed amount of work.
Mistake 3: Counting every line equally
Consider:
for (int i = 0; i < n; i++) {
System.out.println(i);
}
The important question isn't:
"How many lines are in the code?"
Instead ask:
"How many times does each operation execute as n grows?"
The loop executes n times.
Therefore:
O(n)
Mistake 4: Assuming nested loops always mean O(n²)
Consider:
for (int i = 0; i < n; i++) {
for (int j = 0; j < 10; j++) {
System.out.println(i + j);
}
}
The inner loop always runs only 10 times.
Total work:
10n
Therefore:
O(n)
not:
O(n²)
Nested loops matter based on how their iteration counts depend on n.
Mistake 5: Ignoring input characteristics
Consider searching an array.
Linear search can be:
Best case: O(1)
Worst case: O(n)
If the target is the first element, we're done immediately.
If it is the last element, we may inspect everything.
Big-O is often used for worst-case analysis, but best-case and average-case complexity can also be important.
Advanced Notes
1. Big-O vs Big-Theta vs Big-Omega
These notations are related but technically different.
Big-O:
O(f(n))
represents an asymptotic upper bound.
Big-Omega:
Ω(f(n))
represents an asymptotic lower bound.
Big-Theta:
Θ(f(n))
represents a tight asymptotic bound.
For example, if an algorithm consistently grows linearly:
Θ(n)
is a more precise statement than simply:
O(n)
because O(n) technically allows functions that grow more slowly as well.
In everyday algorithm discussions, however, "Big-O" is commonly used to describe asymptotic complexity generally.
2. Amortized Complexity
Some operations are occasionally expensive but cheap on average over a sequence of operations.
A dynamic array is a good example.
Most insertions may take:
O(1)
But occasionally the array becomes full and needs to resize.
That particular operation can take:
O(n)
Yet insertion has an amortized O(1) complexity.
This is why analyzing a sequence of operations can sometimes be more useful than analyzing one operation in isolation.
3. Recursive Algorithms
Big-O becomes especially important with recursion.
Consider:
void countdown(int n) {
if (n == 0) {
return;
}
System.out.println(n);
countdown(n - 1);
}
There are n recursive calls.
Therefore:
Time: O(n)
Space: O(n)
The space complexity comes from the recursive call stack.
4. Exponential Complexity
Consider an algorithm that branches into two recursive calls:
void solve(int n) {
if (n <= 0) {
return;
}
solve(n - 1);
solve(n - 1);
}
The number of calls grows approximately exponentially.
Its complexity is roughly:
O(2ⁿ)
This becomes impractical very quickly.
For example:
n = 10
→ around 1,000 operations
n = 20
→ around 1,000,000 operations
n = 30
→ around 1,000,000,000 operations
This is why algorithm design matters so much.
5. Big-O and the Data Structures We Learned
Big-O connects directly to the data structures we've studied.
For example:
| Data Structure | Operation | Typical Complexity |
|---|---|---|
| Array | Access | O(1) |
| Array | Search | O(n) |
| Linked List | Access | O(n) |
| Linked List | Insert at head | O(1) |
| Stack | Push | O(1) |
| Stack | Pop | O(1) |
| Queue | Enqueue | O(1) |
| Queue | Dequeue | O(1) |
| Hash Table | Search | O(1) average |
| BST | Search | O(log n) average |
| Heap | Insert | O(log n) |
| Heap | Extract min/max | O(log n) |
| Graph | BFS/DFS | O(V + E) |
| Trie | Search | O(L) |
This is one reason Big-O should be learned alongside data structures.
The data structure you choose directly affects the complexity of the operations your program performs.
The Bigger Picture
Big-O gives us a way to compare the algorithms and data structures we have learned.
Imagine searching for an element.
With an unsorted array:
O(n)
With a sorted array using binary search:
O(log n)
With a hash table:
O(1) average
With a balanced search tree:
O(log n)
The problem may be the same:
"Find this element."
But the data structure changes the algorithmic complexity.
This is one of the central ideas of computer science:
Choosing the right data structure can transform the performance of an algorithm.
The Most Important Mental Model
Don't memorize Big-O as a list of symbols.
Instead, ask:
"If I make the input 10× larger, how much more work does my algorithm have to do?"
For example:
O(1)
Input grows → work stays roughly the same.
O(log n)
Input grows → work increases slowly.
O(n)
Input grows 10× → work grows roughly 10×.
O(n²)
Input grows 10× → work grows roughly 100×.
O(2ⁿ)
Input grows slightly → work can explode.
That mental model is more useful than simply memorizing the notation.
Summary
Big-O notation describes how an algorithm's resource requirements grow as the input size increases.
The most important complexities to recognize are:
O(1) Constant
O(log n) Logarithmic
O(n) Linear
O(n log n) Linearithmic
O(n²) Quadratic
O(2ⁿ) Exponential
O(n!) Factorial
Remember:
- Big-O describes growth, not exact execution time.
-
nusually represents input size. - Constants are ignored.
- Lower-order terms are ignored.
- Time and space complexity are separate.
- Worst-case complexity is commonly discussed, but best and average cases also matter.
- Data-structure choices strongly affect complexity.
- A theoretically better complexity can become dramatically more important as
ngrows.
The ultimate goal isn't to memorize:
O(1), O(log n), O(n), O(n²)...
The goal is to look at an algorithm and predict how it will scale.
Top comments (0)