0/23in Data Structures & Algorithms
Data Structures & Algorithms

Complexity & Big-O

Reason about time and space the way interviewers do. Watch how different growth rates diverge as input size climbs.

~14 minLesson 12 of 60
Your progressNot started

Before you can compare two solutions, you need a language for “how expensive is this?” that ignores the noise of a particular laptop, language, or input. That language is Big-O notation: a way to describe how an algorithm’s cost grows as its input grows.

The key word is grows. Big-O throws away constants and lower-order terms because, as the input gets large, only the fastest-growing term matters. A loop that does 3n + 50 operations is simply O(n) — double the input and you roughly double the work.

Watch the curves diverge

The whole reason we obsess over Big-O is this picture. At small inputs, an O(n²) algorithm can look perfectly fine. Drag the slider and watch what happens as the input grows — the quadratic curve leaves everything else behind.

Growth rates as input size climbsn = 16
input size n →operations →
O(1)1O(log n)4O(n)16O(n log n)64O(n²)256

Notice that O(log n) is almost flat: even at n = 40 it has barely moved. That flatness is why binary search and balanced trees feel like magic. Meanwhile O(n²) is the shape of a nested loop, and it is the complexity you most often need to avoid in an interview.

The complexities you must recognize on sight

Notation Name Typical source
O(1) Constant Hash lookup, array index, arithmetic
O(log n) Logarithmic Binary search, balanced tree operations
O(n) Linear A single pass over the data
O(n log n) Linearithmic The best comparison sorts, divide & conquer
O(n²) Quadratic Nested loops over the same collection
O(2ⁿ) Exponential Naive recursion over subsets

Time is not the only cost

Every algorithm also has a space complexity — the extra memory it needs beyond the input. A recursive function that goes n levels deep uses O(n) stack space. Building a hash set of every element uses O(n) memory. Interviewers love the time/space trade-off, because “make it faster by using more memory” (or the reverse) is the single most common optimization in real systems.

Rule of thumb: state your solution’s time and space complexity out loud, unprompted. It signals that you think about cost by default.

How to reason about it quickly

  1. Count the loops. Independent loops add; nested loops over the same input multiply. Two separate passes are O(n) + O(n) = O(n). A loop inside a loop is O(n²).
  2. Find the halving. If each step throws away a constant fraction of the remaining work, you have a logarithm.
  3. Recursion → recurrence. “Do O(n) work, then two half-size calls” is the merge-sort recurrence and resolves to O(n log n).

Keep this lens on as you go through the rest of the module — every pattern that follows is really just a trick to move a solution down this table.

Pattern recognition

Variations

Worked problems

These are not about inventing an algorithm. They are about reading code like an interviewer reads it: count the dominant work, then state the space trade-off.

Easy
  • Complexity
  • Hash set

Analyze a Set-Based Scan

Given this function, identify its time and auxiliary space complexity in terms of n = len(nums):

def first_duplicate(nums):
    seen = set()
    for x in nums:
        if x in seen:
            return x
        seen.add(x)
    return None

Approach. The loop may return early, but Big-O usually asks for the worst case. Hash lookups and inserts are average O(1), so the loop dominates. The set can grow to hold every value.

Show solution
def first_duplicate(nums):
    seen = set()          # up to n elements
    for x in nums:        # at most n iterations
        if x in seen:     # average O(1)
            return x
        seen.add(x)       # average O(1)
    return None

Complexity. O(n) time and O(n) auxiliary space. Early returns can improve a specific run, but they do not change the worst-case bound.

Medium
  • Complexity
  • Nested loops

Count the Nested Work

What is the time complexity of this pair-counting function, and why is it not O(n) just because the inner loop gets shorter?

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

Approach. Add the inner-loop lengths: (n - 1) + (n - 2) + ... + 1. That sum is n(n - 1) / 2, and Big-O keeps the dominant term.

Show solution
def pair_iterations(n):
    return n * (n - 1) // 2

Complexity. O(n²) time and O(1) auxiliary space. Triangular nested loops are still quadratic; they just do about half as many iterations as the full n * n grid.

Medium
  • Complexity
  • Trade-off

Choose the Better Membership Strategy

You have n blocked user IDs and q incoming IDs to check. Approach A scans the blocked list for every query. Approach B builds a set once, then checks each query against the set. Which approach scales better?

Approach. Compare total cost, not just setup. A repeated scan costs O(n) per query, so O(n · q). A set costs O(n) to build and average O(1) per query, so O(n + q), at the price of O(n) extra memory.

Show solution
def mark_blocked(blocked_ids, incoming_ids):
    blocked = set(blocked_ids)
    return [user_id in blocked for user_id in incoming_ids]

Complexity. O(n + q) time and O(n) auxiliary space. This beats O(n · q) once you have more than a tiny number of queries.

Your progressNot started