Kazi Rahamatullah
Kazi Rahamatullah
AboutProjectsBlogContact
UsesBooks
ResumeView CV
Kazi Rahamatullah

© Copyright 2026 Kazi Rahamatullah

AboutProjectsBlogBooksUses
Twitter/XGitHubProduct HuntCodeSandbox
Back to Blog
DSABig OTime ComplexitySpace ComplexityAlgorithms

Big O Notation — Time & Space Complexity

Master Big O notation — O(1) to O(n!), time and space complexity, loop patterns, recurrence relations, growth chart, and an interactive complexity explorer with interview-ready examples.

Jul 4, 202613 min read
Big O Notation — Time & Space Complexity

Introduction

Big O notation describes how an algorithm's time or space requirements grow as input size n increases. It answers: "If I double the data, does runtime double, explode, or stay flat?" — the single most important lens for writing scalable code and passing technical interviews.

This guide covers:

  1. What Big O measures — worst-case growth, not exact milliseconds
  2. Complexity hierarchy — O(1) through O(n!)
  3. Time complexity patterns — loops, halving, recursion
  4. Space complexity — auxiliary memory and call stack
  5. Interactive explorer — toggle curves and match code patterns
  6. Rules of thumb — drop constants, compare algorithms

Note

Big O describes asymptotic growth — behavior as n → ∞. An O(n²) algorithm can beat O(n log n) on tiny inputs because constants and hardware matter at small scale.

Quick index

#Section
1What is Big O?
2Complexity hierarchy
3Big O growth chart
4Interactive explorer
5Full implementation
6Time complexity patterns
7Space complexity
8Analyzing code step by step
9Common algorithm complexities
10Rules & pitfalls
11Real-life examples

1. What is Big O?

Big O expresses an upper bound on growth rate. We keep the dominant term and drop constants and lower-order terms.

ExpressionSimplified Big OWhy
3n + 5O(n)Drop constant 3 and lower term 5
n² + n + 100O(n²)n² dominates as n grows
2ⁿ + n³O(2ⁿ)Exponential beats polynomial
js
// O(n) — one pass
function findMax(arr) {
  let max = arr[0]
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > max) max = arr[i]
  }
  return max
}
 
// O(1) — hash lookup (average case)
function hasUser(map, id) {
  return map.has(id)
}

Tip

In interviews, state time and space separately: "Time O(n log n) for sorting, space O(1) if we sort in-place."

Back to index


2. Complexity hierarchy

From fastest to slowest growth — memorize this order:

Big ONamen = 10n = 100Rating
O(1)Constant11Excellent
O(log n)Logarithmic~3~7Good
O(n)Linear10100Fair
O(n log n)Linearithmic~33~664Bad
O(n²)Quadratic10010,000Horrible
O(2ⁿ)Exponential1,024≈ 1.27×10³⁰Horrible
O(n!)Factorial3,628,800astronomically largeHorrible

Performance

Prefer O(n log n) or better for large datasets. O(n²) is acceptable only when n is guaranteed small (e.g. n ≤ 500) or the code runs once offline.

Warning

O(2ⁿ) and O(n!) become unusable quickly. Naive Fibonacci recursion and brute-force permutations are classic traps — optimize with memoization (DP) or heuristics.

Back to index


3. Big O growth chart

The chart below shows how operations (y-axis) scale as elements (x-axis) grow. Flat lines stay cheap; steep curves become bottlenecks.

Big O complexity chart — O(1) through O(n!) growth curves

RegionComplexitiesReal-world feel
ExcellentO(1)Same cost at any scale
GoodO(log n)Barely notices 10× data
FairO(n)Doubles when data doubles
BadO(n log n)Sorting-scale — usually fine
HorribleO(n²), O(2ⁿ), O(n!)Breaks at thousands+ elements

Note

The chart uses relative growth — exact operation counts depend on CPU, language, and constants. Big O compares shapes, not wall-clock time.

