Go Guide

Core patterns · Lesson 13 of 62 · about 45 min

Big-O: measuring algorithms

How to estimate how fast code is before running it, read problem constraints, and choose the right target complexity.

On this page · 9 sections
  1. 01Counting steps
  2. 02Why we drop constants
  3. 03The common classes
  4. 04Reading constraints
  5. 05Space complexity
  6. 06Worked example: one problem, three solutions
  7. 07Summary
  8. 08Check your understanding
  9. 09Practice in Go

Counting steps#

Count how many basic steps (a comparison, an addition, an index) your code takes for input size n:

Go
total := 0
for _, x := range nums { // runs n times
	total += x            // 1 step each
}

That is about n steps: O(n), "linear". Double the input, double the time.

Go
for i := range nums {         // n times
	for j := range nums {     // n times for each i
		if nums[i]+nums[j] == 0 { ... }
	}
}

That is n × n steps: O(n²), "quadratic". Double the input, four times the time.

See it: count the steps yourself

Instead of trusting the table, make the computer count. Each function below does nothing but add 1 to a counter for every basic step its loop shape performs:

steps.go
Go
package main
 
import "fmt"
 
func linear(n int) int {
	steps := 0
	for i := 0; i < n; i++ {
		steps++
	}
	return steps
}
 
func quadratic(n int) int {
	steps := 0
	for i := 0; i < n; i++ {
		for j := 0; j < n; j++ {
			steps++
		}
	}
	return steps
}
 
func logarithmic(n int) int {
	steps := 0
	for n > 1 {
		n /= 2 // halve the problem
		steps++
	}
	return steps
}
 
func main() {
	fmt.Printf("%7s %7s %10s %9s\n", "n", "O(n)", "O(n²)", "O(log n)")
	for _, n := range []int{10, 100, 1000, 10000} {
		fmt.Printf("%7d %7d %10d %9d\n", n, linear(n), quadratic(n), logarithmic(n))
	}
}
Output
      n    O(n)      O(n²)  O(log n)
     10      10        100         3
    100     100      10000         6
   1000    1000    1000000         9
  10000   10000  100000000        13

Read each column top to bottom: every time n grows 10×, the linear count grows 10×, the quadratic count grows 100×, and the logarithmic count grows by only about 3 (because 2³ ≈ 10).

Why we drop constants#

Big-O keeps only the part that dominates when n is large:

Exact countBig-OWhy
3n + 50O(n)for large n, the 50 is irrelevant and 3 is a constant factor
n²/2 + nO(n²)n² dwarfs n
5O(1)does not grow at all
n log n + nO(n log n)

Constants still matter in practice (that is what benchmarks are for), but they never change which algorithm wins at large sizes.

The common classes#

Big-ONameTypical coden = 1,000,000
O(1)constantindex a slice, map lookup1
O(log n)logarithmicbinary search, halving20
O(n)linearone loop1,000,000
O(n log n)linearithmicsorting20,000,000
O(n²)quadratictwo nested loops10¹² (too slow)
O(2ⁿ)exponentialall subsetsimpossible
O(n!)factorialall orderingsimpossible

Where log n comes from

Anything that halves the problem each step is O(log n). Starting at 1,000,000 and halving, you reach 1 after about 20 steps, because 2²⁰ ≈ 1,000,000. Binary search, balanced trees and heaps all get their speed from this.

Reading constraints#

A typical judge runs about 10⁸ simple operations per second. So the input limit tells you the complexity you need:

n up toTargetApproaches that fit
10O(n!)try every permutation
20O(2ⁿ)try every subset, bitmasks
500O(n³)triple loops, Floyd–Warshall
5,000O(n²)all pairs, 2D dynamic programming
200,000–1,000,000O(n log n)sorting, heaps, binary search
100,000,000O(n)one pass, hashing, two pointers

Make this your first step on every problem: read the constraint, pick the target, then look for an idea that fits it.

Space complexity#

Memory counts too. A solution that copies the input into a map uses O(n) extra space; one that only keeps a few variables uses O(1). Recursion uses stack space proportional to its depth.

Worked example: one problem, three solutions#

"Does this slice contain a duplicate?"

SolutionTimeExtra spaceIdea
compare every pairO(n²)O(1)brute force
sort, then compare neighborsO(n log n)O(1)*duplicates end up adjacent
remember what you've seen in a setO(n)O(n)trade memory for time

*Sorting changes the input, which may not be allowed.

bigo.go
Go
package main
 
import (
	"fmt"
	"math/rand"
	"slices"
	"time"
)
 
// O(n²) time, O(1) space
func hasDupPairs(nums []int) bool {
	for i := range nums {
		for j := i + 1; j < len(nums); j++ {
			if nums[i] == nums[j] {
				return true
			}
		}
	}
	return false
}
 
