Efficiency and Big-O

How to predict what an algorithm will cost before running it: counting loops, finding the dominant term, and the seven efficiency classes.

Published

algorithms · big-o · data structures

This post is a study file for the efficiency topic of BLM212 Data Structures. It was compiled and edited with AI from the course notes; the point is to have the whole topic revisable in one place. The figures and tables are meant to be played with — the gap between the classes is clearer when you move the numbers yourself than when you read about it.

Why measure an algorithm at all?

Time complexity states how running time depends on the size of the input. It is not a number but a function: it takes the input size n and returns the number of steps T(n). Measuring it buys four practical things:

  • Prediction — How long will this run? What is the largest input I can process in a reasonable time?
  • Comparison — Given two algorithms that do the same job, which one is more efficient?
  • Focus — Which part of the code runs most often? That is where optimisation effort belongs.
  • Selection — Which algorithm fits this application?

How do we define "time"?

There are three reasonable options: elapsed seconds, the number of source lines executed, or how many times one particular operation (an addition, say) is performed. Which one we pick does not matter — they are all related by constant factors, and Big-O discards constant factors anyway.

Experimental measurement versus theoretical analysis

In an experimental study the algorithm is actually coded, run against inputs of different sizes, and timed. That is realistic, but it requires writing the algorithm, it only tells you about the inputs you tried, and hardware and compiler change the result.

Theoretical analysis works from the pseudocode instead. Running time is written as a function of n and all possible inputs are accounted for, so a decision can be made without writing code or buying a machine. This is why discussions of efficiency run on theoretical analysis.

Textbooks use efficiency and complexity more or less interchangeably; both mean running time. When the distinction matters: time complexity is the number of steps taken, space complexity is the memory required — both measured against input size.

What happens as n grows?

Every algorithm is fast for small n. The difference shows up as n grows — and it is not "somewhat slower", it is the gap between a microsecond and the age of the universe.

Hover the curves, or focus the figure and change n with the arrow keys.

  • log₂ n
  • n
  • n log₂ n
  • 2ⁿ
Colour runs light to dark as cost rises. The curves are clipped at 100 steps: n³ hits the ceiling at n = 8 and 2ⁿ at n = 7. Hover the curves to read every class's exact step count at that n.

The same data as numbers:

Steps taken as the input size grows
T(n)n = 101001,00010,000magnitude
5 (constant)5555atom
log n36913amoeba
√n31031100bird
n101001,00010,000human
n log n306009,000130,000house
10010,00010⁶10⁸elephant
1,00010⁶10⁹10¹²dinosaur
2ⁿ1,02410³⁰10³⁰⁰10³⁰⁰⁰universe
The analogy on the right is from the course notes, as a way to hold on to the order of magnitude.

Two extremes tell the whole story. For n = 60, 2ⁿ ≈ 10¹⁸ steps — at one step per second that takes as long as the universe has existed. From the other side: even if the input were the size of the entire universe (n = 2⁶⁰), log n would be just 60. A logarithm barely grows; an exponential explodes almost immediately.

One more detail: on a log-log plot every polynomial is a straight line, , n⁴ all look the same apart from their slope. An exponential is still exponential even on a log-log plot. This is why the n^c family counts as feasible and the c^n family does not.

Try it yourself

Change n and watch how many steps each class needs, and how long that takes at one nanosecond per step. The point is how the gap widens with n.

8

1 step = 1 nanosecond

What each efficiency class costs at input size n
efficiencystepstime
log₂ n33 ns
n88 ns
n log₂ n2424 ns
6464 ns
512512 ns
2ⁿ256256 ns
n!40,32040.3 µs

The bars are on a log scale: length is proportional to the digit count of the step count and saturates at 10²⁰ steps. Drawn linearly, every row except 2ⁿ and n! would be invisible — that is how large the difference is.

Counting loops

An algorithm with no loops and no recursion executes a fixed number of instructions; the rest is the speed of the machine, which is rarely the deciding factor. Studying efficiency therefore means studying loops. Every recursion can be turned into a loop anyway.

Single loops

// linear — f(n) = n, the body runs 1000 times
for (i = 0; i < 1000; i++)
    the loop body

// linear with a step of 2 — f(n) = n/2, the body runs 500 times
for (i = 0; i < 1000; i = i + 2)
    the loop body

// logarithmic, multiplying — f(n) = log n, 10 iterations
for (i = 1; i <= 1000; i *= 2)
    the loop body

// logarithmic, dividing — f(n) = log n, 10 iterations
for (i = 1000; i >= 1; i /= 2)
    the loop body

The distinction sits in one place: if the control variable increases or decreases by a fixed amount the loop is linear; if it is multiplied or divided it is logarithmic. Stepping by 2 halves the iteration count but the graph is still a straight line — the constant factor is dropped and the answer is O(n) again. In a logarithmic loop 1000 elements take about 10 iterations and 1,000,000 elements about 20.

Nested loops

The rule is one sentence: total iterations = outer iterations × inner iterations. The rest is reading which type each loop is.

// linear logarithmic — f(n) = n log n
for (i = 1; i <= 10; i++)
  for (j = 1; j <= 10; j *= 2)
      the loop body

// quadratic — f(n) = n², inner loop independent of the outer one
for (i = 1; i <= 10; i++)
  for (j = 1; j <= 10; j++)
      the loop body

// dependent quadratic — f(n) = n(n+1)/2
for (i = 0; i < 10; i++)
  for (j = 0; j < i; j++)     // inner loop depends on the outer one
      the loop body

In the dependent quadratic loop the inner loop runs 0, 1, 2, … 9 times: 45 iterations in total, averaging 45/10 = 4.5 ≈ (n+1)/2. Multiplied by the outer count that gives n(n+1)/2.