Back to index


4. Interactive explorer

Toggle complexity curves, drag n, and click code patterns to see operation counts update in real time.

Big O Complexity Explorer

Toggle curves, drag n, and click code patterns to see how operations scale. Y-axis uses log scale.

Operations (log scale)Elements (n)

Operations at n = 10

O(1)1
O(log n)3
O(n)10
O(n log n)33
O(n²)100

Time complexity patterns — click to highlight

Tip

Use the explorer to feel why O(n²) diverges from O(n): set n = 20 and compare O(n) vs O(n²) operation counts side by side.

Back to index


5. Full implementation

js
// O(1) — hash map lookup
function hasUser(usersById, id) {
  return usersById[id] !== undefined
}
 
// O(n) — single pass
function findMax(arr) {
  let max = arr[0]
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > max) max = arr[i]
  }
  return max
}
 
// O(n²) — nested loops
function hasDuplicateNaive(arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] === arr[j]) return true
    }
  }
  return false
}
 
// O(n) — hash set
function hasDuplicateSet(arr) {
  const seen = new Set()
  for (const v of arr) {
    if (seen.has(v)) return true
    seen.add(v)
  }
  return false
}

Function calls with output

js
// ── Demo calls — output shown on the right ──
const users = { u1: 'Alice', u2: 'Bob' }
hasUser(users, 'u1') // → true
findMax([3, 9, 2, 7]) // → 9
hasDuplicateNaive([1, 2, 1]) // → true
hasDuplicateSet([1, 2, 3, 4]) // → false

Back to index


6. Time complexity patterns

Map code structure to Big O — the pattern table every interview expects.

PatternCode shapeTime complexity
Single loopfor (i = 0; i < n; i++)O(n)
Nested fullfor i × for j < nO(n²)
Nested trianglefor j < iO(n²)
Halving / doublingi *= 2 or n /= 2O(log n)
Half rangei < n/2O(n) — constants drop
Sequential loopstwo for n back-to-backO(n) — not O(n²)
Linear recursionT(n) = T(n-1) + O(1)O(n)
Divide & conquerT(n) = 2T(n/2) + O(n)O(n log n)
Fibonacci recursionT(n) = T(n-1) + T(n-2)O(2ⁿ)

O(n) — single loop

js
function sumArray(arr) {
  let total = 0
  for (let i = 0; i < arr.length; i++) {
    total += arr[i]
  }
  return total
}

O(n²) — nested loops

js
function hasDuplicatePair(arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] === arr[j]) return true
    }
  }
  return false
}

O(n²) Nested Loop Grid

Each highlighted cell = one inner-loop iteration. n × n cells = O(n²) operations.

Outer i: — · Comparisons so far: 0 / 36

0,0
0,1
0,2
0,3
0,4
0,5
1,0
1,1
1,2
1,3
1,4
1,5
2,0
2,1
2,2
2,3
2,4
2,5
3,0
3,1
3,2
3,3
3,4
3,5
4,0
4,1
4,2
4,3
4,4
4,5
5,0
5,1
5,2
5,3
5,4
5,5
for (let i = 0; i < 6; i++) {
  for (let j = 0; j < 6; j++) {
    // 36 total iterations → O(n²)
  }
}

Each grid cell is one inner-loop iteration — n × n = O(n²) comparisons.

O(n + k) — scatter & gather (bucket / counting sort)

One pass to count frequencies, one pass to output — linear when value range k is small. See the full step visualizer: Counting Sort.

js
// Count occurrences — O(n)
for (const num of arr) buckets[num]++
 
// Gather in order — O(n + k)
for (let v = 0; v <= maxVal; v++) {
  while (buckets[v]--) sorted.push(v)
}

O(log n) — halving

