Imagine two delivery apps. Both show you the nearest restaurant — but one takes 2 seconds, the other takes 20. They both "work," but only one is usable at scale. This is what complexity analysis is all about: not just does it work, but how well does it scale?

What is Time Complexity?

Time complexity describes how the number of operations your algorithm performs grows as the input size (n) increases. It is not measuring actual clock time — it's measuring the growth rate of work done.

We express time complexity using Big O notation — a mathematical way to describe the upper bound (worst-case scenario) of an algorithm's performance.

Why worst-case? Because we need guarantees. If your app is slow for even a few users, that's a problem. Big O tells you how bad things can get.

Common Time Complexities with Examples

1. O(1) — Constant Time

The algorithm always does the same number of operations, no matter how large the input is.

python — O(1) Constant Time
def get_first(arr):
    return arr[0]   # O(1)

def get_value(d, key):
    return d[key]   # O(1)

2. O(log n) — Logarithmic Time

The algorithm halves the problem with each step. Classic example: Binary Search.

python — O(log n) Binary Search
def binary_search(arr, target):
    low, high = 0, len(arr) - 1

    while low <= high:
        mid = (low + high) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return -1

# For n = 1,000,000 elements → only ~20 comparisons needed!

3. O(n) — Linear Time

The algorithm visits every element once. If input doubles, work doubles.

python — O(n) Linear Search
def find_max(arr):
    max_val = arr[0]
    for num in arr:
        if num > max_val:
            max_val = num
    return max_val

4. O(n log n) — Linearithmic Time

This is the complexity of most efficient sorting algorithms like Merge Sort and Quick Sort.

python — O(n log n) Merge Sort
def merge_sort(arr):
    if len(arr) <= 1:
        return arr

    mid = len(arr) // 2
    left  = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i]); i += 1
        else:
            result.append(right[j]); j += 1
    return result + left[i:] + right[j:]

5. O(n²) — Quadratic Time

A loop inside a loop. If input doubles, work quadruples. Classic example: Bubble Sort.

python — O(n²) Bubble Sort
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

Avoid O(n²) for large datasets. An array of 10,000 elements with an O(n²) algorithm does 100 million operations — what O(n log n) does in roughly 130,000.

6. O(2ⁿ) — Exponential Time

Each step doubles the work. Usually seen in recursive brute-force solutions.

python — Exponential vs Memoized
# BAD: Exponential
def fib_bad(n):
    if n <= 1:
        return n
    return fib_bad(n - 1) + fib_bad(n - 2)   # O(2ⁿ)

# GOOD: Memoized — O(n)
from functools import lru_cache

@lru_cache(maxsize=None)
def fib_good(n):
    if n <= 1:
        return n
    return fib_good(n - 1) + fib_good(n - 2)

Growth Rate Comparison (Visual)

For n = 100 elements, here's how many operations each complexity requires:

O(1)
1 op
O(log n)
~7 ops
O(n)
100 ops
O(n log n)
~664 ops
O(n²)
10,000 ops
O(2ⁿ)
10³⁰ ops 🔥

Big O Cheat Sheet

NotationNameExampleRating
O(1)ConstantArray index access, dict lookupExcellent
O(log n)LogarithmicBinary search, BST operationsGreat
O(n)LinearLinear search, single loopFair
O(n log n)LinearithmicMerge sort, quicksort, heapsortAcceptable
O(n²)QuadraticBubble sort, nested loopsBad
O(2ⁿ)ExponentialNaive recursion, subset generationTerrible
O(n!)FactorialPermutation brute-forceAvoid

What is Space Complexity?

Space complexity measures how much extra memory your algorithm uses relative to the input size. This includes variables, call stacks (for recursion), and any additional data structures you create.

It does not usually count the input itself — only the auxiliary space (extra memory on top of the input).

Time Complexity

Measures how many operations grow with input size n. Focused on CPU cycles and computation speed.

Space Complexity

Measures how much extra memory is consumed as n grows. Focused on RAM usage and memory efficiency.

Space Complexity Examples

O(1) Space — Constant

python — O(1) Space
def sum_array(arr):
    total = 0
    for num in arr:
        total += num
    return total
# Space: O(1)

O(n) Space — Linear

python — O(n) Space
def duplicate_array(arr):
    result = []
    for num in arr:
        result.append(num)
    return result
# Space: O(n)

O(n) Space — Recursive Call Stack

python — Call Stack Space
def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n - 1)
# Space: O(n) due to call stack depth

O(n²) Space — Quadratic

python — O(n²) Matrix Space
def create_matrix(n):
    matrix = [[0] * n for _ in range(n)]
    return matrix
# Space: O(n²)

The Classic Trade-off: Time vs Space

python — Time vs Space Trade-off
# Approach 1: Brute Force
# Time: O(2ⁿ)  |  Space: O(n) call stack
def fib_brute(n):
    if n <= 1: return n
    return fib_brute(n-1) + fib_brute(n-2)

# Approach 2: Memoization (trade space for time)
# Time: O(n)   |  Space: O(n)
memo = {}
def fib_memo(n):
    if n in memo: return memo[n]
    if n <= 1: return n
    memo[n] = fib_memo(n-1) + fib_memo(n-2)
    return memo[n]

# Approach 3: Bottom-up DP (optimal)
# Time: O(n)   |  Space: O(1)
def fib_dp(n):
    if n <= 1: return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

The bottom-up DP approach is the winner — O(n) time and O(1) space. Always try to find the approach that avoids unnecessary memory use without sacrificing speed.

How to Analyze an Algorithm Step-by-Step

python — Step-by-Step Analysis
def example(arr):
    for x in arr:       # O(n)
        print(x)

    for i in arr:       # O(n²) — nested
        for j in arr:
            print(i, j)

# Total: O(n) + O(n²) → O(n²)

Quick Summary

ConceptWhat it measuresUnitGoal
Time ComplexityOperations as n growsOperations / StepsAs small as possible
Space ComplexityExtra memory as n growsMemory (bytes)As small as possible
Big O NotationWorst-case upper boundO(…)Understand the worst scenario

Practice challenge: Given this code — for i in range(n): for j in range(i, n): print(i, j) — what is the time complexity? (Answer: O(n²), since the total iterations ≈ n(n+1)/2 which simplifies to O(n²).)

Understanding Big O is foundational to writing scalable software, cracking DSA interview questions, and making smart engineering decisions. Practice by analyzing every function you write — ask yourself: what's the time complexity? What's the space complexity? That habit alone will make you a better developer.