← Back to Tutorials Chapter 11

Time Complexity & Asymptotic Analysis

Time complexity measures how the runtime of an algorithm grows as the input size increases. We use asymptotic notation to express this growth in a hardware-independent way. Understanding complexity lets you choose the right algorithm for large datasets.

An algorithm that runs in O(n) on a dataset of 1 million items takes about 1 million operations. One that runs in O(n²) takes about 1 trillion — that is the difference between one second and 12 days.
Big-O complexity graph

Big-O Notation (Upper Bound)

Big-O describes the worst-case growth rate. f(n) = O(g(n)) means that for sufficiently large n, f(n) is bounded above by a constant multiple of g(n).

Common Big-O Classes

NotationNameExampleFeasible for n
O(1)ConstantArray access, hash lookupAny size
O(log n)LogarithmicBinary search, balanced BST10⁹+
O(n)LinearLinear search, array traversal~10⁷
O(n log n)LinearithmicMergesort, Heapsort~10⁶
O(n²)QuadraticBubble sort, nested loops~10⁴
O(2ⁿ)ExponentialFibonacci recursion without memo~30
O(n!)FactorialBrute-force TSP~12
# O(1) — constant time
def get_first(arr):
    return arr[0]

# O(n) — linear time
def linear_search(arr, target):
    for i, v in enumerate(arr):
        if v == target:
            return i
    return -1

# O(n^2) — quadratic time
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]

# O(log n) — logarithmic time
def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1

Omega Notation & Theta Notation

Big-O is the most commonly used in practice. However, when analyzing algorithms like QuickSort, you will see O(n²) worst-case and Ω(n log n) best-case — but the expected case is Θ(n log n).

Analyzing Loops and Recursion

Single Loop

A loop that runs n times is O(n). If the loop variable halves each time (e.g., while i > 0: i //= 2), it is O(log n).

Nested Loops

Two nested loops each running n times yield O(n²). A loop of n iterations with an inner loop that runs i times yields O(n²) as well (sum of 1 to n is n(n+1)/2).

Recurrence Relations

Recursive algorithms are analyzed using recurrences. For mergesort: T(n) = 2T(n/2) + O(n), which solves to T(n) = O(n log n) via the Master Theorem.

# Fibonacci: O(2^n) naive vs O(n) with DP
def fib_naive(n):
    if n <= 1:
        return n
    return fib_naive(n - 1) + fib_naive(n - 2)

def fib_dp(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

Complexity of Sorting Algorithms

AlgorithmBestAverageWorstSpace
Bubble SortO(n)O(n²)O(n²)O(1)
Selection SortO(n²)O(n²)O(n²)O(1)
Insertion SortO(n)O(n²)O(n²)O(1)
Merge SortO(n log n)O(n log n)O(n log n)O(n)
Quick SortO(n log n)O(n log n)O(n²)O(log n)
Heap SortO(n log n)O(n log n)O(n log n)O(1)
Counting SortO(n + k)O(n + k)O(n + k)O(k)

Complexity of Searching Algorithms

AlgorithmTime ComplexitySpace
Linear SearchO(n)O(1)
Binary SearchO(log n)O(1)
BFS / DFSO(V + E)O(V)
DijkstraO((V + E) log V)O(V)
Bellman-FordO(V * E)O(V)
Floyd-WarshallO(V³)O(V²)

Space Complexity

Space complexity measures the amount of extra memory an algorithm uses. An in-place algorithm uses O(1) extra space (e.g., Bubble Sort). Algorithms that allocate new arrays (e.g., Merge Sort) use O(n) extra space. Recursion adds call stack space — a recursive function that calls itself n times deep uses O(n) stack space.

Practice Task

For each of the following code snippets, determine the time complexity using Big-O:

# Snippet 1
for i in range(n):
    for j in range(i, n):
        print(i, j)

# Snippet 2
i = n
while i > 0:
    print(i)
    i //= 2

# Snippet 3
for i in range(n):
    for j in range(10):
        print(i, j)

# Snippet 4
def mystery(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left = mystery(arr[:mid])
    right = mystery(arr[mid:])
    return merge(left, right)

Answers: O(n²), O(log n), O(n), O(n log n). Can you explain why?