Complexity analysis: Big-O, Ω, Θ
You learn to count how many operations a piece of code performs and to write the result with Big-O. The basis of the “What is the running time?” questions that appear on the midterm every year.
The first question of a data structures course is always the same: how fast is this code? We don’t measure in seconds, because seconds change from computer to computer. Instead, we look at how the number of operations grows as the input size n grows. On the exam you are given a piece of code and asked “What is the time complexity?”. The answer is one line, but its justification depends on counting the loops correctly.
Golden rules:
- Drop constants. 3n and n are in the same class: O(n).
- Keep the dominant term. For n² + 1000n the answer is O(n²).
- Sequential loops add, nested loops multiply.
- If a counter doubles or halves, the number of rounds is log n.
- Look at the bound of the inner loop. If it does not depend on n, the inner loop is constant.
Algorithm and running time
An algorithm is a finite sequence of steps that solves a problem. A data structure is how data is organized in memory: array, linked list, stack, queue. You can solve the same problem with different data structures, and the speed can differ a lot. Throughout this course, for each structure we will ask “how many steps for insertion, how many for deletion, how many for search”.
We write the running time as a function of n: T(n). Consider this method that adds up the elements of an array:
int sum = 0; // 1 operation
for (int i = 0; i < n; i++) { // 1 comparison per round
sum += a[i]; // 1 addition per round
}
return sum; // 1 operation
Roughly T(n) = 2n + 2 operations. 22 for n = 10, 202 for n = 100. The exact number doesn’t matter here. What matters: when n grows ten times, the work grows about ten times. This is linear growth and is written O(n).
Big-O, Big-Omega, Big-Theta
There are three notations, and all three answer the question “how fast does the function grow for large n”.
| Notation | Read as | Meaning | Analogy |
|---|---|---|---|
| Big-O | T does not grow faster than f (upper bound) | ||
| Big-Omega | T does not grow slower than f (lower bound) | ||
| Big-Theta | T grows at the same rate as f |
Formal definition: if there exist a constant and an such that for every , then .
Example: . If you choose , , then for . So . Since it is also , and since both hold, .
PITFALL · Big-O is only an upper bound. The statement is also mathematically true, because n² grows faster. But on the exam the “tightest bound” is asked for. Your answer should be O(n).
In practice, exams ask for Big-O and expect the tightest answer. You can think of it as writing the same thing as Big-Theta.
Best, worst and average case
For some algorithms the time also depends on the contents of the input. When doing linear search in an array, if the element you are looking for is first, 1 comparison is enough (best case, O(1)). If it is not there at all, n comparisons are needed (worst case, O(n)). Unless stated otherwise, Big-O is given for the worst case.
Common growth rates
| Class | Name | Typical example | Approximate work for n = 1000 |
|---|---|---|---|
| O(1) | constant | accessing a[i] in an array, inserting at the head of a list | 1 |
| O(log n) | logarithmic | binary search | 10 |
| O(n) | linear | traversing an array once | 1000 |
| O(n log n) | linearithmic | merge sort | ~10 000 |
| O(n²) | quadratic | two nested loops, bubble sort | 1 000 000 |
| O(n³) | cubic | three nested loops | 10⁹ |
| O(2ⁿ) | exponential | trying all subsets | astronomical |
Order:
In computer science, if the base of the log is not stated, it is 2. Inside Big-O the base doesn’t matter, because between and there is only a constant factor.
TIP · Time estimation. For the question “an O(n²) algorithm takes 2 seconds for n = 1000, how long for n = 4000?” use the ratio: n 4 times → time 4² = 16 times → 32 seconds. If it were O(n) it would be 4 times (8 seconds), and if O(n³) it would be 64 times.
Counting operations in loops
This is the real topic of the exam. For each loop ask “how many times does it run?”, then combine.
A single loop
for (int i = 0; i < n; i++) // n rounds → O(n)
count++;
for (int i = 0; i < n; i += 3) // n/3 rounds → still O(n)
count++;
As long as the step size is constant (1, 2, 3…), the loop is linear.
Multiplying / dividing loop
for (int i = 1; i < n; i *= 2) // 1, 2, 4, 8, ... → log n rounds
count++;
How many times do you double before you pass n? If then . The same holds for a loop going down with i /= 2. For n = 16, i: 16, 8, 4, 2, 1. Five rounds, that is .
Verifying with a counter (n doubles):
i *= 2 loop: n=1000 → 10 rounds n=2000 → 11 n=4000 → 12 n=8000 → 13
When n doubles, only one round is added. This is the mark of logarithmic growth.
Nested loops
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
count++; // n · n = n² → O(n²)
If the bound of the inner loop depends on the variable of the outer loop, write out the sum:
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
count++;
i = 0 → inner loop 0 times
i = 1 → 1 time
i = 2 → 2 times
...
i = n-1 → n-1 times
Total = 0 + 1 + ... + (n-1) = n(n-1)/2
n = 10 → 45
. The dominant term is , drop the constant: O(n²). It does half the work but the class doesn’t change.
Mixed: linear × logarithmic
for (int i = 0; i < n; i++) // n times
for (int j = 1; j < n; j *= 2) // log n times each time
count++;
The total is n log n. Counter results:
n=1000 → 10000 n=2000 → 22000 (x2.20) n=4000 → 48000 (x2.18) n=8000 → 104000 (x2.17)
If it were O(n) the ratio would be exactly 2, and if it were O(n²) it would be 4. Slightly above 2: n log n.
Sequential loops
for (int i = 0; i < n; i++) count++; // n
for (int j = 0; j < n * n; j++) count++; // n²
The total is , the dominant term is n²: O(n²). They are added, not multiplied.
A dependent but logarithmic outer loop
A hard example: the outer loop doubles, and the inner loop runs i times.
for (int i = 1; i <= n; i *= 2)
for (int j = 0; j < i; j++)
count++;
The inner loop runs 1, 2, 4, …, n times in turn. The total is . So O(n). 31 rounds for n = 16, 2047 rounds for n = 1024. Don’t answer by reflex “there are two loops, so it’s n log n”; add them up.
A step-by-step solution pattern
On the exam, do this for every piece of code:
1. How many times does the innermost statement run? Write the number of rounds for each loop.
2. If nested, multiply the numbers of rounds (if the inner loop depends on the outer one, expand the sum).
3. If sequential, add.
4. Take the dominant term, drop the constants.
5. If you are not sure, count by hand for n = 8 or n = 16, then double n and check the ratio.
The fifth step is a lifesaver. If you find a result like n = 8 → 24 operations, n = 16 → 64 operations, the ratio is about 2.7, which points to n log n (8·3 = 24, 16·4 = 64).
Common mistakes
| Wrong idea | Correct |
|---|---|
| Two nested loops are always O(n²) | If the inner loop runs a constant number of times it is O(n); if logarithmic, O(n log n) |
An inner loop j < i is O(n log n) | → O(n²) |
| Two sequential loops are n · n² | They are added: n + n² = O(n²) |
| For the answer is O(5n²) | The constant is dropped: O(n²) |
An i *= 2 loop is O(n/2) | The number of rounds is log n: O(log n) |
| Big-O gives the best case | Unless stated otherwise, the worst case |
| is wrong | It is true but not the tightest bound; write O(n) on the exam |
| The base of the log matters | In Big-O the base doesn’t matter, it differs only by a constant factor |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
What is the time complexity of this fragment in Big-O notation?
int count = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
count++;Show answer
The outer loop runs n times, and in each round the inner loop runs n times. In total count++ runs n · n = n² times. Checking with a counter: 1 000 000 for n = 1000, 4 000 000 for n = 2000 (double n, four times the work).
What is the time complexity of this fragment?
int count = 0;
for (int i = 1; i < n; i *= 2)
count++;Show answer
i doubles in each round: 1, 2, 4, 8, ... How many doublings does it take to reach n? About times. 10 rounds for n = 1000, only 20 rounds for n = 1 000 000.
For n = 10, how many times is count++ executed? What is the Big-O?
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
count++;Show answer
When i = 0 the inner loop runs 0 times, when i = 1 it runs once, ..., when i = 9 it runs 9 times. The total is 0 + 1 + ... + 9 = 45. The general formula is , the dominant term is , and dropping the constant gives O(n²).
For n = 16, how many times does the loop body run?
for (int i = n; i > 0; i /= 2)
count++;Show answer
The values of i: 16, 8, 4, 2, 1. Then i becomes 0 and the loop ends. 5 rounds. In general rounds, that is O(log n).
What is the time complexity of this fragment?
for (int i = 0; i < n; i++)
for (int j = 1; j < n; j *= 2)
count++;Show answer
The outer loop runs n times. Since the inner loop doubles j, it runs log n times each time. The product is n log n. Counter: 8 · 3 = 24 for n = 8. When n doubles, the work grows by slightly more than 2 (x2.2), which is the signature of n log n.
Give the tightest Big-O for .
Show answer
For large n the fastest-growing term wins. grows faster than ; constants (3, 100, 5000) are dropped in Big-O. The answer is O(n²).
What is the time complexity of these two sequential loops?
for (int i = 0; i < n; i++)
count++;
for (int j = 0; j < n * n; j++)
count++;Show answer
The times of sequential loops are added: n + n². The dominant term in the sum is n², so O(n²). If they were nested, they would be multiplied.
Order these growth rates from slowest to fastest: , , , , , , .
Show answer
Constant < logarithmic < linear < n log n < quadratic < cubic < exponential. For n = 1000: log n ≈ 10, n log n ≈ 9966, n² = 10⁶, n³ = 10⁹. And is a number with 300 digits.
An algorithm takes 2 seconds for . Approximately how long will it take for ?
Show answer
n grew 4 times. For a quadratic algorithm the time grows times. 2 · 16 = 32 seconds.
What is the time complexity of this fragment?
for (int i = 0; i < n; i++)
for (int j = 0; j < 5; j++)
count++;Show answer
The inner loop does not depend on n, it always runs 5 times. The total is 5n operations. The constant 5 is dropped: O(n). Don't write n² everywhere you see a nested loop.
What is the time complexity of this fragment? For n = 50, how many times does the body run?
for (int i = 1; i * i <= n; i++)
count++;Show answer
The condition is , that is . For n = 50, i = 1..7 (7² = 49 ≤ 50, 8² = 64 > 50), 7 rounds. When n grows 4 times, the number of rounds doubles (n = 10000 → 100, n = 40000 → 200): O(√n).
What is the time complexity of this fragment? (Hint: the inner loop step depends on i.)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j += i)
count++;Show answer
When i = 1 the inner loop runs n times, when i = 2 it runs n/2 times, when i = 3 it runs n/3 times... The total is . The parenthesis is the harmonic series, about ln n. The result is O(n log n). Checking with a counter: when n doubles the work grows x2.10; if it were n² it would be x4.
Verification: The operation count of each code fragment was counted in Java (verify/SEN2211/W01.java); the counters for n = 1000, 2000, 4000, 8000 were compared, and it was checked that when n doubles the ratio (x2, x4, +1, x2.1) matches the claimed growth rate. Exact counts for small n come from the same program.