Skip to content

Understanding Big-O Notation - Learn Algorithm Complexity

Quick View: — Big-O notation is a way to measure how much time and memory your code can consume as the input grows. Mastering Big-O means identifying cost patterns to make informed decisions.

  • Predictable scalability — avoids surprises when n goes from 1k to 10M
  • Common language — candidates and interviewers discuss solutions using Big-O
  • Objective comparison — quickly decide which implementation to keep and optimize
NotationMeaningCase
Ω (Big Omega)Lower boundBest case
Θ (Big Theta)Lower and upper boundAverage case
O (Big O)Upper boundWorst case

Golden Rule: Use Big-O to guarantee your algorithm never exceeds a certain cost.

  • Worst case (Big O) — input that causes maximum work
  • Average case (Θ) — typical performance for random inputs
  • Best case (Ω) — rarely decisive, but serves as minimum guarantee

Identify everything that can grow: n (array size), m (number of edges), k (tree depth) – they’ll appear in your calculations.

Count comparisons, assignments, memory reads, and function calls that really impact performance. Comments and auxiliary variables without asymptotic cost can be ignored.

If two pieces run one after the other, add their costs:

O(n) // loop A
O(n²) // loop B
───────
O(n²) // result: dominant is n²

For if/else, sum only the most expensive path:

if found: # O(1)
return
else: # O(n)
linear_search()
# Big-O = O(n)

Each inner loop multiplies by the outer:

for i in range(n): # n
for j in range(m): # m
operation() # 1
# → O(n · m)

Write the equation and solve via:

  • Recursion tree
  • Master Theorem
  • Substitution

Remove constants and smaller terms:

3n + 7 → O(n)
n/2 → O(n)
log₂(n) → O(log n) // base doesn't matter

Analyze allocated variables, recursion stack, and buffers.

Ex.: Merge Sort uses O(n) extra; Quick Sort in-place uses O(log n) stack.

Disk/network operations can dominate CPU time – use Big-O separately for CPU and I/O when needed.

Use time, perf, or cProfile to confirm actual behavior matches your asymptotic analysis.

def foo(arr):
n = len(arr) # O(1)
total = 0 # O(1)
# Outer loop => n
for i in range(n):
total += arr[i] # O(1)
# Dependent inner loop => i
for j in range(i):
total += arr[j] # O(1)
return total

Recipe:

  1. Nested loops → sum of series 1 + 2 + … + (n-1) = n(n-1)/2O(n²)
  2. Drop constants → O(n²) time, O(1) space

Practical tip: When reviewing code, write the Big-O in a comment for each block. This trains your eye and helps junior colleagues understand code reviews faster.

StructureInsertSearchDeleteNotes
Static arrayO(n)O(n)O(n)Deletion shifts elements
Dynamic arrayAmort. O(1)O(n)O(1)Reallocates by doubling
Linked listO(1)O(1)O(n)No random access
StackO(1)O(1)LIFO
QueueO(1)O(1)FIFO
Hash tableO(1)¹O(1)¹O(1)¹¹Average; collisions → O(n)
AVL/RB treeO(log n)O(log n)O(log n)Self-balancing
HeapO(log n)O(1) topO(log n)Priority queue
TrieO(L)O(L)O(L)L = key length
def first_element(arr):
return arr[0] # always executes in fixed time
def search_element(arr, x):
for i in range(len(arr)):
if arr[i] == x:
return i
return -1
# One comparison per element → linear growth
def binary_search(arr, x):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == x:
return mid
left, right = (mid + 1, right) if arr[mid] < x else (left, mid - 1)
return -1
# Each step discards half the input → logarithmic curve
for i in range(n):
for j in range(n):
process(i, j) # n × n calls
# Inner loop runs n times for each outer loop iteration
def multiply_matrices(A, B, C, N):
for i in range(N):
for j in range(N):
C[i][j] = 0
for k in range(N):
C[i][j] += A[i][k] * B[k][j]
# Three nested loops → N³ operations
def generate_subsets(arr):
n = len(arr)
for mask in range(1 << n): # 2^n masks
for j in range(n):
if mask & (1 << j):
print(arr[j])
# There are 2^n possible masks → time doubles per element
def permutations(a, l, r):
if l == r:
print(a)
return
for i in range(l, r + 1):
swap(a, l, i)
permutations(a, l + 1, r) # n! permutations
swap(a, l, i)
# Number of permutations is n! → explodes rapidly

Observe how O(n!) explodes compared to O(n log n) — fundamental for architectural choices.

As input size increases:

  • O(1) — horizontal line (ideal)
  • O(log n) — grows slowly
  • O(n) — grows linearly
  • O(n log n) — grows between linear and quadratic
  • O(n²) — grows rapidly
  • O(2ⁿ) — explodes
  • O(n!) — apocalypse

Big-O measures memory too.

  • Merge Sort: O(n log n) time, O(n) space
  • Heap Sort: O(n log n) time, O(1) space — trades more comparisons for less RAM
PitfallProblemSolution
Recursive FibonacciO(2ⁿ)Memoize → O(n)
vector::insertThink it’s O(1)Shifts elements → O(n)
Hash without careAlways assume O(1)Use chaining or open addressing

For recursions of the form T(n) = a T(n/b) + f(n):

CaseConditionResult
1f(n) asymptotically smallerΘ(n^{log_b a})
2f(n) equalΘ(n^{log_b a} · log n)
3f(n) largerΘ(f(n))

Takeaway: Algorithms above O(n log n) can become bottlenecks at scale. Use this list as a mental checklist when proposing solutions or reviewing PRs.

| O(1) | Constant | Access arr[i] | | O(n) | Linear | Search in array | | O(log n) | Logarithmic | Binary Search | | O(n log n) | Quasi-linear | Merge Sort | | O(n²) | Quadratic | Bubble Sort, double loops | | O(n³) | Cubic | Matrix multiply (naive) | | O(2ⁿ) | Exponential | Subsets | | O(n!) | Factorial | Permutations |

Goal: Convert a naive O(n²) algorithm to O(n).

def count_pairs(arr):
count = 0
for i in range(len(arr)):
for j in range(i + 1, len(arr)):
if arr[i] == arr[j]:
count += 1
return count

Two nested loops → quadratic. ❌

from collections import Counter
def count_pairs(arr):
freq = Counter(arr) # O(n)
return sum(c * (c - 1) // 2 for c in freq.values()) # O(k)

Now the algorithm runs in O(n) time and O(k) space. ✅

Q: Why do we ignore constants? A: Because at large scale, proportional factors are irrelevant against asymptotic growth.

Q: What operation does Big-O measure time for? A: Any cost metric: CPU, memory, I/O. Specify which you’re analyzing.

Q: Do I need to memorize all classes? A: No. Understand curve styles: constant, log, linear, quadratic, exponential.

When presenting a solution:

  1. What’s the worst case?
  2. Is there a realistic average case estimate?
  3. Space complexity?
  4. Alternative data structure?
  5. Can you parallelize?
  6. Time vs space trade-off acceptable?

Big-O notation is the ruler that helps identify bottlenecks before they explode in production.

With a critical eye and consistent practice, you’ll start recognizing complex patterns at first glance — and optimizing at the second.

Final Challenge: Take a script from yesterday, estimate its Big-O, then measure with time. How well do your estimates match reality?