full quadratic: 10 × 10 = 100 iterationsdependent: 45 iterationsj: 0 timesj: 4 timesj: 9 timesn² iterationsn(n+1)/2 iterations — about half
A dependent quadratic loop scans roughly half the full square. Half of it and all of it are both O(n²) — Big-O asks about the shape, not the ratio.

Deriving Big-O

f(n) is the number of instructions executed for an input of n elements. However complicated the expression, the dominant term in it determines the order of magnitude of the result. That term is the Big-O: O(n)on the order of.

  1. Set every coefficient to 1. ½n² + ½n → n² + n. Constant factors depend on the machine and the compiler; they are not what decides the outcome as n grows.
  2. Drop everything but the largest term. n² + n → n².
½n² + ½nn² + nO(n²)coefficientsmaller termTerm ordering — the dominant one wins as you move rightlog nnn log nn³ … nᵏ2ⁿn!efficientunusable
Two rules, one example: f(n) = ½n² + ½n → O(n²). And another: 3n³ + 100n² + 5000 → n³ + n² + 1 → O(n³) — however large the coefficient 100 is, it cannot beat n³.

Big-O is an order of magnitude, not a duration. An O(n²) algorithm can be faster than an O(n log n) one for small n, because the discarded constants still dominate there. Every efficiency measure assumes a large enough sample.

The seven efficiency categories

The textbook's standard measure is given for n = 10,000.

The seven efficiency categories at n = 10,000
efficiencybig-Oiterationsestimated time
LogarithmicO(log n)14microseconds
LinearO(n)10⁴seconds
Linear logarithmicO(n log n)1.4 × 10⁵seconds
QuadraticO(n²)10⁸minutes
PolynomialO(nᵏ)10⁴ᵏhours
ExponentialO(cⁿ)2¹⁰⁰⁰⁰intractable
FactorialO(n!)10000!intractable
The table runs from most to least efficient; iteration counts are for n = 10,000.

In practice the boundary sits between polynomial and exponential: the n^c family counts as feasible, while c^n and n! become unusable the moment the input grows a little. A faster computer does not close that gap — when Moore's law doubles the speed, an exponential algorithm can handle exactly one more element.

Two classic examples

The method never changes: count the loops, multiply the nested ones, take the dominant term.

Adding two matrices — O(n²)

Algorithm addMatrix (matrix1, matrix2, size, matrix3)
1  r = 1
2  loop (r <= size)                      ← outer loop: size times
   1  c = 1
   2  loop (c <= size)                   ← inner loop: size times
      1  matrix3[r,c] = matrix1[r,c] + matrix2[r,c]
      2  c = c + 1
   3  r = r + 1
3  return
end addMatrix

Each cell of the result needs one addition, and there are size × size cells. A quadratic loop, so O(size²) → O(n²).

Multiplying two matrices — O(n³)

Algorithm multiMatrix (matrix1, matrix2, size, matrix3)
1  loop (not end of row)                 ← size times
   1  loop (not end of column)           ← size times
      1  loop (size of row times)        ← size times
         1  calculate sum of
              (all row cells) * (all column cells)
         2  store sum in matrix3
      2  end loop
   2  end loop
2  end loop
3  return
end multiMatrix

The difference is a single line: each cell of the result is no longer one addition but the sum of the products of a row and a column. That means a third loop — a cubic loop, so O(size³) → O(n³).

Subprogram calls count too: if the loop body calls a subprogram of known complexity, its cost is multiplied by the iteration count. Calling an O(n²) function inside a loop that runs n times gives O(n³).

Exercises

The five questions from the course notes. Solve each one first, then open the answer.

1Order these efficiencies from smallest to largest: a) n log(n) b) n + n² + n³ c) 2⁴ d) n^0.5

The order is 2⁴ < n^0.5 < n log(n) < n + n² + n³.

2⁴ = 16, which is constant — it does not depend on n at all. n^0.5 = √n grows more slowly than n. The dominant term of the last expression is .

2aIf algorithm XX has complexity n², what is the complexity of this fragment?
i = 1
loop (i <= n)          ← n times
   j = 1
   loop (j < n)        ← n−1 times
      XX ( .... )      ← n² per call
      j = j + 1
   i = i + 1

f(n) = n · (n−1) · n² = n⁴ − n³, so O(n⁴).

The outer loop, the inner loop and the call in the body are multiplied; then the dominant term is taken.

2bIf doIt has an efficiency factor of 5n, what is the efficiency of this fragment?
for (i = 1; i <= n; i++)
    doIt ( ... )

f(n) = n · 5n = 5n², so O(n²). The coefficient 5 is dropped by the first rule.

3If doIt has efficiency n², what is the efficiency of this fragment?
for (i = 1; i < n; i *= 2)
    doIt ( ... )

f(n) = log₂n · n², so O(n² log n).

The trap is i *= 2: the loop runs log₂n times, logarithmically, not n times.

4In an algorithm with efficiency n³, one step takes 1 ns. How long does an input of 1000 elements take?

1000³ × 1 ns = 10⁹ × 10⁻⁹ = 1 second.

The same algorithm would take 10¹² × 10⁻⁹ = 1000 seconds, about 17 minutes, for n = 10,000. The input grew 10-fold and the time grew 1000-fold.

5n = 4096 measures 512 ms and n = 16,384 measures 1024 ms. What is the efficiency of this algorithm?

The input grew 4-fold while the time only doubled, so the time is proportional to √n.

Check: √4096 = 64 and 512/64 = 8; √16384 = 128 and 1024/128 = 8. The coefficient is the same in both, so f(n) = 8√n and the answer is O(n^0.5).

The general trick is in that question: "the input grew k-fold, by how much did the time grow?" The answer gives the class away — doubling means linear, quadrupling means quadratic, staying the same means logarithmic.