Big-O & Complexity

Measure how an algorithm's time and memory grow with its input, so you can compare approaches before you write them.

Beginner

How It Works

Big-O describes how the work an algorithm does grows as its input grows. Count the steps as a function of the input size n, keep only the term that dominates for large n, and drop its constant factor. Code that does 3n + 5 steps is O(n): doubling the input roughly doubles the work, and that's the part that matters at scale.

You care because it lets you compare approaches before you write them. An O(n²) solution and an O(n) one both pass a ten-element example, but at a million elements one does about a trillion steps and the other about a million.

            n = 10   n = 1,000   n = 1,000,000
O(1)             1           1               1
O(log n)         3          10              20
O(n)            10       1,000       1,000,000
O(n log n)      33      10,000      20,000,000
O(n²)          100   1,000,000           10^12
O(2^n)       1,024      10^301        too many
Rough step counts, with logs in base 2. The gap between rows widens as n grows.

To find the Big-O of a piece of code, look at its loops and calls:

  • Steps in sequence add. A loop over n items followed by a loop over m items is O(n + m). Two loops in a row over the same array are O(2n), which is just O(n).
  • Nested loops multiply. A loop over n inside another loop over n is O(n²). Library calls count too: x in some_list, indexOf and slice each walk through up to n elements, so they're loops you can't see.
  • Halving gives a log. If each step cuts the remaining work in half, as binary search does, it takes about log n steps to get down to one item.
  • Recursion is calls times work per call. Add up the work over the whole tree of calls. See Recursion.

Counting the Work, Step by Step

Step 1 of 21

items, n = 8

steps

pairs (i, j)

  • Counting now
  • Range the loop covers
  • Counted
  • Halved away

Count how often each loop's innermost line runs when n = 8. Straight-line code runs once however big n gets, so constant = 1.

All 21 steps as text
  1. Count how often each loop's innermost line runs when n = 8. Straight-line code runs once however big n gets, so constant = 1.
  2. The halving loop starts with all 8 items in range. It counts one step, then halves size, the way binary search drops half of what's left.
  3. size = 4 is step 2. Each step throws away half the range, so the count barely grows.
  4. size = 2 is step 3. Halving again leaves 1, which ends the loop: 3 steps for 8 items, because 2^3 = 8. That's log n.
  5. The single loop visits each item once. i = 0 is step 1, and the count goes in the cell.
  6. i = 1 is step 2. One step per item, 6 items to go.
  7. i = 2 is step 3. One step per item, 5 items to go.
  8. i = 3 is step 4. One step per item, 4 items to go.
  9. i = 4 is step 5. One step per item, 3 items to go.
  10. i = 5 is step 6. One step per item, 2 items to go.
  11. i = 6 is step 7. One step per item, 1 item to go.
  12. i = 7 is step 8. One pass over 8 items took 8 steps, so doubling the items doubles the work. That's O(n).
  13. The nested loop runs j over all 8 items for every i. Row i=0 is those 8 pairs, so one turn of the outer loop already costs 8 steps.
  14. i = 1 runs j across all 8 items again: 8 more steps, quadratic = 16 after 2 rows.
  15. i = 2 runs j across all 8 items again: 8 more steps, quadratic = 24 after 3 rows.
  16. i = 3 runs j across all 8 items again: 8 more steps, quadratic = 32 after 4 rows.
  17. i = 4 runs j across all 8 items again: 8 more steps, quadratic = 40 after 5 rows.
  18. i = 5 runs j across all 8 items again: 8 more steps, quadratic = 48 after 6 rows.
  19. i = 6 runs j across all 8 items again: 8 more steps, quadratic = 56 after 7 rows.
  20. i = 7 fills the last row. 8 rows of 8 is 64 steps, because each item meets every item. That's O(n²).
  21. Same 8 items: 3 steps halving, 8 in one pass, 64 for every pair. At n = 16 they become 4, 16 and 256, so the gap keeps widening.

