Back to articles

Understanding Big O Notation

21 min
Algorithm

Big O notation is a mathematical framework used to describe how an algorithm's execution time or memory footprint scales as the input data volume grows. It primarily focuses on the worst-case scenario. Rather than measuring exact execution time in seconds, Big O provides an abstract metric to evaluate whether an algorithm possesses high scalability and efficiency, qualities that become critical when handling large-scale datasets.

The Purpose of Big O

  • Analyzing time complexity (execution performance growth).
  • Analyzing space complexity (memory allocation growth).
  • Guiding the choice of the optimal data structure and algorithmic approach.
  • Comparing performance deltas under high load to diagnose computational or memory bottlenecks.
  • Determining whether an implementation is structurally scalable.

Time Complexity vs. Space Complexity

  • Time Complexity: Measures how the execution time of an algorithm scales relative to the input size (nn).
  • Space Complexity: Measures how the total auxiliary memory allocated by an algorithm scales relative to the input size (nn).

Common Big O Complexity Classes

The table below lists standard complexity classes ordered from highest efficiency to lowest:

Big OClass NameExample Operation / AlgorithmDescription
O(1)O(1)Constant TimeIndex-based array accessRuntime remains identical regardless of input size.
O(logn)O(\log n)Logarithmic TimeBinary SearchProblem size is halved with each execution step.
O(n)O(n)Linear TimeSingle-pass loop (Linear Scan)Runtime scales linearly and directly with input size.
O(nlogn)O(n \log n)Linearithmic TimeQuick Sort / Merge SortStandard for efficient divide-and-conquer sorting algorithms (average case).
O(n2)O(n^2)Quadratic TimeNested loopsEvery element interacts with every other element (e.g., Bubble Sort; common in brute-force paths).
O(2n)O(2^n)Exponential TimeUnoptimized recursive FibonacciExecution doubles with each added input step, leading to explosive growth.
O(n!)O(n!)Factorial TimeBrute-force permutationsTypical of combinatorial brute-force solutions, such as the Traveling Salesperson Problem (TSP).

Time Complexity Examples

O(1)O(1): Constant Time – Array Lookup

The function performs a single atomic lookup, meaning its runtime remains constant regardless of array size.

TypeScript
// TypeScript
function getFirst(arr: T[]): T | undefined {
  return arr[0];
}
Python
# Python
from typing import List, Any, Optional

def get_first(arr: List[Any]) -> Optional[Any]:
    return arr[0] if arr else None
Java
// Java
public class ArrayUtils {
    public static  T getFirst(T[] arr) {
        if (arr == null || arr.length == 0) return null;
        return arr[0];
    }
}

O(n)O(n): Linear Time – Array Traversal

The number of operations scales one-to-one with the size of the dataset.

TypeScript
// TypeScript
function printAll(arr: T[]): void {
  for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
  }
}
Python
# Python
from typing import List, Any

def print_all(arr: List[Any]) -> None:
    for item in arr:
        print(item)
Java
// Java
public class Traversal {
    public static  void printAll(T[] arr) {
        for (T item : arr) {
            System.out.println(item);
        }
    }
}

O(n2)O(n^2): Quadratic Time – Nested Loops

Nested loops cause the total execution count to scale at a rate of n×nn \times n.

TypeScript
// TypeScript
function printPairs(arr: T[]): void {
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length; j++) {
      console.log(arr[i], arr[j]);
    }
  }
}
Python
# Python
from typing import List, Any

def print_pairs(arr: List[Any]) -> None:
    for i in range(len(arr)):
        for j in range(len(arr)):
            print(arr[i], arr[j])
Java
// Java
public class Pairs {
    public static  void printPairs(T[] arr) {
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length; j++) {
                System.out.println(arr[i] + " " + arr[j]);
            }
        }
    }
}

Each execution step discards half of the remaining search space.

