Big-O (Worst Case)
How slow can it get? O(n²) means at most n² steps. Drop constants and smaller terms.
When you write code to solve a problem, there are always many ways to do it. Some ways are fast. Some are slow. Some work fine for small inputs but completely break when the input gets big.
Algorithm analysis is how we figure out: “How does my code behave as the input grows?”
This guide covers everything from scratch - no prior knowledge needed.
Imagine you have two ways to find a name in a list of 1 million people.
Both give the correct answer. But one is 50,000 times faster.
That difference matters in real life - your app either loads instantly or freezes.
So analysis helps us answer:
When we say “how fast is this algorithm?”, we need to ask: fast in what situation?
This is the minimum time the algorithm will ever take.
Example: Searching for a name and it happens to be the very first one you check.
This is rarely useful in practice - you cannot count on being lucky every time.
This is the maximum time the algorithm can take.
Example: Searching for a name and it is the very last one, or not there at all.
This is the expected time across all possible inputs.
It is hard to calculate and rarely asked about. Worst case is almost always what matters.
These are just symbols we use to describe how fast the running time grows as input gets bigger.
We do not care about exact time (like 4.2 seconds). We care about the shape of growth.
Big-O tells you: “In the worst case, this algorithm will take at most this long.”
Example 1 - One loop:
for i in range(n): print(i)This runs n times. So it is O(n) - linear time.
Example 2 - Two nested loops:
for i in range(n): for j in range(n): print(i, j)The inner loop runs n times, and the outer loop runs it n times -> total n x n = n².
So it is O(n²) - quadratic time.
Big-O ignores constants and small terms:
O(3n + 10) -> O(n)O(n² + n) -> O(n²)O(5) -> O(1)Why? Because when n is huge (like 1 billion), constants and smaller terms do not matter.
Omega tells you the minimum time an algorithm needs.
Rarely asked on its own. Useful for theory.
Theta is used when best case and worst case are the same order.
for i in range(n): print(i)This always runs exactly n times - no more, no less. So it is Θ(n).
| Notation | Name | What it means |
|---|---|---|
| O(1) | Constant | Same time no matter how big input is |
| O(log n) | Logarithmic | Cuts the problem in half each step |
| O(n) | Linear | One step per input item |
| O(n log n) | Linearithmic | Slightly more than linear (e.g., merge sort) |
| O(n²) | Quadratic | Nested loops - gets slow fast |
| O(2ⁿ) | Exponential | Doubles with each extra input - very bad |
Visual feel for n = 10:
O(1) -> 1 stepO(log n) -> 3 stepsO(n) -> 10 stepsO(n log n) -> 30 stepsO(n²) -> 100 stepsO(2ⁿ) -> 1024 steps - avoid!These are just two different timings of when you analyze code.
You look at the code (loops, recursion, input size) and figure out complexity without running it.
for i in range(n): for j in range(n): print(i, j)You can say: two loops, each runs n -> O(n²) - without ever executing it.
This is the standard way in computer science.
You run the code and measure actual time on a computer.
The problem: results change based on hardware, language, operating system, etc. Not reliable for comparison.
Some algorithms are recursive - they call themselves on a smaller version of the problem. For these, we cannot just count loops. We need a different tool: recurrence relations.
A recurrence relation is just an equation that describes the running time of a recursive function in terms of itself on a smaller input.
Example - a simple recursive function:
def f(n): if n == 1: return f(n // 2)How do we write the time taken?
n/2 -> T(n/2)+ 1So the recurrence is:
This equation does not directly tell you the answer. It just describes the pattern. You have to solve it.
They help analyze:
There are three main methods:
This is the most fundamental method. You expand the recurrence manually, step by step, until you can see a pattern, then you solve it.
The idea: “Replace the recursive part with its own definition, again and again, until you reach the base case.”
You do not guess the answer. You derive it.
Most recurrences you will see look like:
Where b > 1 (the input shrinks each time) and f(n) is extra work done outside the recursive call.
Let us solve , with base case .
Write the recurrence clearly
First substitution - replace T(n/2)
Since , substitute :
Plug back in:
Second substitution - replace T(n/4)
Plug back in:
Find the general pattern after k substitutions
After k steps, the pattern becomes:
Stop at the base case
We stop when .
Solving: , so .
Substitute k into the expression
Since :
Sum the series using the GP formula
The formula for is:
Here and , so:
Since :
Final simplification
Drop constants:
k mathematicallyWhen you do back-substitution, you end up with a series of numbers you need to sum. That sum is either an Arithmetic Progression (AP) or a Geometric Progression (GP).
Example - is this AP or GP?
2, 4, 8, 16, ... each term x 2 -> GP (ratio = 2)2, 5, 8, 11, ... each term + 3 -> AP (difference = 3)| Symbol | Meaning |
|---|---|
| Sum of all terms | |
| First term | |
| Common difference (what you add each time) | |
| Number of terms |
When :
When :
| Symbol | Meaning |
|---|---|
| First term | |
| Common ratio (what you multiply each time) | |
| Number of terms |
This one comes up a lot in algorithm analysis:
| Symbol | Meaning in algorithm context |
|---|---|
| Cost at the root level | |
| Ratio of work between levels | |
| Index of the last level | |
| Cost at the last level | |
| Total number of levels |
This method is the same as back-substitution, but instead of doing algebra, you draw a tree and add up the cost at each level.
Every recursive call is a node in the tree. Each level of the tree represents one round of expansion. You:
Level 0 (root): problem size n, cost = n
Level 1: problem size n/2, cost = n/2
Level 2: problem size n/4, cost = n/4
…
Level k: problem size n/2^k, cost = n/2^k
We stop when size = 1, so k = log₂ n levels.
Total cost - sum of all levels:
This is a GP with , , and terms:
Same answer as back-substitution - just a different way to think about it.
The Master Theorem is a ready-made formula for solving recurrences of a specific common form. Instead of expanding step by step, you just plug in numbers and read off the answer.
The recurrence must look exactly like this:
Where:
a = number of subproblems (how many recursive calls)b = how much the input shrinks each call (b > 1)k = the power of n in the extra work outside the recursionp = the power of log n in the extra work (often 0)Constraints: a >= 1, b > 1, k >= 0.
Examples that fit this form:
Compare and :
Case 1 - If (recursion dominates):
Case 2 - If (recursion and work are balanced):
Case 3 - If (extra work dominates):
Solve:
Step 1 - Identify values: , , , so and .
Step 2 - Compute :
Step 3 - Compare: , so we are in Case 2, and .
Step 4 - Apply formula:
Answer: - this is Merge Sort’s complexity!
| Situation | Best Method |
|---|---|
| You want to understand the derivation deeply | Back-Substitution |
| You want to visualize what is happening | Recursive Tree |
| The recurrence fits | Master Theorem |
| Recurrence does not fit Master Theorem | Back-Sub or Tree |
Big-O (Worst Case)
How slow can it get? O(n²) means at most n² steps. Drop constants and smaller terms.
Recurrence Relation
Describes recursive time: T(n) = T(n/2) + 1. Does not give the answer directly - you must solve it.
Back-Substitution
Expand step by step, find the pattern, stop at the base case, sum the series.
Recursive Tree
Draw the call tree level by level. Sum costs at each level. Total levels = log n usually.
GP vs AP
Same ratio between terms -> GP. Same difference between terms -> AP. Use the right formula to sum.
Master Theorem
Shortcut for T(n) = aT(n/b) + f(n). Compare log_b(a) with k to pick Case 1, 2, or 3.