Skip to content

Understanding Big-O Notation - Learn Algorithm Complexity

Feb 25, 2026 · by Dionisio · 8 min read

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
Notation Meaning Case
Ω (Big Omega) Lower bound Best case
Θ (Big Theta) Lower and upper bound Average case
O (Big O) Upper bound Worst 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)/2 → O(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.

Structure Insert Search Delete Notes
Static array O(n) O(n) O(n) Deletion shifts elements
Dynamic array Amort. O(1) O(n) O(1) Reallocates by doubling
Linked list O(1) O(1) O(n) No random access
Stack O(1) — O(1) LIFO
Queue O(1) — O(1) FIFO
Hash table O(1)¹ O(1)¹ O(1)¹ ¹Average; collisions → O(n)
AVL/RB tree O(log n) O(log n) O(log n) Self-balancing
Heap O(log n) O(1) top O(log n) Priority queue
Trie O(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
Pitfall Problem Solution
Recursive Fibonacci O(2ⁿ) Memoize → O(n)
vector::insert Think it’s O(1) Shifts elements → O(n)
Hash without care Always assume O(1) Use chaining or open addressing

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

Case Condition Result
1 f(n) asymptotically smaller Θ(n^{log_b a})
2 f(n) equal Θ(n^{log_b a} · log n)
3 f(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?