Big-O & Complexity
Measure how an algorithm's time and memory grow with its input, so you can compare approaches before you write them.
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
To find the Big-O of a piece of code, look at its loops and calls:
- Steps in sequence add. A loop over
nitems followed by a loop overmitems 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
ninside another loop overnis O(n²). Library calls count too:x in some_list,indexOfandsliceeach walk through up tonelements, 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
- Count how often each loop's innermost line runs when
n = 8. Straight-line code runs once however bigngets, soconstant = 1. - The halving loop starts with all
8items in range. It counts one step, then halvessize, the way binary search drops half of what's left. size = 4is step2. Each step throws away half the range, so the count barely grows.size = 2is step3. Halving again leaves1, which ends the loop:3steps for8items, because2^3 = 8. That's log n.- The single loop visits each item once.
i = 0is step1, and the count goes in the cell. i = 1is step2. One step per item,6items to go.i = 2is step3. One step per item,5items to go.i = 3is step4. One step per item,4items to go.i = 4is step5. One step per item,3items to go.i = 5is step6. One step per item,2items to go.i = 6is step7. One step per item,1item to go.i = 7is step8. One pass over8items took8steps, so doubling the items doubles the work. That's O(n).- The nested loop runs
jover all8items for everyi. Rowi=0is those8pairs, so one turn of the outer loop already costs8steps. i = 1runsjacross all8items again:8more steps,quadratic = 16after2rows.i = 2runsjacross all8items again:8more steps,quadratic = 24after3rows.i = 3runsjacross all8items again:8more steps,quadratic = 32after4rows.i = 4runsjacross all8items again:8more steps,quadratic = 40after5rows.i = 5runsjacross all8items again:8more steps,quadratic = 48after6rows.i = 6runsjacross all8items again:8more steps,quadratic = 56after7rows.i = 7fills the last row.8rows of8is64steps, because each item meets every item. That's O(n²).- Same
8items:3steps halving,8in one pass,64for every pair. Atn = 16they become4,16and256, 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
nup 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. nis at most a few thousand. O(n²) usually fits comfortably, so don't over-engineer.nis 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
| Operation | Time | Space |
|---|---|---|
| Index into an array | O(1) | O(1) |
| Hash map insert or lookup | O(1) average | O(n) for n keys |
| Binary search a sorted array | O(log n) | O(1) |
| Sort with a library sort | O(n log n) | O(log n) to O(n), depending on the sort |
| Compare every pair | O(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.
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 SolutionHide 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));
}
}
}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 SolutionHide 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])); // 0package 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
nitems then a loop overmitems is O(n + m), and one nested inside the other is O(n · m). Neither is O(n) unlessmis a fixed size. - Missing hidden loops.
x in some_list,indexOf,containsandsliceeach 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
ncalls 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
nis 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.