Time Complexity
Big O, Big Omega, Big Theta and the little-o cousins — every notation used to describe how an algorithm grows, with practice questions along the way.
Why not just time the code?
You could run two solutions and compare stopwatches, but the result would depend on your laptop, the language, and what else was running. Complexity analysis instead measures how the work grows as the input grows. That answer is the same everywhere.
We care about growth because inputs get bigger. A solution that takes 1 second on 1,000 items might take 1 second on 10,000 items, or it might take 100 seconds. Time complexity tells you which.
The common growth classes
| Notation | Name | Example |
|---|---|---|
| Constant | Array index access | |
| Logarithmic | Binary search | |
| Linear | Single loop over input | |
| Linearithmic | Merge sort, efficient sorting | |
| Quadratic | Nested loop over the same array | |
| Exponential | Naive recursive Fibonacci | |
| Factorial | Generating all permutations |
What the difference actually means
For , assuming a rough one million operations per second:
| Complexity | Operations | Time |
|---|---|---|
| ~20 | Instant | |
| 1,000,000 | ~1 second | |
| ~20,000,000 | ~20 seconds | |
| 1,000,000,000,000 | ~11 days |
This is why solutions fail on large inputs in coding rounds. It is not that they are wrong — they simply never finish.
Quick check
Before moving on, order these from fastest-growing to slowest: , , , .
<details> <summary>Answer</summary>— exponential dwarfs quadratic, which dwarfs linear, which dwarfs logarithmic.
</details>The full family of notations
Big O is the one everyone quotes, but it is only one member of a family called asymptotic notation. Each member answers a slightly different question about how a function grows.
Big O — the upper bound
means grows no faster than , for large enough . It is a ceiling — the algorithm is at most this bad.
Formally: there exist constants and such that for all .
f(n) = 3n + 5 → O(n)
Because never grows faster than, say, once is large enough.
This is what interviews mean by default when they say "what is the complexity" — the worst-case upper bound.
Big Omega (Ω) — the lower bound
means grows at least as fast as . It is a floor — the algorithm takes at least this long.
Linear search is Ω(1) — best case, found immediately
Linear search is O(n) — worst case, scanned everything
Omega and O are opposites. O caps the worst case; Ω floors the best case.
Big Theta (Θ) — the tight bound
means grows exactly as fast as — both an upper and lower bound at once. This is the most precise statement you can make.
Summing an array is always exactly n additions.
That is Θ(n) — not just "at most n", but "exactly proportional to n" every time.
If best case and worst case are the same order, you can say Θ. If they differ — like linear search's best case versus worst case — you cannot; you can only bound each case separately with O or Ω.
Little o — the strict upper bound
means grows strictly slower than — it says "and never catches up", not just "does not exceed".
n = o(n^2) true, n is strictly smaller for large n
n = O(n) true
n = o(n) false — n does not grow strictly slower than itself
The difference from Big O: allows and to grow at the same rate (up to a constant). Little demands becomes insignificant compared to as grows.
Little omega (ω) — the strict lower bound
is the mirror of little o: grows strictly faster than .
n^2 = ω(n) true, n² eventually dwarfs n
n = ω(n) false — equal growth, not strictly faster
The full picture, side by side
| Notation | Reads as | Bound type | Analogy |
|---|---|---|---|
| grows at most as fast as | Upper (may be tight or loose) | ||
| grows at least as fast as | Lower (may be tight or loose) | ||
| grows exactly as fast as | Tight, both directions | ||
| grows strictly slower than | Strict upper | ||
| grows strictly faster than | Strict lower |
You will use in nearly every interview answer. comes up when someone asks "is that bound tight?". , little and little mostly appear in academic contexts or when a professor is testing whether you understand what Big O actually promises.
Quick check
Which notation would you use to say "this algorithm takes exactly n² steps, no more, no less, on every input"?
<details> <summary>Answer</summary>— since both the upper and lower bound are the same, the tight bound applies.
</details>Why we drop constants and lower-order terms
If an algorithm does operations, we call it — not .
We drop the constants because they do not affect the shape of the curve. For large , the difference between and is a fixed multiple, while the difference between and grows without limit. Big O cares about the shape, not the exact count.
f(n) = 3n + 50 → O(n)
f(n) = 5n² + 3n + 100 → O(n²) the n² term dominates everything else
Working out the complexity
Rule 1: Count the loops over the input
A single pass is linear.
def total(nums):
result = 0
for n in nums: # runs n times
result += n
return result
This is .
Practice question
What is the time complexity of this function?
def print_pairs_sum(nums, k):
for n in nums:
if n == k:
print(n)
if check inside it.
</details>
Rule 2: Nested loops multiply
def has_duplicate(nums):
for i in range(len(nums)): # n times
for j in range(i + 1, len(nums)): # up to n times
if nums[i] == nums[j]:
return True
return False
The inner loop runs for each iteration of the outer, so this is .
Practice question
What is the complexity of three nested loops, each running times?
<details> <summary>Answer</summary>— each additional nested loop multiplies in another factor of .
</details>Rule 3: Sequential blocks add, then you keep the largest
def process(nums):
for n in nums: # O(n)
print(n)
for a in nums: # O(n^2)
for b in nums:
print(a, b)
That is . The smaller term disappears because it stops mattering as grows.
Practice question
A function does an sort, then a single pass over the result. What is the overall complexity?
<details> <summary>Answer</summary>— it dominates , since grows faster than alone.
</details>Rule 4: Halving the input each step is logarithmic
def binary_search(nums, target):
low, high = 0, len(nums) - 1
while low <= high:
mid = (low + high) // 2
if nums[mid] == target:
return mid
if nums[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
Each iteration throws away half the remaining range, so the loop runs about times. For a million items, that is roughly 20 steps.
Note the C++ version useslow + (high - low) / 2rather than(low + high) / 2. With large indices the naive form can overflow a 32-bitintand produce a negative midpoint. Python integers are arbitrary precision, so it never bites there — but the habit is worth carrying across.
Practice question
An algorithm halves its input at every step, doing work per step. What is its time complexity?
<details> <summary>Answer</summary>— the number of halvings needed to shrink down to 1 is .
</details>Best, average and worst case
Every one of the notations above (O, Ω, Θ) can be applied to three different scenarios:
- Best case — the friendliest possible input
- Average case — a typical, random input
- Worst case — the least friendly possible input
Take linear search for a target that may or may not be present:
- Best case — the target is first. , or more precisely .
- Average case — the target is somewhere in the middle. .
- Worst case — the target is last or absent. .
Unless stated otherwise, assume worst case. It is the only bound that guarantees anything — a best-case claim tells you nothing about what happens on a bad day, and interviewers are testing whether your solution survives one.
Practice question
Quicksort has an average time complexity of but a worst case of . If someone asks "what is the time complexity of quicksort?" with no further context, which should you give?
<details> <summary>Answer</summary>State both, but lead with the worst case: " worst case, average — the worst case happens with a poor pivot choice, like an already-sorted array with a naive last-element pivot." That answer signals real understanding rather than a memorised number.
</details>Test yourself
Work out the complexity of this function before reading on, then run it to check your reasoning.
The first loop is and the nested loop is . Added together and keeping the dominant term, the function is .
One more round of practice
Try to state the tightest notation you can (Big O is fine, Θ is better if you can justify it) before checking each answer.
1. A function does a singlefor loop that always runs exactly times, with work each iteration.
<details>
<summary>Answer</summary>
— every input of size takes exactly the same number of steps, so the bound is tight in both directions.
</details>2. A function checks if the first element of an array equals a target, and returns immediately either way.
<details> <summary>Answer</summary>— constant work regardless of array size.
</details> 3. Recursive Fibonacci without memoization:fib(n) = fib(n-1) + fib(n-2).
<details>
<summary>Answer</summary>
— each call branches into two more calls, and the tree of calls roughly doubles in size at each level.
</details>4. A hash map lookup, in the worst case where every key collides into one bucket.
<details> <summary>Answer</summary>worst case, even though it is on average. Interviewers who ask "what if every key collides?" are testing whether you know the worst case exists at all.
</details>5. Generating every permutation of an array of size .
<details> <summary>Answer</summary>— there are distinct orderings, and producing each one takes at least constant work.
</details>Common mistakes
- Keeping constants. is just . Write the simplified form.
- Forgetting hidden costs. Sorting inside a loop is not free —
sort()is every time it runs. - Assuming nested loops are always quadratic. If the inner loop runs a fixed number of times, it is still .
- Confusing with . The first halves the problem; the second does linear work at each of levels.
- Using Big O when Big Theta is meant. Saying "the complexity is " for an algorithm that always takes exactly steps is technically true but weaker than saying — O only claims a ceiling, Θ claims the exact shape.
- Quoting only the average case. Always mention the worst case unless the question specifically asks for average.
Summary
Time complexity describes growth, not wall-clock time. Big O gives an upper bound, Big Omega a lower bound, and Big Theta a tight bound that pins down both at once — little o and little omega are their strict versions, mostly seen in academic settings. Count loops, multiply when nested, add when sequential and keep the dominant term, and always default to worst case unless told otherwise.