SEN2211 · Week 1 · Introduction to Data Structures and Algorithms, Complexity Analysis

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.

algorithmrunning timetime complexityBig-OBig-OmegaBig-Thetagrowth rateworst casebest caselogarithmiclinearithmicquadratic

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:

  1. Drop constants. 3n and n are in the same class: O(n).
  2. Keep the dominant term. For n² + 1000n the answer is O(n²).
  3. Sequential loops add, nested loops multiply.
  4. If a counter doubles or halves, the number of rounds is log n.
  5. 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”.

NotationRead asMeaningAnalogy
T(n)=O(f(n))T(n) = O(f(n))Big-OT does not grow faster than f (upper bound)≤\le
T(n)=Ω(f(n))T(n) = \Omega(f(n))Big-OmegaT does not grow slower than f (lower bound)≥\ge
T(n)=Θ(f(n))T(n) = \Theta(f(n))Big-ThetaT grows at the same rate as f==

Formal definition: if there exist a constant c>0c > 0 and an n0n_0 such that T(n)≤c⋅f(n)T(n) \le c \cdot f(n) for every n≥n0n \ge n_0, then T(n)=O(f(n))T(n) = O(f(n)).

Example: T(n)=3n+5T(n) = 3n + 5. If you choose c=4c = 4, n0=5n_0 = 5, then 3n+5≤4n3n + 5 \le 4n for n≥5n \ge 5. So T(n)=O(n)T(n) = O(n). Since 3n+5≥3n3n + 5 \ge 3n it is also Ω(n)\Omega(n), and since both hold, Θ(n)\Theta(n).

PITFALL · Big-O is only an upper bound. The statement 3n+5=O(n2)3n + 5 = O(n^2) 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

ClassNameTypical exampleApproximate work for n = 1000
O(1)constantaccessing a[i] in an array, inserting at the head of a list1
O(log n)logarithmicbinary search10
O(n)lineartraversing an array once1000
O(n log n)linearithmicmerge sort~10 000
O(n²)quadratictwo nested loops, bubble sort1 000 000
O(n³)cubicthree nested loops10⁹
O(2ⁿ)exponentialtrying all subsetsastronomical

Order: 1<log⁡n<n<n<nlog⁡n<n2<n3<2n<n!1 < \log n < \sqrt{n} < n < n\log n < n^2 < n^3 < 2^n < n!

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 log⁡2n\log_2 n and log⁡10n\log_{10} n 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 2k≥n2^k \ge n then k=log⁡2nk = \log_2 n. The same holds for a loop going down with i /= 2. For n = 16, i: 16, 8, 4, 2, 1. Five rounds, that is log⁡216+1\log_2 16 + 1.

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

n(n−1)2=n22−n2\frac{n(n-1)}{2} = \frac{n^2}{2} - \frac{n}{2}. The dominant term is n22\frac{n^2}{2}, 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 n+n2n + n^2, 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 1+2+4+⋯+n=2n−11 + 2 + 4 + \dots + n = 2n - 1. 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 ideaCorrect
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)n(n−1)/2n(n-1)/2 → O(n²)
Two sequential loops are n · n²They are added: n + n² = O(n²)
For 5n2+3n5n^2 + 3n 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 caseUnless stated otherwise, the worst case
3n=O(n2)3n = O(n^2) is wrongIt is true but not the tightest bound; write O(n) on the exam
The base of the log mattersIn 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.

Question 1easy

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
O(n^2)

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).

Question 2easy

What is the time complexity of this fragment?

int count = 0;
for (int i = 1; i < n; i *= 2)
    count++;
Show answer
O(log n)

i doubles in each round: 1, 2, 4, 8, ... How many doublings does it take to reach n? About log⁡2n\log_2 n times. 10 rounds for n = 1000, only 20 rounds for n = 1 000 000.

Question 3medium

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
45, O(n^2)

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 n(n−1)2\frac{n(n-1)}{2}, the dominant term is n22\frac{n^2}{2}, and dropping the constant gives O(n²).

Question 4medium

For n = 16, how many times does the loop body run?

for (int i = n; i > 0; i /= 2)
    count++;
Show answer
5

The values of i: 16, 8, 4, 2, 1. Then i becomes 0 and the loop ends. 5 rounds. In general ⌊log⁡2n⌋+1\lfloor \log_2 n \rfloor + 1 rounds, that is O(log n).

Question 5medium

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
O(n log n)

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.

Question 6easy

Give the tightest Big-O for f(n)=3n2+100nlog⁡n+5000f(n) = 3n^2 + 100n\log n + 5000.

Show answer
O(n^2)

For large n the fastest-growing term wins. n2n^2 grows faster than nlog⁡nn\log n; constants (3, 100, 5000) are dropped in Big-O. The answer is O(n²).

Question 7medium

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
O(n^2)

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.

Question 8easy

Order these growth rates from slowest to fastest: n2n^2, log⁡n\log n, 2n2^n, nn, nlog⁡nn\log n, 11, n3n^3.

Show answer
1, log n, n, n log n, n^2, n^3, 2^n

Constant < logarithmic < linear < n log n < quadratic < cubic < exponential. For n = 1000: log n ≈ 10, n log n ≈ 9966, n² = 10⁶, n³ = 10⁹. And 2n2^n is a number with 300 digits.

Question 9medium

An O(n2)O(n^2) algorithm takes 2 seconds for n=1000n = 1000. Approximately how long will it take for n=4000n = 4000?

Show answer
32 seconds

n grew 4 times. For a quadratic algorithm the time grows 42=164^2 = 16 times. 2 · 16 = 32 seconds.

Question 10easy

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
O(n)

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.

Question 11hard

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
O(sqrt(n)), 7

The condition is i2≤ni^2 \le n, that is i≤ni \le \sqrt{n}. 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).

Question 12hard

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
O(n log n)

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 n(1+12+13+⋯+1n)n(1 + \frac{1}{2} + \frac{1}{3} + \dots + \frac{1}{n}). 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.

Review card

Big-O = upper bound (used for the worst case), Ω = lower bound, Θ = both (exact growth) Rule 1: drop constants 5n → O(n) n²/2 → O(n²) Rule 2: keep the dominant term n² + n log n + 1000 → O(n²) Sequential loops: ADD (n + n² = O(n²)) Nested loops: MULTIPLY (n · n = O(n²)) i++ up to n → n rounds i *= 2 or i /= 2 → log n rounds i * i <= n → √n rounds inner loop j < i → n(n-1)/2 → O(n²) constant inner loop (j < 5) → O(n), NOT n² j += i (harmonic) → O(n log n) Order: 1 < log n < √n < n < n log n < n² < n³ < 2ⁿ < n! Time estimate: O(n²) and n x4 → time x16; O(n) → x4; O(log n) → +2 steps When n doubles: O(n) x2, O(n²) x4, O(n³) x8, O(log n) +1, O(n log n) ~x2.1