Coding Interview Prep · Lesson

Big-O Notation from Scratch

Understand why we care about asymptotic growth, how to drop constants and lower-order terms, and how to read Big-O at a glance.

Lesson 1 of 413 steps

Big-O Notation from Scratch is a free Coding Interview Prep lesson on CoddyKit. This is lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Coding Interview Prep learning path, and your progress syncs across the web and the CoddyKit app. The Coding Interview Prep course includes 4 lessons in total.

Why Measure Algorithm Efficiency?

Two programs can both be correct, yet one finishes in a blink and the other runs for hours. Time complexity describes how runtime grows as the input gets bigger.

# O(n) approach
def find_max_linear(nums):
    m = nums[0]
    for n in nums:
        if n > m: m = n
    return m

# O(n^2) approach (unnecessary double loop)
def find_max_quadratic(nums):
    for i in range(len(nums)):
        is_max = all(nums[i] >= nums[j] for j in range(len(nums)))
        if is_max: return nums[i]

print(find_max_linear([3, 1, 4, 1, 5, 9]))  # 9

Big-O: Asymptotic Upper Bound

Big-O describes the worst-case upper bound on how fast cost grows. The trick: drop constants and smaller terms, because only the dominant term matters at scale. See the code.

# T(n) = 3n^2 + 5n + 100 is O(n^2)
# because the n^2 term dominates for large n

# T(n) = 2n + 1000 is O(n)
# the constant 1000 becomes negligible

# Rule: drop constants and lower-order terms
# 5n^3 + 2n^2 + n + 1  =>  O(n^3)
# 100 * log(n) + n      =>  O(n)
print('O(n^2) example: counting iterations')
n = 1000
count = sum(1 for i in range(n) for j in range(n))
print(count)  # 1_000_000 = n^2

Common Complexity Classes

From fastest to slowest: O(1), O(log n), O(n), O(n log n), O(n^2), O(2^n), O(n!). Knowing these lets you pick the right approach before writing a single line.

import math

n = 1000
print(f'O(1):       {1}')
print(f'O(log n):   {int(math.log2(n))}')
print(f'O(n):       {n}')
print(f'O(n log n): {int(n * math.log2(n))}')
print(f'O(n^2):     {n**2}')
# O(2^n) for n=1000 is astronomically large
# O(n!) even larger

Dropping Constants: Why It Matters

Running 5n steps or 2n steps are both O(n) — constants depend on hardware, not the algorithm. Big-O drops them so you compare scaling on equal footing.

# Both are O(n) — different constants
def count_a(n):
    total = 0
    for i in range(n):   # n ops
        total += 1
    for i in range(n):   # n ops
        total += 1
    return total  # T(n) = 2n  =>  O(n)

def count_b(n):
    total = 0
    for i in range(5 * n):  # 5n ops
        total += 1
    return total  # T(n) = 5n  =>  O(n)

print(count_a(10), count_b(10))  # 20 50

Best, Average, and Worst Cases

Big-O is the worst case; Omega is the best case; Theta is a tight bound on both. When an interviewer asks "the complexity," they almost always mean worst case.

def linear_search(nums, target):
    for i, n in enumerate(nums):
        if n == target:
            return i  # best case: target at index 0 => O(1)
    return -1         # worst case: not found => O(n)

# Best case O(1): target is first element
print(linear_search([5,1,2,3], 5))   # 0

# Worst case O(n): target not in list
print(linear_search([1,2,3,4], 9))   # -1

O(log n): Halving the Search Space

An algorithm is O(log n) when it halves the input each step, like binary search. Even for a billion items that is only about 30 steps — incredibly fast. See the code.

def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    steps = 0
    while lo <= hi:
        steps += 1
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid, steps
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1, steps

import math
arr = list(range(1000))
idx, s = binary_search(arr, 999)
print(f'Found at {idx} in {s} steps (log2(1000)~={math.log2(1000):.1f})')

O(n log n): Sorting Lower Bound

Any comparison sort needs at least O(n log n) in the worst case — a real math lower bound. So sort-then-scan is O(n log n) overall, not O(n^2). The code shows merge sort.