TypeScript
// TypeScript
function binarySearch(arr: number[], target: number): number {
  let left = 0;
  let right = arr.length - 1;
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}
Python
# Python
def binary_search(arr: list[int], target: int) -> int:
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        if arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1
Java
// Java
public class BinarySearch {
    public static int binarySearch(int[] arr, int target) {
        int left = 0;
        int right = arr.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2; // Prevents integer overflow
            if (arr[mid] == target) return mid;
            if (arr[mid] < target) left = mid + 1;
            else right = mid - 1;
        }
        return -1;
    }
}

Non-Symmetric Nested Loops and Multiple Variables

If nested loops iterate over independent datasets, their dimensions must be explicitly isolated. For example, if the outer loop scales with nn and the inner loop scales with mm, the time complexity evaluates to O(n×m)O(n \times m).

TypeScript
// TypeScript
function processTwoSizes(n: number, m: number): void {
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < m; j++) {
      // ...
    }
  }
}
Python
# Python
def process_two_sizes(n: int, m: int) -> None:
    for i in range(n):
        for j in range(m):
            pass
Java
// Java
public class NestedLoops {
    public static void processTwoSizes(int n, int m) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                // ...
            }
        }
    }
}

Evaluating an Implementation's Time Complexity

  1. Isolate repetitive control paths, such as loops or recursive jumps.
  2. Determine how internal iterations map to raw input variables (nn).
  3. Eliminate scaling coefficients and non-dominant factors to focus on asymptotic growth (e.g., O(3n)O(n)O(3n) \to O(n)).

Space Complexity Examples

O(1)O(1) Space – Constant Footprint

Time Complexity: O(n)O(n), Space Complexity: O(1)O(1). Memory usage is static because the primitive variable total allocates a constant amount of stack space.

TypeScript
// TypeScript
function sumArray(arr: number[]): number {
  let total = 0; // O(1) Space
  for (let i = 0; i < arr.length; i++) {
    total += arr[i];
  }
  return total;
}
Python
# Python
def sum_array(arr: list[int]) -> int:
    total = 0 # O(1) Space
    for num in arr:
        total += num
    return total
Java
// Java
public class ArraySum {
    public static int sumArray(int[] arr) {
        int total = 0; // O(1) Space
        for (int num : arr) {
            total += num;
        }
        return total;
    }
}

O(n)O(n) Space – Dynamic Allocations

Space Complexity evaluates to O(n)O(n) because the algorithm instantiates an internal data structure whose footprint grows in direct proportion to the parameter nn.

TypeScript
// TypeScript
function createArray(n: number): number[] {
  let result: number[] = [];
  for (let i = 0; i < n; i++) {
    result.push(i);
  }
  return result;
}
Python
# Python
def create_array(n: int) -> list[int]:
    result = []
    for i in range(n):
        result.append(i)
    return result
Java
// Java
import java.util.ArrayList;
import java.util.List;

public class ArrayFactory {
    public static List createArray(int n) {
        List result = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            result.add(i);
        }
        return result;
    }
}

Recursive O(2n)O(2^n) – Fibonacci (Unoptimized) and Stack Memory Bounds

Since every execution context triggers two downstream invocations, the time complexity exhibits exponential growth at O(2n)O(2^n).

Concurrently, the space complexity evaluates to O(n)O(n). At peak execution depth, the Call Stack must simultaneously preserve nn active frame references, meaning auxiliary memory allocation scales linearly with the recursion depth.

TypeScript
// TypeScript
function fib(n: number): number {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}
Python
# Python
def fib(n: int) -> int:
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)
Java
// Java
public class Fibonacci {
    public static int fib(int n) {
        if (n <= 1) return n;
        return fib(n - 1) + fib(n - 2);
    }
}

Essential Rules for Big O Simplification

Optimization RuleMathematical Application
Drop ConstantsEliminate constant modifiers: O(3n)O(n)O(3n) \to O(n)
Drop Non-Dominant TermsIsolate the highest algebraic degree: O(n2+n)O(n2)O(n^2 + n) \to O(n^2)
Multiply Nested ComplexitiesMultiply nested loops: Two loopsO(n×n)O(n2)\text{Two loops} \to O(n \times n) \to O(n^2)
Isolate Sequential PhasesRetain the maximum dominant block value: O(n)+O(logn)O(n)O(n) + O(\log n) \to O(n)
Evaluate Recurrence TreeResolve recursive equations via recursion trees (e.g., Memoized Fibonacci O(n)\to O(n))

