Time and space complexity

Big O in plain words: count the steps, keep the biggest term, and know what fits in one second.

Pattern: Complexity25 min read
Quick recall2 questions
  1. 01What are the six steps to attack a new problem?

    Read it, try small examples by hand, write the brute force, find the waste, code it, then dry run it.

  2. 02Why does a Set beat the pair check when you look for a duplicate?

    A Set answers "have I seen this?" in one step. So one pass of n lookups replaces about n × n / 2 comparisons.

The table from the last lesson had a column called "time grows like". It said n × n for the pair check and n for the Set. In this lesson that column gets its real name, Big O.

After your code runs, the interviewer asks for its time and space complexity. This lesson teaches you to answer for any code you write.

Count steps, not seconds

Run the same code on an old laptop and a new one. The old one might take 2 seconds, the new one 1 second. Seconds describe the machine, not the code.

The number of steps is the same on every machine. So we count steps, and ask one question: how does the count grow when the input grows?

is the answer, written as a formula of n, the input size.

  • O(n): double the input, double the work.
  • O(n²): double the input, four times the work.

Always the worst case

Big O describes the input that makes your code work hardest.

  • The Set check from the last lesson can stop at the second item, if the duplicate is there.
  • When nothing repeats, it reads all n items.
  • So the Set check is O(n).

You plan for the input that hurts, not the lucky one.

Keep the biggest term, drop the constant

Counting every step gives a long formula. Two rules turn it into Big O:

  1. Keep the biggest term. For large n, it is the only one that matters.
  2. Drop the constant in front. 3n² and n² grow the same way.
Keep the term that grows fastest, then drop its constant.

Why drop so much? Take n = 1,000:

  • 3n² is 3,000,000.
  • 5n + 2 is only 5,002, less than 0.2 percent of the total.

The constant goes for a similar reason. Double n, and both 3n² and n² grow four times. Big O records that growth, not the exact count.

Read the loops

Most of the time, the shape of the loops tells you the Big O. Four shapes cover most code.

Constant: O(1)

int first(int[] nums) {
    return nums[0];
}

One step at any size. Reading an index and a Set lookup are O(1) too.

Linear: O(n)

long total(int[] nums) {
    long sum = 0;
    for (int x : nums) sum += x;
    return sum;
}

The loop body runs once per item, so n items take n steps. Two loops one after the other take n + n = 2n steps. Drop the 2: still O(n).

  • In Java, the sum is a long. An int stops at 2,147,483,647, and three values of 10⁹ already pass it.
  • In Python, the sum is named s, so it does not hide the built-in sum() function.

Nested: O(n²)

long countPairs(int[] nums) {
    long pairs = 0;
    for (int i = 0; i < nums.length; i++) {
        for (int j = i + 1; j < nums.length; j++) pairs++;
    }
    return pairs;
}

This is the pair check from the last lesson. Count it out loud:

  • When i is 0, j runs n − 1 times.
  • When i is 1, j runs n − 2 times.
  • And so on, down to 0 times for the last item.
× is a pair the loops count (j after i). · is a cell they skip. For n = 5, that is 10 of the 25 cells.

The pairs fill about half of an n × n square: n × (n − 1) / 2 steps. Keep n², drop the half. A loop inside a loop over the same input is O(n²).

In Java, pairs is a long: for n = 100,000 the count is 4,999,950,000, past the int limit.

Halving: O(log n)

int halvings(int n) {
    int left = n;
    int steps = 0;
    while (left > 1) {
        left = left / 2;
        steps++;
    }
    return steps;
}
  • Each pass cuts the work in half, so the loop ends fast.
  • Each language drops the fraction its own way. Java's / on two ints does it. Python needs //, because / gives a float such as 3.5. TypeScript needs Math.floor.
Each row is half the row above. 16 reaches 1 after 4 halvings.

The number of halvings is . halvings(1_000_000) returns 19, so even a million items take about 20 steps.

Binary search works this way. You will meet it in a few weeks.

What fits in one second

A rough rule from coding contests: an runs about 10⁸ (100 million) simple steps per second. Use it to check an idea before you code it.

Big ONameSteps for n = 1,000Largest n in about 1 second
O(1)constant1any
O(log n)logarithmic10any
O(n)linear1,000about 100,000,000
O(n log n)n log n10,000about 5,000,000
O(n²)quadratic1,000,000about 10,000
O(n³)cubic1,000,000,000about 500
O(2ⁿ)exponentialfar too manyabout 25

A time limit of 2 seconds allows about 2 × 10⁸ steps. Double the time, double the steps.

Put the largest n into your Big O before you code. When n can be 100,000, an O(n²) answer takes 10¹⁰ steps. That is 100 times the budget, so you need O(n log n) or better:

n = 10⁵about 10⁸ steps fit in 1 second
  • O(1)fits
  • O(log n)fits
  • O(n)fits
  • O(n log n)fits
  • O(n²)too slow
  • O(n³)too slow
  • O(2ⁿ)too slow
  • O(n!)too slow