// O(n log n) time; sorts a copy so the caller's slice is untouched
func hasDupSort(nums []int) bool {
	s := slices.Clone(nums)
	slices.Sort(s)
	for i := 1; i < len(s); i++ {
		if s[i] == s[i-1] {
			return true
		}
	}
	return false
}
 
// O(n) time, O(n) space
func hasDupSet(nums []int) bool {
	seen := make(map[int]struct{}, len(nums))
	for _, n := range nums {
		if _, ok := seen[n]; ok {
			return true
		}
		seen[n] = struct{}{}
	}
	return false
}
 
func main() {
	fmt.Println(hasDupPairs([]int{1, 2, 3, 1}), hasDupSort([]int{1, 2, 3}), hasDupSet([]int{4, 4}))
 
	nums := rand.Perm(20_000) // no duplicates: the worst case for all three
	timeIt := func(fn func([]int) bool) time.Duration {
		start := time.Now()
		fn(nums)
		return time.Since(start)
	}
	pairs, sorted, set := timeIt(hasDupPairs), timeIt(hasDupSort), timeIt(hasDupSet)
	fmt.Println("pairs at least 10x slower than sort:", pairs > 10*sorted)
	fmt.Println("pairs at least 10x slower than set: ", pairs > 10*set)
}
Output
true false true
pairs at least 10x slower than sort: true
pairs at least 10x slower than set:  true

At 20,000 elements the pairwise version does about 200 million comparisons, while sorting does about 300,000 and the set does 20,000 lookups. Print the actual durations with fmt.Println(pairs, sorted, set) and try 40,000 elements: the pairs time roughly quadruples while the others roughly double. (The program prints comparisons instead of times because timings differ on every machine.)

Spot the hidden loop

Here is the mistake from the callout above, measured. Both functions find the values two slices have in common. The first looks like one loop, but the inner for is exactly what slices.Contains(b, x) does for you. Each function counts its work:

hidden.go
Go
package main
 
import "fmt"
 
// Looks like one loop over a, but scans b for every x: O(len(a) × len(b)).
func commonScan(a, b []int) (out []int, work int) {
	for _, x := range a {
		for _, y := range b { // what slices.Contains(b, x) does inside
			work++
			if x == y {
				out = append(out, x)
				break
			}
		}
	}
	return out, work
}
 
// Pays O(len(b)) once to build a set, then O(1) per lookup: O(len(a) + len(b)).
func commonSet(a, b []int) (out []int, work int) {
	set := make(map[int]bool, len(b))
	for _, y := range b {
		set[y] = true
		work++
	}
	for _, x := range a {
		work++
		if set[x] {
			out = append(out, x)
		}
	}
	return out, work
}
 
func main() {
	a := make([]int, 1000) // 0..999
	b := make([]int, 1000) // 500..1499
	for i := range a {
		a[i], b[i] = i, i+500
	}
	out1, work1 := commonScan(a, b)
	out2, work2 := commonSet(a, b)
	fmt.Println("scan:", len(out1), "common values,", work1, "steps")
	fmt.Println("set: ", len(out2), "common values,", work2, "steps")
}
Output
scan: 500 common values, 625250 steps
set:  500 common values, 2000 steps

Same answer, over 300 times less work. Every value of a below 500 makes the scanning version walk all 1,000 elements of b without finding a match.

Summary#

  • Big-O describes how work grows with input size, ignoring constants.
  • Nested loops multiply; halving gives log n; sorting costs n log n.
  • Read the constraints first and pick a target complexity before choosing an approach.
  • Count extra memory too, including recursion depth.
  • Prove performance claims with benchmarks.

Check your understanding

Answer before scrolling back up. If you miss one, reread the matching section, then try again.

  1. Q1.What is the time complexity of two nested loops that each run n times?

  2. Q2.Constraints say n ≤ 100,000. Which complexity is safe for about one second?

  3. Q3.A loop halves n each time until it reaches 1. Its complexity is:

  4. Q4.Why is O(3n + 50) written as O(n)?

  5. Q5.What extra space does a hash-set duplicate check use?

Practice in Go

Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.

  • Solve it three ways (nested loops, sort, map) and benchmark all three.

    Show hint

    Sub-benchmarks with b.Run at n = 100, 10,000 and 100,000 show the crossover clearly.

  • Show hint

    O(n) time and O(1) space: the expected sum 0+1+…+n minus the actual sum.

  • Show hint

    Checking each number by trial division is too slow. The Sieve of Eratosthenes is O(n log log n).

  • Complexity journalbuild

    For every solution you write from now on, add a comment with its time and space complexity and one sentence explaining why.

Finished this lesson?

Mark it done to track your progress. It is saved in this browser only.