Space complexity counts the extra memory you allocate the same way: hash sets, copies of the input, output buffers and the recursion stack. The input itself usually isn't counted. Many solutions trade one for the other. A hash set spends O(n) memory to turn an O(n²) search into an O(n) one.

When to Reach for It

  • The constraints allow n up to 100,000 or a million. Aim for O(n log n) or O(n), because O(n²) would be 10 billion steps or more.
  • n is at most a few thousand. O(n²) usually fits comfortably, so don't over-engineer.
  • n is 20 or less. That's a hint that an exponential O(2^n) search over every subset is expected.
  • A single input value can be a billion or more. You can't loop up to it, so look for O(log n) or O(1), such as binary search or a formula.
  • The question asks for O(1) extra space or a better time than the obvious answer. That rules out the brute force and tells you the target.

Complexity

OperationTimeSpace
Index into an arrayO(1)O(1)
Hash map insert or lookupO(1) averageO(n) for n keys
Binary search a sorted arrayO(log n)O(1)
Sort with a library sortO(n log n)O(log n) to O(n), depending on the sort
Compare every pairO(n²)O(1)

The Template

Counting the Work

countSteps runs five common loop shapes and counts how many times each innermost statement executes. The demo prints n and then the five counts, in the order O(1), O(log n), O(n), O(n log n), O(n²).

Compare the first two lines. Going from 8 to 16 doubles n. The O(1) count doesn't move, O(log n) gains one step, O(n) doubles, O(n log n) grows a bit faster than that, and O(n²) quadruples. By 1,024 the quadratic loop has run over a million times while the halving loop has run 10 times.

Time O(n²), from the quadratic loopSpace O(1)

def count_steps(n: int) -> list[int]:
    """How many times the innermost statement runs for an input of size n, for
    five common loop shapes: O(1), O(log n), O(n), O(n log n) and O(n²)."""
    # O(1): straight-line code. One step, however big n is.
    constant = 1

    # O(log n): halve what's left each time, like binary search.
    logarithmic = 0
    size = n
    while size > 1:
        logarithmic += 1
        size //= 2

    # O(n): visit each element once.
    linear = 0
    for _ in range(n):
        linear += 1

    # O(n log n): a halving loop for each of the n elements.
    linearithmic = 0
    for _ in range(n):
        size = n
        while size > 1:
            linearithmic += 1
            size //= 2

    # O(n²): pair every element with every element.
    quadratic = 0
    for _ in range(n):
        for _ in range(n):
            quadratic += 1

    return [constant, logarithmic, linear, linearithmic, quadratic]


if __name__ == "__main__":
    # Each line is n, then the five counts in the order above.
    print(8, count_steps(8))  # 8 [1, 3, 8, 24, 64]
    print(16, count_steps(16))  # 16 [1, 4, 16, 64, 256]
    print(1024, count_steps(1024))  # 1024 [1, 10, 1024, 10240, 1048576]
// How many times the innermost statement runs for an input of size n, for
// five common loop shapes: O(1), O(log n), O(n), O(n log n) and O(n²).
export function countSteps(n: number): number[] {
  // O(1): straight-line code. One step, however big n is.
  const constant = 1;

  // O(log n): halve what's left each time, like binary search.
  let logarithmic = 0;
  for (let size = n; size > 1; size = Math.floor(size / 2)) logarithmic++;

  // O(n): visit each element once.
  let linear = 0;
  for (let i = 0; i < n; i++) linear++;

  // O(n log n): a halving loop for each of the n elements.
  let linearithmic = 0;
  for (let i = 0; i < n; i++) {
    for (let size = n; size > 1; size = Math.floor(size / 2)) linearithmic++;
  }

  // O(n²): pair every element with every element.
  let quadratic = 0;
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) quadratic++;
  }

  return [constant, logarithmic, linear, linearithmic, quadratic];
}

// Each line is n, then the five counts in the order above.
console.log(8, JSON.stringify(countSteps(8))); // 8 [1,3,8,24,64]
console.log(16, JSON.stringify(countSteps(16))); // 16 [1,4,16,64,256]
console.log(1024, JSON.stringify(countSteps(1024))); // 1024 [1,10,1024,10240,1048576]
package main

