Skip to main content

Command Palette

Search for a command to run...

Big O Notation Explained: Algorithm Complexity for Beginners

Learn: Big O Notation Explained: Algorithm Complexity for Beginners

Updated
7 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Big O Notation Explained: Algorithm Complexity for Beginners

Time and space complexity made simple

What Is It

Big O notation is a mathematical framework for describing how an algorithm's performance scales as input size grows. It answers the fundamental question: "How does my code slow down when I have more data?"

Rather than measuring execution time in seconds (which varies by hardware), Big O expresses the relationship between input size and operations performed. This universal language lets developers compare algorithms objectively.

Key insight: Big O describes worst-case scenario performance, giving you a performance guarantee.

The Notation Explained

Big O uses O(n) syntax where n represents input size:

  • O(1) - Constant time (always same speed)
  • O(log n) - Logarithmic (halves problem each step)
  • O(n) - Linear (proportional to input)
  • O(n log n) - Linearithmic (efficient sorting)
  • O(n²) - Quadratic (nested loops)
  • O(2ⁿ) - Exponential (recursive problems)
  • O(n!) - Factorial (permutations)

Why It Matters

Understanding Big O prevents catastrophic performance failures:

Real-world scenario: Your app works fine with 100 users but crashes at 10,000. Why? You likely used an O(n²) algorithm when O(n log n) was available.

Business Impact

ScenarioAlgorithm1,000 items1,000,000 items
O(n)Linear search1ms1s
O(n²)Bubble sort1s1,000,000s (11 days!)
O(log n)Binary search0.01ms0.02ms

The difference between O(n) and O(n²) at scale is the difference between a responsive app and a frozen one.

Core Concepts Explained

Time Complexity

Time complexity measures how many operations an algorithm performs relative to input size.

# O(1) - Constant Time
def get_first_element(arr):
    return arr[0]  # Always 1 operation, regardless of array size

# O(n) - Linear Time
def find_max(arr):
    max_val = arr[0]
    for num in arr:  # Loop runs n times
        if num > max_val:
            max_val = num
    return max_val

# O(n²) - Quadratic Time
def bubble_sort(arr):
    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]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

# O(log n) - Logarithmic Time
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2  # Halves search space each iteration
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

Space Complexity

Space complexity measures how much additional memory an algorithm uses relative to input size.

# O(1) - Constant Space
def sum_array(arr):
    total = 0  # Only one variable, regardless of input size
    for num in arr:
        total += num
    return total

# O(n) - Linear Space
def create_doubled_array(arr):
    result = []  # New array proportional to input size
    for num in arr:
        result.append(num * 2)
    return result

# O(n²) - Quadratic Space
def create_matrix(n):
    matrix = []
    for i in range(n):  # n rows
        row = []
        for j in range(n):  # n columns
            row.append(0)
        matrix.append(row)
    return matrix

# O(log n) - Logarithmic Space (Recursion Depth)
def binary_search_recursive(arr, target, left, right):
    if left > right:
        return -1
    mid = (left + right) // 2
    if arr[mid] == target:
        return mid
    elif arr[mid] < target:
        return binary_search_recursive(arr, target, mid + 1, right)
    else:
        return binary_search_recursive(arr, target, left, mid - 1)
    # Call stack depth: O(log n)

Dropping Constants and Non-Dominant Terms

Big O ignores constants and focuses on growth rate:

# O(2n) simplifies to O(n)
def process_twice(arr):
    for num in arr:  # n operations
        print(num)
    for num in arr:  # n operations
        print(num)

# O(n² + n) simplifies to O(n²)
def nested_with_linear(arr):
    for i in range(len(arr)):  # n iterations
        for j in range(len(arr)):  # n iterations
            print(arr[i], arr[j])  # n² operations
    for num in arr:  # n operations
        print(num)
    # n² dominates, so O(n²)

Practical Examples (Code)

Example 1: Finding Duplicates

# Approach 1: Brute Force - O(n²) time, O(1) space
def has_duplicates_brute(arr):
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] == arr[j]:
                return True
    return False

# Approach 2: Hash Set - O(n) time, O(n) space
def has_duplicates_optimized(arr):
    seen = set()
    for num in arr:
        if num in seen:  # O(1) lookup
            return True
        seen.add(num)
    return False

# Performance comparison:
# Array of 10,000 items:
# Brute force: ~100,000,000 comparisons
# Hash set: ~10,000 lookups

Example 2: Sorting Algorithms

# Bubble Sort - O(n²) time, O(1) space
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]
    return arr