Why Algorithmic Analysis Matters

Using Big O notation provides concrete engineering benefits:

  • Enables direct efficiency comparisons across competing algorithmic approaches.
  • Predicts resource consumption characteristics before exposing systems to large data volumes.
  • Isolates critical bottlenecks during code profiling and refactoring.

Quick Evaluation Matrix

Code Structural ProfileResulting Complexity Class
Sequential operations without iterative loops or active recursionO(1)O(1)
Single-pass iterative traversal loopO(n)O(n)
Dual-level nested iterative loopsO(n2)O(n^2)
Iterative steps that reduce the problem domain by half each cycleO(logn)O(\log n)
Recursive splits that fork into dual execution paths (binary tree pathing)O(2n)O(2^n)

Application in Technical Interviews and Production Environments

Practical ScenarioTime Complexity
Locating the maximum or minimum value in an unsorted collectionO(n)O(n)
Validating whether a string is a palindromeO(n)O(n)
Executing a Bubble Sort routineO(n2)O(n^2)
Executing a Quick Sort routineO(nlogn)O(n \log n)
Constructing a HashMap and executing data lookupsO(n)O(n) allocation + O(1)O(1) lookup
Resolving the nn-th Fibonacci termO(2n)O(2^n) via raw recursion or O(n)O(n) via Dynamic Programming

Real-World Architectural Deltas

Operational TaskInefficient Path (Sub-Optimal)Efficient Path (Optimal)
Querying datasetsO(n)O(n) Linear SearchO(logn)O(\log n) Binary Search
Sorting large collectionsO(n2)O(n^2) Bubble SortO(nlogn)O(n \log n) Merge / Quick Sort
Dictionary lookup operationsO(n)O(n) Collection ScanningO(1)O(1) HashSet Key Verification
Performance optimizationArbitrary code restructuringProfiling critical components via Big O tracking

Best, Average, and Worst-Case Analysis

While Big O primarily evaluates worst-case behavior, comprehensive system design requires evaluating all execution thresholds:

  • Best Case: In Bubble Sort, if the input array is already completely sorted, the algorithm terminates after a single confirmation sweep, scaling at O(n)O(n).
  • Average Case: Quick Sort exhibits a standard time complexity of O(nlogn)O(n \log n) across randomly distributed datasets.
  • Worst Case: If Quick Sort picks poor pivot points on already sorted arrays, execution can degrade to O(n2)O(n^2). Production systems mitigate this risk by utilizing randomized pivot selections or median-of-three selection heuristics.

Common Fallacies and Technical Misconceptions

Common MisconceptionCorrect Engineering Perspective
O(n)O(n) algorithms are always universally slow.Execution times depend on the actual data volume scale and implementation details.
Systems engineering only requires worst-case tracking.Best and average-case profiles offer critical insights based on domain architecture needs.
Big O yields real-world execution runtime metrics.It models the mathematical growth rate relative to data growth, not clock time.
Lower Big O classes run faster across all configurations.For small datasets, fixed overhead and runtime setup constants can skew actual processing speeds.
Sequential block complexities must always be summed.Only the dominant, highest-order complexity component that governs asymptotic scaling is retained.
O(n2)O(n^2) algorithms are universally broken and unusable.For small array instances or rare executions, clear and simple quadratic code blocks remain perfectly acceptable.

Conclusion

Big O notation does not track raw processing seconds; it maps the long-term trend and growth rate of an algorithm's resource consumption. Mastering Big O principles empowers engineers to:

  • Author implementations optimized for low CPU overhead and small memory footprints.
  • Architect high-throughput, horizontally scalable distributed systems.

When designing system logic, Big O functions as the definitive metric for evaluating structural scalability and finding potential computational bounds.