import "fmt"

// countSteps returns how many times the innermost statement runs for an input
// of size n, for five common loop shapes: O(1), O(log n), O(n), O(n log n)
// and O(n²).
func countSteps(n int) []int {
	// O(1): straight-line code. One step, however big n is.
	constant := 1

	// O(log n): halve what's left each time, like binary search.
	logarithmic := 0
	for size := n; size > 1; size /= 2 {
		logarithmic++
	}

	// O(n): visit each element once.
	linear := 0
	for i := 0; i < n; i++ {
		linear++
	}

	// O(n log n): a halving loop for each of the n elements.
	linearithmic := 0
	for i := 0; i < n; i++ {
		for size := n; size > 1; size /= 2 {
			linearithmic++
		}
	}

	// O(n²): pair every element with every element.
	quadratic := 0
	for i := 0; i < n; i++ {
		for j := 0; j < n; j++ {
			quadratic++
		}
	}

	return []int{constant, logarithmic, linear, linearithmic, quadratic}
}

func main() {
	// Each line is n, then the five counts in the order above.
	fmt.Println(8, countSteps(8))       // 8 [1 3 8 24 64]
	fmt.Println(16, countSteps(16))     // 16 [1 4 16 64 256]
	fmt.Println(1024, countSteps(1024)) // 1024 [1 10 1024 10240 1048576]
}
using System;

public class Solution
{
    // How many times the innermost statement runs for an input of size n, for
    // five common loop shapes: O(1), O(log n), O(n), O(n log n) and O(n²).
    public static int[] CountSteps(int n)
    {
        // O(1): straight-line code. One step, however big n is.
        int constant = 1;

        // O(log n): halve what's left each time, like binary search.
        int logarithmic = 0;
        for (int size = n; size > 1; size /= 2) logarithmic++;

        // O(n): visit each element once.
        int linear = 0;
        for (int i = 0; i < n; i++) linear++;

        // O(n log n): a halving loop for each of the n elements.
        int linearithmic = 0;
        for (int i = 0; i < n; i++)
        {
            for (int size = n; size > 1; size /= 2) linearithmic++;
        }

        // O(n²): pair every element with every element.
        int quadratic = 0;
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++) quadratic++;
        }

        return new[] { constant, logarithmic, linear, linearithmic, quadratic };
    }

    public static void Main()
    {
        // Each line is n, then the five counts in the order above.
        Console.WriteLine("8 " + Format(CountSteps(8))); // 8 [1, 3, 8, 24, 64]
        Console.WriteLine("16 " + Format(CountSteps(16))); // 16 [1, 4, 16, 64, 256]
        Console.WriteLine("1024 " + Format(CountSteps(1024))); // 1024 [1, 10, 1024, 10240, 1048576]
    }

    static string Format(int[] values) => "[" + string.Join(", ", values) + "]";
}
import java.util.Arrays;

public class Solution {
    // How many times the innermost statement runs for an input of size n, for
    // five common loop shapes: O(1), O(log n), O(n), O(n log n) and O(n²).
    static int[] countSteps(int n) {
        // O(1): straight-line code. One step, however big n is.
        int constant = 1;

        // O(log n): halve what's left each time, like binary search.
        int logarithmic = 0;
        for (int size = n; size > 1; size /= 2) logarithmic++;

        // O(n): visit each element once.
        int linear = 0;
        for (int i = 0; i < n; i++) linear++;

        // O(n log n): a halving loop for each of the n elements.
        int linearithmic = 0;
        for (int i = 0; i < n; i++) {
            for (int size = n; size > 1; size /= 2) linearithmic++;
        }

        // O(n²): pair every element with every element.
        int quadratic = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) quadratic++;
        }

        return new int[] {constant, logarithmic, linear, linearithmic, quadratic};
    }

    public static void main(String[] args) {
        // Each line is n, then the five counts in the order above.
        System.out.println("8 " + Arrays.toString(countSteps(8))); // 8 [1, 3, 8, 24, 64]
        System.out.println("16 " + Arrays.toString(countSteps(16))); // 16 [1, 4, 16, 64, 256]
        System.out.println("1024 " + Arrays.toString(countSteps(1024))); // 1024 [1, 10, 1024, 10240, 1048576]
    }
}