# Merge Sort - O(n log n) time, O(n) space
def merge_sort(arr):
    if len(arr) <= 1:
        return arr

    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])

    return merge(left, right)

def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result.extend(left[i:])
    result.extend(right[j:])
    return result

# For 10,000 items:
# Bubble sort: ~100,000,000 operations
# Merge sort: ~130,000 operations

Example 3: Recursive Fibonacci

# Naive Recursion - O(2ⁿ) time, O(n) space (call stack)
def fibonacci_naive(n):
    if n <= 1:
        return n
    return fibonacci_naive(n - 1) + fibonacci_naive(n - 2)
    # Exponential explosion: fib(40) requires billions of calls

# Memoization - O(n) time, O(n) space
def fibonacci_memo(n, memo=None):
    if memo is None:
        memo = {}
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
    return memo[n]

# Dynamic Programming - O(n) time, O(n) space
def fibonacci_dp(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]

# Performance for fib(40):
# Naive: ~2 billion calls (unusable)
# Memoization: 40 calls (instant)
# DP: 40 iterations (instant)

When to Use

Use Big O When:

  • Comparing algorithms - Choose the one with better complexity
  • Predicting scalability - Will this work with 1 million items?
  • Identifying bottlenecks - Where does performance degrade?
  • Making trade-offs - Is extra memory worth faster execution?

Don't Rely Solely on Big O When:

  • Constants matter (O(n) with coefficient 1000 vs 1)
  • Working with small datasets (overhead dominates)
  • Real-time constraints require actual benchmarking
  • Hardware-specific optimizations are critical

Common Patterns

Pattern Recognition Guide

# O(1) - Direct access, simple operations
arr[0]
dict_lookup[key]
math_operation = 5 + 3

# O(log n) - Divide and conquer, binary search
binary_search()
balanced_tree_operations()

# O(n) - Single loop through data
for item in arr:
    process(item)

# O(n log n) - Efficient sorting, divide and conquer with merging
merge_sort()
quick_sort()  # average case
heap_sort()

# O(n²) - Nested loops
for i in arr:
    for j in arr:
        compare(i, j)

# O(2ⁿ) - Recursive without memoization, all subsets
fibonacci_naive()
generate_all_subsets()

# O(n!) - Permutations, all orderings
generate_all_permutations()
traveling_salesman_brute_force()

Practice Exercises

Exercise 1: Analyze This Code

def mystery_function(arr):
    for i in range(len(arr)):
        for j in range(len(arr)):
            if arr[i] == arr[j] and i != j:
                return True
    return False

Question: What's the time and space complexity?

Answer: O(n²) time, O(1) space. Two nested loops iterate through the array.


Exercise 2: Optimize This

def find_pairs_sum(arr, target):
    pairs = []
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] + arr[j] == target:
                pairs.append((arr[i], arr[j]))
    return pairs

Optimized Solution:

def find_pairs_sum_optimized(arr, target):
    seen = set()
    pairs = set()
    for num in arr:
        complement = target - num
        if complement in seen:
            pairs.add((min(num, complement), max(num, complement)))
        seen.add(num)
    return list(pairs)
# O(n²) → O(n) time, O(n) space

Exercise 3: Choose the Best Algorithm

You need to search a sorted array of 1 million items frequently.

  • Option A: Linear search O(n)
  • Option B: Binary search O(log n)

Answer: Binary search. For 1 million items: O(n) = 1,000,000 operations vs O(log n) ≈ 20 operations.

Summary

Big O notation is your algorithmic compass. It guides you toward scalable solutions and away from performance cliffs.

Key Takeaways

  1. Big O measures growth rate, not absolute time
  2. Worst-case analysis provides performance guarantees
  3. Common complexities from best to worst: O(1) → O(log n) → O(n) → O(n log n) → O(n²) → O(2ⁿ) → O(n!)
  4. Time-space trade-offs are real—optimize based on constraints
  5. Practice pattern recognition to quickly identify complexity
  6. Memoization and dynamic programming transform exponential to polynomial
  7. Constants matter at scale, but Big O ignores them for growth analysis

Next Steps

  • Implement sorting algorithms and compare their complexities
  • Solve LeetCode problems while analyzing Big O
  • Profile your code to see Big O theory in practice
  • Build intuition by predicting complexity before coding

Remember: Writing fast code isn't about micro-optimizations—it's about choosing the right algorithm. Big O teaches you how.


Master Big O, and you'll write code that scales from 10 users to 10 million.