Bytebook Compiler

Analysis of Algorithms

Analysing Loops & Recursive Code

Turn code into running times: independent and dependent nested loops, logarithmic loops, writing recurrences for recursive functions, and an introduction to amortised analysis.

5 min read2 runnable examples4-question quiz

Independent loops

Count the iterations of each loop and multiply when they are nested:

C++
for (int i = 0; i < n; i++)          // n times
    for (int j = 0; j < m; j++)      // m times each
        work();                      // total n·m → O(nm)

Dependent loops: sum the iterations

When the inner loop depends on the outer one, add up its iteration counts:

C++
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= i; j++)     // runs i times
        work();

Total = 1 + 2 + … + n = n(n + 1)/2 = Θ(n²).

C++
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j += i)  // runs about n/i times
        work();

Total = n/1 + n/2 + … + n/n = n·(1 + 1/2 + … + 1/n) ≈ n ln n = Θ(n log n) (the harmonic series).

Logarithmic loops

A loop variable that is multiplied or divided by a constant each step runs Θ(log n) times:

C++
for (int i = 1; i < n; i *= 2) work();       // log2 n iterations
for (int i = n; i > 0; i /= 3) work();       // log3 n iterations
for (int i = 2; i < n; i = i * i) work();    // log log n iterations

Measure it yourself

C++
#include <cstdio>

int main() {
    for (long n = 1000; n <= 1000000; n *= 10) {
        long a = 0, b = 0, c = 0, d = 0;
        for (long i = 1; i < n; i *= 2) a++;                         // log n
        for (long i = 1; i <= n; i++) for (long j = 1; j <= n; j += i) b++;   // n log n (harmonic)
        for (long i = 2; i < n; i = i * i) c++;                      // log log n
        for (long i = 1; i * i <= n; i++) d++;                       // sqrt n
        printf("n=%8ld  log:%3ld  harmonic:%9ld  loglog:%2ld  sqrt:%5ld\n", n, a, b, c, d);
    }
    return 0;
}
Output
n=    1000  log: 10  harmonic:     8053  loglog: 4  sqrt:   31
n=   10000  log: 14  harmonic:   103643  loglog: 4  sqrt:  100
n=  100000  log: 17  harmonic:  1266714  loglog: 5  sqrt:  316
n= 1000000  log: 20  harmonic: 14969985  loglog: 5  sqrt: 1000

Common patterns

Code shape Running time
single loop to n Θ(n)
two nested loops to n Θ(n²)
i doubles each step Θ(log n)
loop to n with an inner doubling loop Θ(n log n)
inner loop runs i times Θ(n²)
inner loop runs n/i times Θ(n log n)
loop while i*i <= n Θ(√n)

Recursive code: write a recurrence

For a recursive function, write T(n) in terms of T on smaller inputs plus the work done outside the recursive calls:

Function Recurrence Solution
factorial(n) = n · factorial(n−1) T(n) = T(n−1) + O(1) Θ(n)
binary search T(n) = T(n/2) + O(1) Θ(log n)
merge sort T(n) = 2T(n/2) + O(n) Θ(n log n)
naive Fibonacci T(n) = T(n−1) + T(n−2) + O(1) Θ(φⁿ) ≈ Θ(1.618ⁿ)
Tower of Hanoi T(n) = 2T(n−1) + O(1) Θ(2ⁿ)
selection sort, recursive T(n) = T(n−1) + O(n) Θ(n²)

The next lesson shows how to solve recurrences like these.

Best, worst and average case

For linear search on n items:

  • Best case: key at position 1 → 1 comparison → Θ(1).
  • Worst case: key absent → n comparisons → Θ(n).
  • Average case: if the key is equally likely to be at any position, expected comparisons = (1 + 2 + … + n)/n = (n + 1)/2 → Θ(n).

Amortised analysis

Some operations are usually cheap but occasionally expensive. Amortised analysis averages the cost over a sequence of operations (no probability involved).

Example: vector::push_back doubles the capacity when full, copying all elements. A single push can cost O(n), but over n pushes the total copying is 1 + 2 + 4 + … + n < 2n, so each push costs O(1) amortised.

C++
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    long copies = 0;
    for (int i = 0; i < 1000; i++) {
        if (v.size() == v.capacity()) copies += v.size();     // a reallocation copies everything
        v.push_back(i);
    }
    cout << "1000 pushes, total elements copied: " << copies << " (less than 2 x 1000)" << endl;
    cout << "final capacity: " << v.capacity() << endl;
    return 0;
}
Output
1000 pushes, total elements copied: 1023 (less than 2 x 1000)
final capacity: 1024

The three classic methods are the aggregate, accounting and potential methods.

Test yourself

4 questions · pick an answer to check it instantly

  1. 1

    What is the running time of this code?

    C++
    for (int i = 1; i <= n; i *= 3)
        for (int j = 0; j < n; j++)
            x++;
  2. 2

    What is the recurrence of binary search?

  3. 3

    How many times does for (i = 1; i * i <= n; i++) iterate?

  4. 4

    What is the amortised cost of push_back on a doubling dynamic array?