Worked Problems

Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.

Contains Duplicate

EASYLeetCode 217

Problem

Given an integer array nums, return true if any value appears at least twice, and false if every value is distinct.

Example: [1, 2, 3, 1] returns true because 1 appears twice. [1, 2, 3, 4] returns false.

Show Solution

Approach

The brute force compares every pair of positions i < j. That's up to n(n - 1) / 2 comparisons, so it takes O(n²) time but only O(1) extra space.

The faster version walks the array once and keeps a hash set of the values it has already passed. Asking the set about each new value costs O(1) on average, so the whole pass is O(n). The price is O(n) extra memory for the set. This is the classic trade of space for time, and both versions sit side by side in the code so you can compare them.

Time O(n²) by pairs, O(n) average with a setSpace O(1) by pairs, O(n) with a set

def contains_duplicate_quadratic(nums: list[int]) -> bool:
    """O(n²) time, O(1) space: compare every pair of positions."""
    for i in range(len(nums)):
        # Start j after i so each pair is checked once and never against itself.
        for j in range(i + 1, len(nums)):
            if nums[i] == nums[j]:
                return True
    return False


def contains_duplicate_linear(nums: list[int]) -> bool:
    """O(n) time, O(n) space: remember every value in a hash set."""
    seen: set[int] = set()
    for num in nums:
        if num in seen:  # O(1) on average, instead of a scan.
            return True
        seen.add(num)
    return False


if __name__ == "__main__":
    # Both versions agree on every input: true, false, true, false.
    for nums in [[1, 2, 3, 1], [1, 2, 3, 4], [1, 1, 1, 3, 3, 4, 3, 2, 4, 2], [7]]:
        print(contains_duplicate_quadratic(nums), contains_duplicate_linear(nums))
// O(n²) time, O(1) space: compare every pair of positions.
export function containsDuplicateQuadratic(nums: number[]): boolean {
  for (let i = 0; i < nums.length; i++) {
    // Start j after i so each pair is checked once and never against itself.
    for (let j = i + 1; j < nums.length; j++) {
      if (nums[i] === nums[j]) return true;
    }
  }
  return false;
}

// O(n) time, O(n) space: remember every value in a hash set.
export function containsDuplicateLinear(nums: number[]): boolean {
  const seen = new Set<number>();
  for (const num of nums) {
    if (seen.has(num)) return true; // O(1) on average, instead of a scan.
    seen.add(num);
  }
  return false;
}

// Both versions agree on every input: true, false, true, false.
for (const nums of [[1, 2, 3, 1], [1, 2, 3, 4], [1, 1, 1, 3, 3, 4, 3, 2, 4, 2], [7]]) {
  console.log(containsDuplicateQuadratic(nums), containsDuplicateLinear(nums));
}
package main

import "fmt"

// containsDuplicateQuadratic takes O(n²) time and O(1) space: it compares
// every pair of positions.
func containsDuplicateQuadratic(nums []int) bool {
	for i := 0; i < len(nums); i++ {
		// Start j after i so each pair is checked once and never against itself.
		for j := i + 1; j < len(nums); j++ {
			if nums[i] == nums[j] {
				return true
			}
		}
	}
	return false
}

// containsDuplicateLinear takes O(n) time and O(n) space: it remembers every
// value in a hash set.
func containsDuplicateLinear(nums []int) bool {
	seen := map[int]bool{}
	for _, num := range nums {
		if seen[num] { // O(1) on average, instead of a scan.
			return true
		}
		seen[num] = true
	}
	return false
}

func main() {
	// Both versions agree on every input: true, false, true, false.
	for _, nums := range [][]int{{1, 2, 3, 1}, {1, 2, 3, 4}, {1, 1, 1, 3, 3, 4, 3, 2, 4, 2}, {7}} {
		fmt.Println(containsDuplicateQuadratic(nums), containsDuplicateLinear(nums))
	}
}
using System;
using System.Collections.Generic;