js
function binarySearch(arr, target) {
  let lo = 0
  let hi = arr.length - 1
  while (lo <= hi) {
    const mid = Math.floor((lo + hi) / 2)
    if (arr[mid] === target) return mid
    if (arr[mid] < target) lo = mid + 1
    else hi = mid - 1
  }
  return -1
}

O(n log n) — merge sort recurrence

js
function mergeSort(arr) {
  if (arr.length <= 1) return arr
  const mid = Math.floor(arr.length / 2)
  const left = mergeSort(arr.slice(0, mid))
  const right = mergeSort(arr.slice(mid))
  return merge(left, right) // O(n) merge × log n levels
}

Note

Two sequential O(n) loops = O(n), not O(n²). Multiplication applies when loops are nested, not sequential.

Back to index


7. Space complexity

Space complexity measures extra memory an algorithm uses — variables, data structures, and the call stack for recursion.

Big OExampleWhat uses memory
O(1)Iterative binary searchFixed pointers lo, hi, mid
O(log n)Recursive binary searchCall stack depth log n
O(n)Copy input to new arrayarr.slice(), result buffer
O(n)Recursive factorial / linear DFSStack depth n
O(n²)2D DP table n × nMatrix for all-pairs paths
js
// O(1) space — in-place swap
function reverseInPlace(arr) {
  let i = 0
  let j = arr.length - 1
  while (i < j) {
    ;[arr[i], arr[j]] = [arr[j], arr[i]]
    i++
    j--
  }
}
 
// O(n) space — new array
function doubleValues(arr) {
  return arr.map((x) => x * 2)
}
 
// O(n) stack space — naive recursive sum
function recursiveSum(arr, i = 0) {
  if (i >= arr.length) return 0
  return arr[i] + recursiveSum(arr, i + 1)
}

Tip

Auxiliary space excludes the input itself unless the problem says otherwise. Clarify in interviews: "O(n) auxiliary for the output array; input not counted."

Warning

Recursive DFS on a graph with depth n uses O(n) stack space — deep trees can cause stack overflow. Iterative BFS/DFS with a queue avoids deep recursion.

Back to index


8. Analyzing code step by step

Use this checklist on any snippet:

  1. Identify the input size — usually n = array.length
  2. Count nested loops — each level multiplies complexity
  3. Check loop bounds — halving → log; triangle inner → still n²
  4. Account for recursion — depth × work per call
  5. Drop constants — n/2, 3n, 100 → O(n)
  6. Keep the dominant term — n² + n → O(n²)
js
function example(n) {
  let count = 0
  for (let i = 0; i < n; i++) {
    count++
  }
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      count++
    }
  }
  return count
}
// Loop 1: O(n)
// Loop 2: O(n²) — nested
// Total: O(n + n²) → O(n²)

Interview Answer

"What is the time complexity of this function?"

Walk the interviewer through structure: identify loops and recursion, multiply nested depths, add sequential blocks, then simplify to the dominant term. State assumptions (e.g. hashMap.get is O(1) average). Mention space if relevant.

Back to index


9. Common algorithm complexities

Algorithm / operationTimeSpace
Array index accessO(1)O(1)
Array push (amortized)O(1)O(1)
Linear searchO(n)O(1)
Binary searchO(log n)O(1) iterative
Bubble / insertion sortO(n²)O(1)
Counting / bucket sortO(n + k)O(n + k)
Merge sortO(n log n)O(n)
Quick sort (average)O(n log n)O(log n) stack
Hash map get/set (avg)O(1)O(n) storage
BFS / DFS (graph)O(V + E)O(V)
Dijkstra (binary heap)O((V + E) log V)O(V)
js
// O(1) average — Map for frequency count
function charFrequency(s) {
  const freq = new Map()
  for (const ch of s) {
    freq.set(ch, (freq.get(ch) ?? 0) + 1)
  }
  return freq
}

Performance

Choosing the right structure beats micro-optimizing loops: HashMap turns O(n) lookup into O(1) average — the difference between O(n²) and O(n) for many interview problems.

Back to index


