Performance

Big O notation, without the maths

Short answer: Big O describes how the work grows when the input grows. O(n) means the work doubles when the input doubles; O(n²) means it quadruples. It is a shape, not a stopwatch.

Why you cannot just time it

Timing a program tells you about this machine on this run. Big O tells you what happens at 10x the input, on any machine. Compare two ways to add up 1..n:

#include <iostream>
using namespace std;

int main() {
    int n = 10;

    // way 1: a loop — n additions
    int a = 0;
    for (int i = 1; i <= n; i++) a += i;

    // way 2: the closed form — always 1 multiply
    int b = n * (n + 1) / 2;

    cout << a << " " << b << endl;
    return 0;
}

Same answer, wildly different growth. Way 1 is O(n); way 2 is O(1) — constant, no matter how large n gets.

The four shapes you will actually meet

Big ONameIf n goes 10×Typical cause
O(1)constantsamearithmetic, array index
O(log n)logarithmic+ a littlehalving the range
O(n)linear10×one pass over the data
O(n²)quadratic100×a loop inside a loop

Count the work, don't time it

The trick is to count operations as a function of n. A single loop does n of them:

#include <iostream>
using namespace std;

int main() {
    int n = 8;
    int steps = 0;
    for (int i = 0; i < n; i++) {
        steps++;            // one unit of work per pass
    }
    cout << "n = " << n << ", steps = " << steps << endl;
    return 0;
}

Now nest it. The inner loop runs n times for each of the n outer passes:

#include <iostream>
using namespace std;

int main() {
    int n = 8;
    int steps = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            steps++;
        }
    }
    cout << "n = " << n << ", steps = " << steps << " = n*n" << endl;
    return 0;
}

Change n to 4 and 16 in the code: the step count goes 16 → 256. That is quadratic growth, and it is why a nested loop over 100,000 items is a problem.

Logarithmic: halving

Each pass throws away half the remaining range, so the count grows like log₂(n):

#include <iostream>
using namespace std;

int main() {
    int n = 1000000;
    int steps = 0;
    while (n > 1) {
        n /= 2;
        steps++;
    }
    cout << "steps to shrink 1000000 to 1: " << steps << endl;
    return 0;
}

20 steps for a million. This is why binary search is fast, and why the √n prime check beats trial division.

Drop the constants, keep the shape

O(2n) is written O(n); O(n² + n) is written O(n²). Constants and smaller terms disappear because at large n the biggest term dominates. So two loops one after another is O(n), not O(n²):

#include <iostream>
using namespace std;

int main() {
    int n = 8;
    int steps = 0;
    for (int i = 0; i < n; i++) steps++;     // n
    for (int i = 0; i < n; i++) steps++;     // + n
    cout << "two separate loops: " << steps << " = 2n, which is O(n)" << endl;
    return 0;
}

What this changes in your own code

Interview follow-ups

  1. What is the complexity of this loop? Count how many times the innermost statement runs as a function of n, then drop constants.
  2. Why is O(n log n) the usual sorting bound? Because comparison sorting needs n log n comparisons in the worst case — better than the O(n²) of bubble sort.
  3. Does Big O care about memory? Not this notation — space complexity is a separate Big O over memory. Counting sort trades space for time.

Related: compiler errors, vector vs array, and the practice section where every problem's deep-dive states its complexity.