public class Solution
{
    // O(n²) time, O(1) space: compare every pair of positions.
    public static bool ContainsDuplicateQuadratic(int[] nums)
    {
        for (int i = 0; i < nums.Length; i++)
        {
            // Start j after i so each pair is checked once and never against itself.
            for (int j = i + 1; j < nums.Length; j++)
            {
                if (nums[i] == nums[j]) return true;
            }
        }
        return false;
    }

    // O(n) time, O(n) space: remember every value in a hash set.
    public static bool ContainsDuplicateLinear(int[] nums)
    {
        var seen = new HashSet<int>();
        foreach (int num in nums)
        {
            if (seen.Contains(num)) return true; // O(1) on average, instead of a scan.
            seen.Add(num);
        }
        return false;
    }

    public static void Main()
    {
        // Both versions agree on every input: true, false, true, false.
        int[][] inputs = { new[] { 1, 2, 3, 1 }, new[] { 1, 2, 3, 4 }, new[] { 1, 1, 1, 3, 3, 4, 3, 2, 4, 2 }, new[] { 7 } };
        foreach (int[] nums in inputs)
        {
            Console.WriteLine(ContainsDuplicateQuadratic(nums) + " " + ContainsDuplicateLinear(nums));
        }
    }
}
import java.util.HashSet;
import java.util.Set;

public class Solution {
    // O(n²) time, O(1) space: compare every pair of positions.
    static boolean containsDuplicateQuadratic(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            // Start j after i so each pair is checked once and never against itself.
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] == nums[j]) return true;
            }
        }
        return false;
    }

    // O(n) time, O(n) space: remember every value in a hash set.
    static boolean containsDuplicateLinear(int[] nums) {
        Set<Integer> seen = new HashSet<>();
        for (int num : nums) {
            if (seen.contains(num)) return true; // O(1) on average, instead of a scan.
            seen.add(num);
        }
        return false;
    }

    public static void main(String[] args) {
        // Both versions agree on every input: true, false, true, false.
        int[][] inputs = {{1, 2, 3, 1}, {1, 2, 3, 4}, {1, 1, 1, 3, 3, 4, 3, 2, 4, 2}, {7}};
        for (int[] nums : inputs) {
            System.out.println(containsDuplicateQuadratic(nums) + " " + containsDuplicateLinear(nums));
        }
    }
}

Missing Number

EASYLeetCode 268

Problem

The array nums holds n distinct numbers taken from the range 0 to n. That range has n + 1 numbers, so exactly one is missing. Return it.

Example: [3, 0, 1] has n = 3, so the range is 0 to 3, and 2 is the number that isn't there.

Show Solution

Approach

The numbers 0 through n add up to n * (n + 1) / 2. Start from that total and subtract every number in the array. Whatever is left is the missing number. That's one pass with no extra storage: O(n) time and O(1) space.

Compare the obvious alternatives. Searching the array for each candidate is O(n²). Sorting first and looking for the first index where nums[i] != i is O(n log n). A hash set of the values gets O(n) time but costs O(n) memory. The formula beats all three because it uses something you know about the input: which numbers should be there.

Time O(n)Space O(1)

def missing_number(nums: list[int]) -> int:
    """The one number from 0 to n missing from `nums`, which holds n distinct
    numbers from that range."""
    n = len(nums)
    # 0 + 1 + ... + n by formula, so no loop is needed for the full total.
    missing = n * (n + 1) // 2
    # Subtract every number that is present. Only the missing one is left.
    for num in nums:
        missing -= num
    return missing


if __name__ == "__main__":
    print(missing_number([3, 0, 1]))  # 2
    print(missing_number([0, 1]))  # 2
    print(missing_number([9, 6, 4, 2, 3, 5, 7, 0, 1]))  # 8
    print(missing_number([1]))  # 0
