FirstHack Learn
Log in Sign up free
Lessons in this course 0/6 All courses Algorithms and Complexity

CSE

Progress0 / 6 lessons
  1. 1. Big-O without hand-waving
  2. 2. Searching: linear and binary
  3. 3. Bubble, selection and insertion sort
  4. 4. Merge sort and quick sort
  5. 5. Recursion and how to reason about it
  6. 6. Greedy versus dynamic programming

Courses › Algorithms and Complexity

Big-O without hand-waving

What the notation really claims, why constants are dropped, and a table of n against operations.

11 min read · Lesson 1 of 6 · Free

The question Big-O answers

Not "how fast is this program?" - that depends on the machine. The question is: if I double the input, what happens to the running time?

That question has a machine-independent answer, and it is the only thing that matters once your input is large.

Count operations, then throw things away

C++
int sum_all(const int a[], int n) {
    int total = 0;              // 1 operation
    for (int i = 0; i < n; i++) // n+1 comparisons, n increments
        total += a[i];          // n additions
    return total;               // 1 operation
}

Total: roughly 3n + 3 operations. We then do two things.

Drop the constant term. For n = 1,000,000, the +3 is irrelevant.

Drop the multiplier. 3n and 100n both double when n doubles. The multiplier depends on your CPU and compiler; the shape does not.

What is left is n. We write O(n) and say the function is linear.

ℹ️

O(n) is a claim about growth, not speed. An O(n) algorithm with a huge constant can be slower than an O(n^2) one for small n. That is why std::sort switches to insertion sort for tiny sub-arrays. Big-O tells you who wins eventually, not who wins today.

Reading a loop

C++
for (int i = 0; i < n; i++)              // O(n)
    for (int j = 0; j < n; j++)          // O(n) each time
        cout << i * j << " ";            // O(1)

Nested loops multiply: O(n) x O(n) = O(n^2).

C++
for (int i = 0; i < n; i++)
    for (int j = i + 1; j < n; j++)
        cout << a[i] << a[j] << "\n";

This one looks cheaper, and it is - but only by a constant. The inner loop runs n-1, then n-2, down to 0. The total is n(n-1)/2, which is n^2/2 - n/2. Drop the constant and the lower term: still O(n^2). Half of a huge number is still huge.

C++
for (int i = 1; i < n; i = i * 2)
    cout << i << " ";

i goes 1, 2, 4, 8, ... The loop runs until 2^k is at least n, so k = log2(n). This is O(log n). Any time a variable is multiplied or divided instead of incremented, you have a logarithm.

The table you should memorise

Operations performed, for each growth rate:

n O(log n) O(n) O(n log n) O(n^2) O(2^n)
10 3 10 33 100 1,024
100 7 100 664 10,000 10^30
1,000 10 1,000 9,966 1,000,000 forget it
100,000 17 100,000 1.7 million 10 billion forget it
1,000,000 20 1 million 20 million 10^12 forget it

A modern CPU does roughly 10^8 to 10^9 simple operations per second. Put that against the table:

  • O(n^2) at n = 100,000 is 10^10 operations: about a minute or more.
  • O(n log n) at the same n is 1.7 million: instant.
  • O(2^n) at n = 100 is more operations than there are atoms in the observable

universe. No faster computer will save you. Only a better algorithm will.

💡

Competitive programming rule of thumb, and it works for placement tests too: if n is up to 10^5 or 10^6, you need O(n) or O(n log n). If n is up to a few thousand, O(n^2) is fine. If n is 20 or less, an exponential 2^n solution is probably what is expected.

Big-O, Big-Omega, Big-Theta

These get confused constantly, and the difference is one mark.

  • O(f) is an upper bound. "No worse than."
  • Omega(f) is a lower bound. "No better than."
  • Theta(f) is both. "Exactly this growth."

Linear search is O(n) and Omega(1) - one comparison if the item is first, n if it is last. It is not Theta of anything single, so we usually state the worst case, O(n).

Technically, saying merge sort is O(n^2) is true - it is an upper bound, just a loose one. In an exam, always give the tightest bound you can.

Best, average, worst

Algorithm Best Average Worst
Linear search O(1) O(n) O(n)
Binary search O(1) O(log n) O(log n)
Bubble sort (with flag) O(n) O(n^2) O(n^2)
Insertion sort O(n) O(n^2) O(n^2)
Merge sort O(n log n) O(n log n) O(n log n)
Quick sort O(n log n) O(n log n) O(n^2)
Hash table lookup O(1) O(1) O(n)

Quick sort's worst case is O(n^2) and hash lookup's worst case is O(n), yet both are used everywhere - because the bad cases are rare and avoidable in practice. Knowing when the worst case happens is the real skill, and the next few lessons cover exactly that.

Space complexity

The same notation applies to memory.

C++
int count_pairs(const int a[], int n) {
    int c = 0;
    for (int i = 0; i < n; i++)
        for (int j = i + 1; j < n; j++)
            if (a[i] + a[j] == 0) c++;
    return c;
}

Time O(n^2), space O(1) - it uses three variables no matter how big n is.

Merge sort is O(n log n) time but O(n) extra space, because it needs a temporary array. Quick sort is in-place, O(log n) space for the recursion stack. On a memory-limited system that difference decides which one you use.

⚠️

Recursion is never free in space. Each call keeps a stack frame alive. A recursive function with depth n uses O(n) memory even if it allocates nothing. That is what "stack overflow" means, and it is why recursion depth matters as much as call count.