The line marks 10⁸ steps. Aim for O(n log n) or faster.

A bar that stops before the line runs in about a second.

Space: the memory you add

Your code uses two kinds of memory:

  • Input space: the memory the input takes. You were given it.
  • Extra space: the memory your code adds, such as variables, a copy or a Set. It is also called auxiliary space.

in an interview usually means the extra space, in the same Big O terms. Most interviewers do not count the output either.

Answer from the last lessonExtra spaceWhy
Compare every pairO(1)two counters
Sort, check neighboursO(n)the sorted copy
Remember in a SetO(n)the set can hold every item

Faster code often pays with memory. The next problem shows that trade.

One problem, two ways

Think of a sales dashboard. You drag across a range of days, and it shows the total for that range.

Problem: Range Sum Queryn ≤ 10⁵
it asks
For each query [l, r], return the sum of nums[l] to nums[r]. The array never changes between queries.
example
in nums = [2, 4, 1, 3, 5], queries = [[1, 3], [0, 4]] out [8, 15]

4 + 1 + 3 = 8. All five items add up to 15.

limits
  • 1 ≤ n ≤ 10⁵
  • 1 ≤ q ≤ 10⁵
  • 0 ≤ l ≤ r < n
budget
About 10⁸ steps run in one second. With n up to 10⁵, aim for O(n log n) or faster.
Which pattern? Decide first, then open.

“many range sums”do the adding once, before the queries

“the array never changes”work done once stays correct for every query

pattern: Prefix sums

Here q is the number of queries. Both n and q can reach 100,000.

Brute force: add up each range

For each query, walk from l to r and add.

long[] rangeSums(int[] nums, int[][] queries) {
    long[] answers = new long[queries.length];
    for (int k = 0; k < queries.length; k++) {
        int l = queries[k][0], r = queries[k][1];
        long sum = 0;
        for (int i = l; i <= r; i++) sum += nums[i];
        answers[k] = sum;
    }
    return answers;
}

Count the cost:

  • The outer loop runs q times.
  • The inner loop reads up to n items each time.
  • So time is O(n × q), and extra space is O(1).

At the limits, that is 100,000 × 100,000 = 10 billion steps. Far too slow.

Best: build running totals once

The waste: every query adds the same items again. So add them once.

Build a array. prefix[i] holds the sum of the first i items, so prefix[0] is 0.

The sum from l to r is everything up to r, minus everything before l:

  • For [1, 3]: the first 4 items add up to 10. The first item alone is 2.
  • So the answer is 10 − 2 = 8, in one subtraction.
build the prefix, then answeroperations 0
2
0
4
1
1
2
3
3
5
4
prefix{0}

1/8Start the prefix with 0: the sum of no items.

Five additions build the prefix. After that, every query costs one subtraction.
long[] rangeSums(int[] nums, int[][] queries) {
    long[] prefix = new long[nums.length + 1];
    for (int i = 0; i < nums.length; i++) {
        prefix[i + 1] = prefix[i] + nums[i];
    }
    long[] answers = new long[queries.length];
    for (int k = 0; k < queries.length; k++) {
        int l = queries[k][0], r = queries[k][1];
        answers[k] = prefix[r + 1] - prefix[l];
    }
    return answers;
}
  • The first loop adds the next item to the last running total.
  • The last part answers each query with prefix[r + 1] - prefix[l]: a loop in Java, a list comprehension in Python, map in TypeScript.
  • In Java, prefix holds long values. 10⁵ items of 10⁹ add up to 10¹⁴, far past the int limit.
  • Building the prefix is O(n). The q queries are O(q). So time is O(n + q), and the prefix costs O(n) extra space.

Side by side

ApproachThe ideaTimeExtra spacen = q = 100,000
Add up each rangere-add every queryO(n × q)O(1)about 10 billion steps
Prefix sumsadd once, subtract per queryO(n + q)O(n)about 200,000 steps

You trade n extra numbers in memory for a speed-up of 50,000 times. That is almost always a good deal.

take U forwardYouTube
Time and Space Complexity: Striver's A2Z DSA Course
Remember4 points
  1. 01

    Big O says how the steps grow with n, in the worst case. Count the steps, keep the biggest term, drop the constant.

  2. 02

    Read the loops: one loop is O(n), a loop inside a loop is O(n²), halving each time is O(log n).

  3. 03

    Check the limits first: about 10⁸ simple steps fit in one second, so n = 100,000 rules out O(n²).

  4. 04

    Space complexity counts the memory you add. Trading O(n) memory for time, as prefix sums do, is a common win.

Read more

  • Big-O cheat sheet, the common classes and data structure costs on one page
  • Reconciliation in the React docs, the O(n³) problem and React's O(n) shortcut
  • Prefix sum on Wikipedia, with more uses of running totals
  • Big O notation on Wikipedia, the formal definition for when you want the maths

Next: how to read a problem statement, and the questions to ask before you write any code.

Practice

Solve these on the sheet after the lesson. Try each one for 20 minutes before you open its solution.

Striver's A2Z sheet0/1 solved
Open the sheet