Mojo Algorithm Complexity

Algorithm complexity measures how the runtime or memory usage of a program grows as the input size grows. Knowing complexity lets you predict whether a solution handles 100 items or 10 million items without running it first. This knowledge drives every performance decision in Mojo — when to vectorize, when to tile, and when to choose a different algorithm entirely.

The Restaurant Analogy

  Finding one name in a guest list of N people:

  Approach 1: Read every name until you find it
    1 person → 1 check
    10 people → up to 10 checks
    1000 people → up to 1000 checks
    Growth: proportional to N  → O(n)

  Approach 2: List is sorted alphabetically — flip to middle,
              go left or right, repeat
    1 person → 1 check
    10 people → up to 4 checks
    1000 people → up to 10 checks
    Growth: proportional to log(N) → O(log n)

  O(log n) wins by a massive margin at scale.

Big-O Notation

Big-O describes the worst-case growth rate of an algorithm, ignoring constant factors and lower-order terms. It answers: "as N doubles, how does the time change?"

  Notation  | Name          | N=10  | N=100  | N=1000
  ----------|---------------|-------|--------|--------
  O(1)      | Constant      |   1   |    1   |     1
  O(log n)  | Logarithmic   |   3   |    7   |    10
  O(n)      | Linear        |  10   |  100   |  1000
  O(n log n)| Log-linear    |  33   |  664   |  9966
  O(n²)     | Quadratic     | 100   | 10000  |  1000000
  O(2^n)    | Exponential   |1024   |  huge  |  ∞ (impractical)
Growth rate visual (relative time):

  O(1)      ──────────────────────────── flat line
  O(log n)  ─────────────────╱─ slow curve
  O(n)      ──────────────╱──── diagonal
  O(n log n)────────────╱───── steeper diagonal
  O(n²)     ──────────╱──────── steep curve
  O(2^n)    ────────╱────────── vertical wall

  N →  10   100  1000  10000

O(1) — Constant Time

The operation takes the same time regardless of input size.

fn get_first(data: List[Int]) -> Int:
    return data[0]   # always one step, N doesn't matter

fn main():
    var big = List[Int]()
    for i in range(1000000):
        big.append(i)

    print(get_first(big))   # instant — O(1)
Examples of O(1):
  Array index access:  data[i]
  Dictionary lookup:   scores["Alice"]
  Stack push/pop:      stack.push(x)
  Math operations:     a + b, a * b

O(n) — Linear Time

Time grows proportionally with input size. Doubling N doubles the time.

fn find_max(data: List[Int]) -> Int:
    var maximum = data[0]
    for i in range(len(data)):   # visits every element once
        if data[i] > maximum:
            maximum = data[i]
    return maximum
Examples of O(n):
  Linear search (unsorted list)
  Summing all elements
  Printing every item
  Copying a list

O(n²) — Quadratic Time

A nested loop where both loops run N times. Doubling N quadruples the time.

fn bubble_sort(inout arr: List[Int]):
    var n = len(arr)
    for i in range(n):           # outer loop: n times
        for j in range(n - 1):  # inner loop: n times
            if arr[j] > arr[j+1]:
                var t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t
Examples of O(n²):
  Bubble sort, selection sort, insertion sort (worst case)
  All-pairs comparison (find duplicates by brute force)
  Naive string search (fixed approach)

  N=100:   10,000 operations   → fast
  N=10000: 100,000,000 ops     → noticeable
  N=100000: 10 billion ops     → too slow

O(log n) — Logarithmic Time

Each step halves the remaining work. Even for a billion items, only 30 steps are needed.

fn binary_search(data: List[Int], target: Int) -> Int:
    var lo = 0
    var hi = len(data) - 1
    while lo <= hi:
        var mid = (lo + hi) // 2
        if data[mid] == target: return mid
        elif data[mid] < target: lo = mid + 1
        else: hi = mid - 1
    return -1
Binary search halving diagram:
  N=1024 items:
  Step 1: 1024 → 512  (check middle, go left or right)
  Step 2:  512 → 256
  Step 3:  256 → 128
  Step 4:  128 →  64
  Step 5:   64 →  32
  ...
  Step 10:   1 → found or not found

  log₂(1024) = 10 steps max.
  log₂(1,000,000) ≈ 20 steps.
  log₂(1,000,000,000) ≈ 30 steps.

O(n log n) — Log-Linear Time

The best achievable complexity for comparison-based sorting. Merge sort and Mojo's built-in sort achieve this.

from algorithm import sort

fn main():
    var data = List[Int]()
    for i in range(100_000):
        data.append(100_000 - i)

    sort(data)   # O(n log n) — handles 100k elements comfortably
    print(data[0], data[len(data)-1])   # 1 100000

Space Complexity

Space complexity measures how memory usage grows with input size, using the same Big-O notation.

fn sum_list(data: List[Int]) -> Int:
    var total = 0               # O(1) extra space — one variable
    for i in range(len(data)):
        total += data[i]
    return total

fn copy_list(data: List[Int]) -> List[Int]:
    var result = List[Int]()    # O(n) extra space — grows with input
    for i in range(len(data)):
        result.append(data[i])
    return result
Space complexity examples:
  O(1): bubble sort (sorts in-place, one temp variable)
  O(n): merge sort (needs a copy of the data)
  O(n): storing all elements in a new list
  O(n²): storing an n×n matrix

Measuring Complexity in Mojo Code

How to identify the complexity of your code:

  Single loop over n items         → O(n)
  Two independent loops over n     → O(n) + O(n) = O(n)
  Loop inside a loop, both size n  → O(n²)
  Halving the input each step      → O(log n)
  Recursion that branches twice    → O(2^n) — dangerous!
  Recursion that halves each time  → O(log n)

Example analysis:
  fn example(data: List[Int]) -> Int:
      var total = 0               # O(1)
      for i in range(len(data)):  # O(n)
          for j in range(len(data)):  # O(n) per outer step
              total += data[i] * data[j]
      return total
  Overall: O(n × n) = O(n²)

Practical Impact in Mojo

  Problem: process 1,000,000 data points
  Target: complete in under 1 second (CPU ~10⁹ ops/sec)

  Algorithm     Complexity  Operations    Feasible?
  ─────────────────────────────────────────────────
  Direct access O(1)          1            Yes (instant)
  Linear scan   O(n)          1,000,000    Yes
  Merge sort    O(n log n)   20,000,000   Yes
  Bubble sort   O(n²)    1,000,000,000,000  No (1000 seconds)
  Brute force   O(2^n)    ∞               No

  SIMD vectorization multiplies throughput by 8-16×
  but does NOT change the Big-O complexity class.
  O(n) with SIMD is still O(n), just ~10× faster in practice.

Key Takeaways

Big-O notation describes how algorithm runtime grows as input size N grows. O(1) is constant, O(log n) is logarithmic (binary search), O(n) is linear, O(n log n) is optimal sorting, O(n²) is quadratic (nested loops), and O(2^n) is exponential (impractical). Always check the complexity of your algorithm before optimizing at the hardware level — a faster CPU cannot save an O(n²) algorithm when n is large. SIMD and parallelism multiply throughput but do not change complexity class. Space complexity measures memory growth using the same notation. When choosing between algorithms, prefer the lower complexity class first, then optimize constants with Mojo's hardware features.

Leave a Comment

Your email address will not be published. Required fields are marked *