// The one number from 0 to n missing from nums, which holds n distinct
// numbers from that range.
export function missingNumber(nums: number[]): number {
  const n = nums.length;
  // 0 + 1 + ... + n by formula, so no loop is needed for the full total.
  let missing = (n * (n + 1)) / 2;
  // Subtract every number that is present. Only the missing one is left.
  for (const num of nums) missing -= num;
  return missing;
}

console.log(missingNumber([3, 0, 1])); // 2
console.log(missingNumber([0, 1])); // 2
console.log(missingNumber([9, 6, 4, 2, 3, 5, 7, 0, 1])); // 8
console.log(missingNumber([1])); // 0
package main

import "fmt"

// missingNumber returns the one number from 0 to n missing from nums, which
// holds n distinct numbers from that range.
func missingNumber(nums []int) int {
	n := len(nums)
	// 0 + 1 + ... + n by formula, so no loop is needed for the full total.
	missing := n * (n + 1) / 2
	// Subtract every number that is present. Only the missing one is left.
	for _, num := range nums {
		missing -= num
	}
	return missing
}

func main() {
	fmt.Println(missingNumber([]int{3, 0, 1}))                   // 2
	fmt.Println(missingNumber([]int{0, 1}))                      // 2
	fmt.Println(missingNumber([]int{9, 6, 4, 2, 3, 5, 7, 0, 1})) // 8
	fmt.Println(missingNumber([]int{1}))                         // 0
}
using System;

public class Solution
{
    // The one number from 0 to n missing from nums, which holds n distinct
    // numbers from that range.
    public static int MissingNumber(int[] nums)
    {
        int n = nums.Length;
        // 0 + 1 + ... + n by formula, so no loop is needed for the full total.
        int missing = n * (n + 1) / 2;
        // Subtract every number that is present. Only the missing one is left.
        foreach (int num in nums) missing -= num;
        return missing;
    }

    public static void Main()
    {
        Console.WriteLine(MissingNumber(new[] { 3, 0, 1 })); // 2
        Console.WriteLine(MissingNumber(new[] { 0, 1 })); // 2
        Console.WriteLine(MissingNumber(new[] { 9, 6, 4, 2, 3, 5, 7, 0, 1 })); // 8
        Console.WriteLine(MissingNumber(new[] { 1 })); // 0
    }
}
public class Solution {
    // The one number from 0 to n missing from nums, which holds n distinct
    // numbers from that range.
    static int missingNumber(int[] nums) {
        int n = nums.length;
        // 0 + 1 + ... + n by formula, so no loop is needed for the full total.
        int missing = n * (n + 1) / 2;
        // Subtract every number that is present. Only the missing one is left.
        for (int num : nums) missing -= num;
        return missing;
    }

    public static void main(String[] args) {
        System.out.println(missingNumber(new int[] {3, 0, 1})); // 2
        System.out.println(missingNumber(new int[] {0, 1})); // 2
        System.out.println(missingNumber(new int[] {9, 6, 4, 2, 3, 5, 7, 0, 1})); // 8
        System.out.println(missingNumber(new int[] {1})); // 0
    }
}

Common Pitfalls

  • Dropping a term that depends on a different input. A loop over n items then a loop over m items is O(n + m), and one nested inside the other is O(n · m). Neither is O(n) unless m is a fixed size.
  • Missing hidden loops. x in some_list, indexOf, contains and slice each touch up to O(n) elements, so calling one inside a loop over the input makes it O(n²). In Java and C#, building a string with += in a loop copies it every time, with the same result.
  • Counting only time. If the question asks for O(1) extra space, a hash set, a sorted copy or a recursion n calls deep all break the rule.
  • Quoting the best case. A Big-O answer means the worst case unless you say otherwise. Hash maps and quicksort are O(1) and O(n log n) on average, and you should say "on average" when you use them.
  • Expecting Big-O to predict speed on tiny inputs. Constant factors win when n is small: insertion sort is O(n²) but beats merge sort on a dozen elements, which is why library sorts switch to it for short runs.

Practice

More problems to try on your own.