# Merge sort: O(n log n)
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(a, b):
    res, i, j = [], 0, 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]: res.append(a[i]); i+=1
        else:             res.append(b[j]); j+=1
    return res + a[i:] + b[j:]

print(merge_sort([5,2,8,1,9,3]))  # [1,2,3,5,8,9]

Amortised Complexity

Amortized analysis averages cost over many operations. Python's append is O(1) amortized: usually instant, with the rare O(n) resize spread thin across all appends.

# Dynamic array append is O(1) amortised
import sys

lst = []
capacities = []
for i in range(16):
    lst.append(i)
    capacities.append(sys.getsizeof(lst))

# Size jumps show reallocation events
for i, c in enumerate(capacities):
    if i > 0 and capacities[i] != capacities[i-1]:
        print(f'Realloc at i={i}, new size={c} bytes')

Recognising Complexity in Code

A quick rule: count loops. One loop is O(n), two nested is O(n^2), a halving loop is O(log n). Independent passes add; only nested loops multiply. See the code.

# Two independent passes: O(n) + O(n) = O(n)
def two_passes(nums):
    total = sum(nums)           # O(n)
    mean = total / len(nums)
    diffs = [abs(n - mean) for n in nums]  # O(n)
    return max(diffs)           # O(n)
# Overall: O(n) -- NOT O(n^2)

# Nested loops: O(n) * O(n) = O(n^2)
def all_pairs(nums):
    pairs = []
    for i in range(len(nums)):       # O(n)
        for j in range(i+1, len(nums)): # O(n)
            pairs.append((nums[i], nums[j]))
    return pairs  # O(n^2)

Space Complexity Basics

Space complexity tracks the extra memory you use beyond the input. In-place reversal is O(1); a hash map is O(n). When you trade time for space, always state both.

# O(1) space: reverse in-place
def reverse_inplace(arr):
    l, r = 0, len(arr) - 1
    while l < r:
        arr[l], arr[r] = arr[r], arr[l]
        l += 1; r -= 1

# O(n) space: create reversed copy
def reverse_copy(arr):
    return arr[::-1]

a = [1, 2, 3, 4, 5]
reverse_inplace(a)
print(a)  # [5, 4, 3, 2, 1]

Talking Complexity in Interviews

Always volunteer the complexity without being asked: "This is O(n log n) time, O(n) space." Then offer a faster option. That habit signals real seniority.

# Example of explaining complexity step by step
def two_sum(nums, target):
    # O(n) time: one pass through nums
    # O(n) space: hash map stores up to n elements
    seen = {}  # value -> index
    for i, n in enumerate(nums):
        complement = target - n
        if complement in seen:   # O(1) lookup
            return [seen[complement], i]
        seen[n] = i
    return []

print(two_sum([2, 7, 11, 15], 9))  # [0, 1]

Quick Check

Quick check — show what you have absorbed about Big-O and complexity classes. One question, you have got this. 🎯

Lesson Recap

Recap: Big-O is worst-case growth with constants dropped, you know the classes from O(1) to O(n!), and independent loops add while nested loops multiply.

Free to start

Learn Coding Interview Prep with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
90
Lessons
360

Frequently Asked Questions

Is the “Big-O Notation from Scratch” lesson free?

Yes — the full text of “Big-O Notation from Scratch” is free to read here on the web. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Coding Interview Prep course, upgrade to CoddyKit PRO. The Coding Interview Prep course includes 4 lessons in total.

What will I learn in “Big-O Notation from Scratch”?

Understand why we care about asymptotic growth, how to drop constants and lower-order terms, and how to read Big-O at a glance. You practise Coding Interview Prep with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Coding Interview Prep?

No prior experience is required. Coding Interview Prep on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 1 of 4.

How long does the “Big-O Notation from Scratch” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Coding Interview Prep lesson?

Yes. Every Coding Interview Prep lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Big-O Notation from Scratch
  2. Analysing Loops and Nested Loops
  3. Recursion and the Recursion Tree Method
  4. Space Complexity and Trade-offs
← Back to Coding Interview Prep