10. Rules & pitfalls

RuleExample
Drop constantsO(2n) → O(n)
Drop lower termsO(n² + n) → O(n²)
Different inputs use different varsO(a + b), not O(n) if two arrays
Nested → multiplyO(n × m) for n × m grid
Sequential → add, keep maxO(n) + O(n²) → O(n²)
Best vs worst vs averageBinary search worst O(log n); quicksort worst O(n²)

Common mistakes

MistakeReality
"Two loops = O(n²)"Only if nested
"Recursion is always O(n)"Can be O(2ⁿ) — check branching factor
"Big O is exact runtime"It's growth class, not milliseconds
Ignoring spaceO(n log n) time may still need O(n) space

Tip

Production checklist

  • Target O(n log n) or better for user-facing paths on large data
  • Profile before optimizing — measure real bottlenecks
  • Use Maps/Sets to avoid hidden O(n²) .includes() in loops
  • Memoize overlapping recursive subproblems (Fibonacci, DP)
  • State time and space in code reviews and design docs

Warning

Hidden O(n²): arr.filter(...).includes(x) inside a loop, or comparing every pair with nested iteration — common in JavaScript codebases. Replace with a Set for O(n) total.

Back to index


11. Real-life examples

ScenarioComplexityWhy it matters
Database index lookupO(log n)B-tree index on 10M rows → ~23 hops, not 10M scans
Nested React rendersO(n²)Rendering a list inside a list without memoization
HashMap user lookupO(1) avgusersById[id] vs scanning entire user array
Brute-force password crackO(2ⁿ)Each added character doubles search space
Merge two sorted CSV exportsO(n + m)Linear merge — same idea as merge sort's merge step

Signup duplicate-email check

HashSet gives O(n) duplicate detection; nested loops would be O(n²) and fail at scale.

users = 5 signups

alice@x.com, bob@y.com, alice@x.com, carol@z.com, bob@y.com

5 signups — one duplicate email to catch.

Real-world implementation

function findDuplicateEmails(users) {
  const seen = new Set()
  const duplicates = []
  for (const user of users) {
    const email = user.email.toLowerCase()
    if (seen.has(email)) duplicates.push(email)
    else seen.add(email)
  }
  return duplicates
}

function findDuplicateEmailsNaive(users) {
  const duplicates = []
  for (let i = 0; i < users.length; i++) {
    for (let j = i + 1; j < users.length; j++) {
      if (users[i].email === users[j].email) duplicates.push(users[i].email)
    }
  }
  return [...new Set(duplicates)]
}

Full working code for the scenario above.

Function calls with output

// ── Function calls with output ──
const users = [
  { email: "alice@x.com" },
  { email: "bob@y.com" },
  { email: "alice@x.com" },  // duplicate
  { email: "carol@z.com" },
  { email: "bob@y.com" },    // duplicate
]

Side output shows return value after each call — step through to trace execution.

Tip

Before optimizing, ask: "What is n in production?" A O(n²) loop on 50 items is fine; on 50,000 users it breaks.

Back to index


Summary

ConceptTakeaway
Big OUpper bound on growth as n increases
TimeHow operations scale — loops, recursion, divides
SpaceExtra memory + call stack depth
GoalExcellent/Good/Fair for large n; avoid Horrible at scale

Big O is the vocabulary for comparing algorithms before writing code. Use the growth chart for intuition, the interactive explorer for pattern matching, and the hierarchy table for interviews.

Next reads: Counting Sort — Visualized for scatter/gather step breakdown, JavaScript Arrays for built-in method complexities, and System Design Fundamentals for scaling beyond single-machine algorithms.

Share this article

XLinkedInFacebook
Kazi Rahamatullah

Written by

Kazi Rahamatullah

FullStack Developer

X / TwitterGitHubLinkedIn

Subscribe to my newsletter

Stay up to date and get notified when I share new contents.

No spam ever, unsubscribe anytime