Chapter 01 — Big-O & Complexity Analysis
Chapter 01 — Big-O & Complexity Analysis
Hey everyone! Welcome to Namaste DSA!
Before we learn a single data structure or algorithm, we need a way to compare them. If I give you two solutions to the same problem, how do you decide which one is "better"? You can't just say "this one feels faster." We need a proper tool — and that tool is Big-O notation.
This is the most important chapter in the entire series. Every single chapter after this one will end with a line like "this runs in O(n)". By the end of today, you'll know exactly what that means and how to figure it out yourself. Let's go deep — but slow and simple!
What we will cover:
- Why we don't measure code in seconds
- What "input size n" actually means
- Counting operations — the simple way
- Big-O explained like you're five
- Every complexity: O(1), O(log n), O(n), O(n log n), O(n²), O(2ⁿ), O(n!)
- How to read Big-O straight off the code (loop rules)
- Why we drop constants and small terms
- Best vs Average vs Worst case
- Time complexity vs Space complexity
- The Big-O cheat sheet (memorize this!)
- Interview Questions
1. Why Not Just Measure in Seconds?
The obvious idea: run the code, start a timer, and whichever finishes faster wins. This is a trap. Here's why measuring time in seconds is useless for comparing algorithms:
┌─────────────────────────────────────────────────────────────┐ │ WHY "SECONDS" IS A BAD MEASURE │ ├─────────────────────────────────────────────────────────────┤ │ │ │ The SAME code takes different time depending on: │ │ │ │ • The COMPUTER → your gaming PC vs an old laptop │ │ • The LANGUAGE → C is faster than Python │ │ • Other APPS → 50 Chrome tabs open? slower │ │ • The INPUT → sorting 10 items vs 10 million items │ │ │ │ So "2 seconds" tells us NOTHING about the algorithm. │ │ It tells us about the machine it ran on. │ │ │ └─────────────────────────────────────────────────────────────┘
We need something that ignores the hardware and only describes the algorithm itself. So instead of asking "how many seconds?" we ask a much smarter question:
"As the input gets bigger, how does the amount of work grow?"
That single question is the whole idea of Big-O. We don't count seconds — we count how the number of steps grows as the input grows.
2. What Is "n" (the Input Size)?
Throughout DSA, n means the size of your input. That's it. What counts as "size" depends on the problem:
| Problem | What "n" means |
|---|---|
| Searching an array of 100 numbers | n = 100 (the array length) |
| Reversing a string of 20 letters | n = 20 (the string length) |
| Counting nodes in a linked list | n = number of nodes |
| Checking all pairs of friends | n = number of people |
Big-O describes the relationship between n and the number of steps. When we say a piece of code is "O(n)", we mean: if the input doubles, the work roughly doubles too.
3. Counting Operations — The Simple Way
Let's count the "steps" in a tiny function. We'll treat each basic action (an assignment, a comparison, an addition) as 1 operation.
function addFirstAndLast(arr) {
const first = arr[0]; // 1 operation
const last = arr[arr.length - 1]; // 1 operation
return first + last; // 1 operation
}
No matter if the array has 5 items or 5 million, this function does 3 operations. The size of the array does not change the work. We call this constant time — written O(1).
Now look at a function that DOES depend on the input:
function sumAll(arr) {
let total = 0; // 1 operation
for (let i = 0; i < arr.length; i++) { // runs n times
total = total + arr[i]; // 1 operation, n times
}
return total; // 1 operation
}
Let's count: 1 (setup) + n (the loop body) + 1 (return) = n + 2 operations.
HAND TRACE: sumAll([10, 20, 30]) → n = 3 total = 0 ← 1 op i=0: total = 0+10 = 10 ← op i=1: total = 10+20 = 30 ← op i=2: total = 30+30 = 60 ← op return 60 ← 1 op ─────────────────────────────── Total = 3 + 2 = 5 operations (n + 2, with n = 3) ✔
So the work is n + 2. As you'll see next, Big-O simplifies "n + 2" down to just O(n).
4. Why We Drop Constants and Small Terms
Big-O cares about the big picture — what happens when n becomes HUGE. When n is a million, the difference between "n" and "n + 2" is meaningless. So we follow two simplification rules:
┌─────────────────────────────────────────────────────────────┐ │ THE TWO SIMPLIFICATION RULES │ ├─────────────────────────────────────────────────────────────┤ │ │ │ RULE 1 — Drop the constants │ │ ────────────────────────── │ │ 5n → O(n) │ │ n/2 → O(n) │ │ 2n + 3 → O(n) │ │ 100 → O(1) │ │ │ │ RULE 2 — Keep only the biggest term │ │ ────────────────────────────────── │ │ n² + n + 5 → O(n²) │ │ n² + 1000n → O(n²) │ │ n + log n → O(n) │ │ │ │ Why? When n is huge, the biggest term dominates │ │ everything else. │ │ │ └─────────────────────────────────────────────────────────────┘
Let's prove Rule 2 with real numbers. Take n² + n + 5:
| n | n² | n | 5 | n² as % of total |
|---|---|---|---|---|
| 10 | 100 | 10 | 5 | 87% |
| 100 | 10,000 | 100 | 5 | 99% |
| 1,000 | 1,000,000 | 1,000 | 5 | 99.9% |
See? As n grows, the n² term becomes basically the entire answer. The "+ n + 5" is a rounding error. That's why we throw it away and just say O(n²).
5. Every Complexity — From Fastest to Slowest
Here is the full ladder, from best (top) to worst (bottom). Memorize this order:
┌─────────────────────────────────────────────────────────────┐ │ BIG-O FROM BEST TO WORST │ ├─────────────────────────────────────────────────────────────┤ │ │ │ FAST ↑ O(1) Constant — the dream │ │ │ O(log n) Logarithmic— binary search │ │ │ O(n) Linear — one simple loop │ │ │ O(n log n) Linearithmic— good sorting │ │ │ O(n²) Quadratic — nested loops │ │ │ O(2ⁿ) Exponential— naive recursion │ │ SLOW ↓ O(n!) Factorial — permutations │ │ │ └─────────────────────────────────────────────────────────────┘
Let's feel how badly the slow ones explode. Imagine n = 100 and each operation is 1 microsecond:
| Complexity | Steps for n = 100 | Rough time |
|---|---|---|
| O(1) | 1 | instant |
| O(log n) | ~7 | instant |
| O(n) | 100 | instant |
| O(n log n) | ~700 | instant |
| O(n²) | 10,000 | 0.01 sec |
| O(2ⁿ) | 1,267,650,600,000,000,000,000,000,000,000 | longer than the age of the universe |
| O(n!) | 9.3 × 10¹⁵⁷ | truly hopeless |
This is why we care so much. A clever O(n) solution finishes instantly; a naive O(2ⁿ) one literally never finishes. Now let's see each one in code.
O(1) — Constant Time
Work does NOT depend on n. Same effort every time.
function getFirst(arr) {
return arr[0]; // always 1 step, no matter the size
}
O(log n) — Logarithmic Time
You cut the problem in half every step. To go from n down to 1 by halving takes about log₂(n) steps. (Don't be scared of "log" — it just answers: "how many times can I halve n before I hit 1?")
n = 16 → 8 → 4 → 2 → 1 that's 4 halvings → log₂(16) = 4
function countHalvings(n) {
let steps = 0;
while (n > 1) {
n = Math.floor(n / 2); // halve each time
steps++;
}
return steps; // ~ log₂(n)
}
This is the magic behind binary search (Chapter 11). A million items? Only ~20 steps. A billion? Only ~30 steps. Logarithmic is almost as good as constant.
O(n) — Linear Time
One loop over the input. Double the input → double the work.
function findMax(arr) {
let max = arr[0]; // 1
for (let i = 1; i < arr.length; i++) { // n times
if (arr[i] > max) max = arr[i];
}
return max;
}
// We MUST look at every element at least once → O(n)
O(n log n) — Linearithmic Time
A linear amount of work, repeated for each of the log n "levels". This is the speed of the best sorting algorithms (Merge Sort, Quick Sort — Chapter 12). If you ever see "sort the array first," you've usually just paid O(n log n).
arr.sort((a, b) => a - b); // JS built-in sort → O(n log n)
O(n²) — Quadratic Time
A loop inside a loop, both running over the input. For every element, you touch every element. This is the classic "brute force" complexity.
function printAllPairs(arr) {
for (let i = 0; i < arr.length; i++) { // n times
for (let j = 0; j < arr.length; j++) { // n times each
console.log(arr[i], arr[j]); // runs n × n = n² times
}
}
}
HAND TRACE: arr = [A, B, C] → n = 3, expect n² = 9 prints i=A → j=A, j=B, j=C (3 prints) i=B → j=A, j=B, j=C (3 prints) i=C → j=A, j=B, j=C (3 prints) ────────────────────────────────── Total = 9 prints = 3² ✔ → O(n²)
O(2ⁿ) — Exponential Time
Every extra input element doubles the work. This shows up in naive recursion that makes two calls each time (like naive Fibonacci — Chapter 05). Avoid it whenever possible.
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2); // 2 calls each time → O(2ⁿ)
}
O(n!) — Factorial Time
The work grows by multiplying: n × (n-1) × (n-2) × … This is the cost of generating all possible orderings (permutations). Usable only for tiny n.
6. How to Read Big-O Straight Off the Code
You don't need to count every operation. Use these simple rules:
┌─────────────────────────────────────────────────────────────┐ │ THE LOOP RULES │ ├─────────────────────────────────────────────────────────────┤ │ │ │ RULE A — No loop, fixed work → O(1) │ │ │ │ RULE B — One loop over n → O(n) │ │ │ │ RULE C — Loop INSIDE a loop (both n)→ O(n²) │ │ (three nested → O(n³), etc.) │ │ │ │ RULE D — Loop that HALVES n → O(log n) │ │ │ │ RULE E — Two SEPARATE loops (one → O(n) │ │ after the other) (n + n = 2n → n) │ │ │ │ RULE F — Loops over DIFFERENT inputs→ O(n + m) │ │ (n items then m items) (keep both!) │ │ │ └─────────────────────────────────────────────────────────────┘
Two gotchas people get wrong:
GOTCHA 1 — Sequential loops ADD, they don't multiply:
for (...n...) { } // O(n)
for (...n...) { } // O(n)
→ O(n) + O(n) = O(2n) = O(n) NOT O(n²)!
Nested = multiply. Sequential = add.
GOTCHA 2 — Different inputs keep BOTH letters:
for (let i of arrA) { } // n
for (let j of arrB) { } // m
→ O(n + m). You CANNOT call this O(n) —
arrB might be huge while arrA is tiny.
7. Best Case vs Average Case vs Worst Case
The same algorithm can do different amounts of work depending on the specific input. Consider searching a list one item at a time (linear search):
function linearSearch(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) return i; // found it — stop early!
}
return -1;
}
| Case | When | Work |
|---|---|---|
| Best (Ω, "Omega") | Target is the FIRST element | O(1) — found immediately |
| Average (Θ, "Theta") | Target is somewhere in the middle | O(n) |
| Worst (O, "Big-O") | Target is LAST, or not present | O(n) — checked everything |
The convention: when people say "Big-O" without qualifying, they almost always mean the worst case. We plan for the worst because that's the guarantee — "this will never be slower than O(n)."
8. Time Complexity vs Space Complexity
Big-O isn't only about time. We also measure extra memory an algorithm uses — that's space complexity. It follows the exact same notation.
┌─────────────────────────────────────────────────────────────┐ │ TIME vs SPACE │ ├─────────────────────────────────────────────────────────────┤ │ │ │ TIME complexity → how many STEPS as n grows │ │ SPACE complexity → how much EXTRA MEMORY as n grows │ │ │ │ "Extra" = memory you create, NOT the input itself. │ │ │ └─────────────────────────────────────────────────────────────┘
Compare two ways to double every number in an array:
// VERSION A — builds a NEW array → O(n) extra space
function doubleNew(arr) {
const result = []; // grows to size n
for (let i = 0; i < arr.length; i++) {
result.push(arr[i] * 2);
}
return result; // O(n) time, O(n) space
}
// VERSION B — changes the array in place → O(1) extra space
function doubleInPlace(arr) {
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i] * 2; // no new array created
}
return arr; // O(n) time, O(1) space
}
Both are O(n) in time, but Version B uses O(1) extra space because it doesn't allocate a new array. This time-vs-space trade-off comes up constantly — sometimes you spend memory to save time (that's exactly what hashing does in Chapter 04).
Heads up — recursion uses space too! Every recursive call sits on the call stack until it returns. A recursion that goes n deep secretly uses O(n) space, even if it looks like it allocates nothing. We'll see this clearly in Chapter 05.
9. The Big-O Cheat Sheet (Memorize This!)
These are the complexities of operations you'll use in every future chapter. You don't need to derive them yet — just get familiar. We'll prove each one when we reach that data structure.
| Data Structure | Access | Search | Insert | Delete |
|---|---|---|---|---|
| Array (by index) | O(1) | O(n) | O(n) | O(n) |
| Hash Map / Set | — | O(1)* | O(1)* | O(1)* |
| Linked List | O(n) | O(n) | O(1)** | O(1)** |
| Stack / Queue | — | O(n) | O(1) | O(1) |
| Binary Search Tree (balanced) | O(log n) | O(log n) | O(log n) | O(log n) |
| Heap | — | O(n) | O(log n) | O(log n) |
* Hash map = O(1) AVERAGE case. Worst case (lots of collisions) is O(n). ** Linked list insert/delete is O(1) ONLY if you already hold the node. Finding the spot first still costs O(n).
And the common algorithm complexities:
| Algorithm | Time |
|---|---|
| Linear Search | O(n) |
| Binary Search | O(log n) |
| Bubble / Selection / Insertion Sort | O(n²) |
| Merge Sort / Quick Sort (avg) | O(n log n) |
| BFS / DFS on a graph | O(V + E) |
10. Common Mistakes People Make
✗ MISTAKE 1: "It has two loops, so it's O(n²)."
✔ Only if one is NESTED inside the other.
Side-by-side loops are O(n).
✗ MISTAKE 2: Forgetting hidden loops.
✔ arr.includes(x), arr.indexOf(x), and "x in array"
each scan the array → O(n). A nested .includes()
secretly makes your code O(n²)!
✗ MISTAKE 3: Ignoring built-in costs.
✔ arr.sort() is O(n log n). arr.unshift() is O(n)
(it shifts every element). string + string in a
loop can be O(n²).
✗ MISTAKE 4: Counting the input as "extra" space.
✔ Space complexity is the EXTRA memory you allocate,
not the input you were given.
✗ MISTAKE 5: Saying "O(2n)" or "O(n + 5)" in an interview.
✔ Always simplify. It's O(n). Constants are dropped.
Interview Questions — Quick Fire!
Q: What is Big-O notation?
"Big-O describes how the running time or memory of an algorithm grows as the input size grows. It's not about exact seconds — it's about the growth rate. It lets us compare algorithms independently of the hardware or language they run on. By convention, Big-O describes the worst case."
Q: Why don't we just measure the time in milliseconds?
"Because actual time depends on the machine, the language, the compiler, and what else is running. The same algorithm could be 2 seconds on one laptop and 0.2 seconds on a faster one. Big-O ignores all of that and describes the algorithm itself — how the work scales with the input."
Q: Why do we drop constants and lower-order terms?
"Because Big-O is about behaviour as n approaches infinity. When n is huge, the largest term completely dominates — a constant multiplier or a smaller term becomes negligible. So 5n² + 3n + 100 simplifies to O(n²). It keeps the notation about the growth rate, not the exact step count."
Q: What's the difference between O(n) and O(n²)?
"O(n) means the work grows in proportion to the input — usually a single loop. O(n²) means the work grows with the square of the input — typically a nested loop where, for each element, you process every element. If the input doubles, an O(n) algorithm does twice the work, but an O(n²) algorithm does four times the work."
Q: What does O(log n) mean and where does it come from?
"O(log n) means the algorithm reduces the problem size by a constant factor — usually half — each step. To go from n down to 1 by halving takes about log₂(n) steps. Binary search is the classic example: a million elements take only about 20 steps. It's almost as fast as constant time."
Q: What's the difference between time and space complexity?
"Time complexity measures how the number of operations grows with input size. Space complexity measures how much extra memory the algorithm needs as input grows — not counting the input itself. For example, reversing an array in place is O(1) space, but copying it into a new array is O(n) space. Recursion also costs space because of the call stack."
Q: What is the worst case vs average case vs best case?
"They describe how the algorithm performs on different inputs. Best case is the luckiest input, worst case is the hardest input, and average case is the typical one. For linear search: best case O(1) if the target is first, worst case O(n) if it's last or missing. When people say 'Big-O' unqualified, they mean the worst case."
Q: Is a hash map lookup always O(1)?
"On average, yes — that's the whole point of hashing. But the worst case is O(n), when many keys hash to the same bucket and collisions pile up. In practice with a good hash function it's effectively O(1), which is why we say 'O(1) average'."
Q: If I have two separate loops over the same array, is it O(n²)?
"No. Two loops one after the other add up: O(n) + O(n) = O(2n) = O(n). It's only O(n²) when one loop is nested inside the other, so the inner loop runs fully for every iteration of the outer one."
Quick Recap
| Concept | Key Takeaway |
|---|---|
| Big-O | Describes how work GROWS with input size n — not actual seconds. |
| n | The size of the input (array length, number of nodes, etc.). |
| Drop constants | 5n → O(n). 2n + 3 → O(n). 100 → O(1). |
| Keep biggest term | n² + n + 5 → O(n²). The largest term dominates. |
| O(1) | Constant — work doesn't depend on n. The dream. |
| O(log n) | Halve the problem each step. Binary search. |
| O(n) | One loop over the input. |
| O(n log n) | Best sorting algorithms (merge, quick). |
| O(n²) | Nested loops — brute force. Try to improve it. |
| O(2ⁿ) / O(n!) | Exponential / factorial — usable only for tiny n. |
| Nested vs sequential | Nested loops MULTIPLY (n²). Sequential loops ADD (n). |
| Worst case | The default meaning of Big-O — the guarantee. |
| Space complexity | Extra memory used. In-place = O(1). Recursion costs stack space. |
What's Next?
In Chapter 02, we will:
- Open up the Array — the most fundamental data structure of all
- Learn how arrays live in memory as contiguous boxes, and why index access is O(1)
- See the real cost of inserting, deleting, and searching (and use our new Big-O skills to explain why!)
- Master the prefix sum pattern that turns repeated O(n) work into O(1)
- Meet Kadane's Algorithm for the maximum subarray problem
Make sure you:
- Can look at a loop and say its Big-O out loud in 2 seconds
- Memorize the complexity ladder: O(1) → O(log n) → O(n) → O(n log n) → O(n²) → O(2ⁿ) → O(n!)
- Practice on real code — open any function you've written and find its Big-O
- Remember the golden rule: nested loops multiply, sequential loops add
Keep coding, keep grinding! See you in the next one!
Post a Comment