Quiz 2

02. Algorithm Analysis & Big-O Notation

2001 words
10 min read
Python Week 1: the first filter for runtime behavior
Visual companion
Python
Type and operator map

Python Week 1: the first filter for runtime behavior

View
Revision summary

What this note is really saying

Short form

# 02. Algorithm Analysis & Big-O Notation > **What problem does this solve?** When you write a program, how do you know if it will finish in 1 second or 1 year?

02. Algorithm Analysis & Big-O Notation

What problem does this solve? When you write a program, how do you know if it will finish in 1 second or 1 year? Algorithm analysis gives us a mathematical language to talk about how runtime grows with input size — without running the code on any specific machine.

1. Why Analyze Algorithms?

Mental Model

Imagine you're sorting exam papers:
  • Naïve method (selection sort): Find the max, put it aside, repeat. For 1000 papers, you scan 1000 times = ~1 million operations.
  • Smart method (merge sort): Split into piles, sort each pile, merge. For 1000 papers, ~10,000 operations. The difference isn't a constant factor — it's the rate of growth. For 1 billion papers, naïve takes 10^18 operations (centuries), smart takes ~30 billion operations (minutes on a supercomputer). We measure time complexity (how runtime grows) and space complexity (how memory grows).

Key Parameters

  • Input size (n) — length of list, number of vertices, number of digits
  • Basic operation — comparison, assignment, arithmetic operation
  • Worst case — the input that makes the algorithm slowest

2. Asymptotic Notation — Big O, Ω, Θ

Big O — Upper Bound (O(g(n)))

Intuition: (f(n) = O(g(n))) means (f(n)) grows no faster than (g(n)). Formal Definition: (f(n) = O(g(n))) if there exist constants (c > 0) and (n_0 \ge 0) such that:
f(n)cg(n)for all nn0f(n) \le c \cdot g(n) \quad \text{for all } n \ge n_0
python
# runnable
# Example: f(n) = 100n + 5 is O(n^2)
# Because: 100n + 5 ≤ 100n + 5n = 105n ≤ 105n² for n ≥ 1
# So: c = 105, n₀ = 1
# But it's also O(n)!
# 100n + 5 ≤ 101n for n ≥ 5
# So: c = 101, n₀ = 5
# Tightest bound: O(n) — we always want the tightest!

Omega — Lower Bound (\Omega(g(n)))

Intuition: (f(n) = \Omega(g(n))) means (f(n)) grows at least as fast as (g(n)). Formal Definition: (f(n) = \Omega(g(n))) if there exist constants (c > 0) and (n_0 \ge 0) such that:
f(n)cg(n)for all nn0f(n) \ge c \cdot g(n) \quad \text{for all } n \ge n_0

Theta — Tight Bound (\Theta(g(n)))

Intuition: (f(n) = \Theta(g(n))) means (f(n)) grows at the same rate as (g(n)). Formal Definition: (f(n) = \Theta(g(n))) if (f(n) = O(g(n))) AND (f(n) = \Omega(g(n))).
python
# runnable
# Example: f(n) = n(n-1)/2 is Θ(n²)
# Upper bound: n(n-1)/2 ≤ n²/2 ≤ n² for n ≥ 1
# Lower bound: n(n-1)/2 ≥ n²/4 for n ≥ 2
# So c₁ = 1/4, c₂ = 1, n₀ = 2

Growth Hierarchy (from slowest to fastest)

(Diagram)
NotationNameExamplen=100n=10,000
(O(1))ConstantArray access1 op1 op
(O(\log n))LogarithmicBinary search~7 ops~14 ops
(O(n))LinearLinear search100 ops10,000 ops
(O(n \log n))LinearithmicMerge sort664 ops132,877 ops
(O(n²))QuadraticSelection sort10,000 ops100,000,000 ops
(O(2ⁿ))ExponentialFibonacci (naïve)2¹⁰⁰ ops
(O(n!))FactorialTraveling salesman100! ops

3. Analyzing Iterative Programs

Rule 1: Single loop → O(n)

python
# runnable
def find_max(L):
    """O(n) — one pass through the list."""
    max_val = L[0]          # 1 operation
    for x in L:             # n iterations
        if x > max_val:     # 1 comparison per iteration
            max_val = x     # at most 1 assignment
    return max_val
# Total: 1 + n * (1 + up to 1) + 1 = O(n)

Rule 2: Nested loops → O(n²)

python
# runnable
def has_duplicates(L):
    """O(n²) — compare every pair."""
    n = len(L)
    for i in range(n):           # n iterations
        for j in range(i + 1, n):  # n-i-1 iterations
            if L[i] == L[j]:      # 1 comparison
                return True
    return False
# Total comparisons: n(n-1)/2 = O(n²)

Rule 3: Halving loop → O(log n)

python
# runnable
def count_bits(n):
    """O(log n) — n halves each iteration."""
    count = 0
    while n > 0:
        n = n // 2
        count += 1
    return count
print(count_bits(32))   # 6 (32→16→8→4→2→1→0)
print(count_bits(1000)) # 10
# Why? n halves each time → number of iterations = log₂(n) + 1

Rule 4: Loop with constant work → Multiply

python
# runnable
def matrix_multiply(A, B):
    """O(mnp) — three nested loops for matrix multiplication."""
    m, n = len(A), len(A[0])
    p = len(B[0])
    C = [[0 for _ in range(p)] for _ in range(m)]
    for i in range(m):       # m iterations
        for j in range(p):   # p iterations
            for k in range(n):  # n iterations
                C[i][j] += A[i][k] * B[k][j]
    return C
# If all are n: O(n³)

Rule 5: Sum of loop iterations

python
# runnable
def print_triangle(n):
    """O(n²) — rows decrease linearly."""
    for i in range(n, 0, -1):  # n, n-1, n-2, ..., 1
        print('*' * i)
# Total operations: n + (n-1) + ... + 1 = n(n+1)/2 = O(n²)

4. Analyzing Recursive Programs

Step 1: Write the Recurrence Relation

T(n)={cif n1aT(n/b)+f(n)if n>1T(n) = \begin{cases} c & \text{if } n \le 1 \\ a \cdot T(n/b) + f(n) & \text{if } n > 1 \end{cases}
Where:
  • (a) = number of recursive calls
  • (b) = fraction of input each call processes
  • (f(n)) = work done outside recursion (combining/partitioning)

Common Recurrences

RecurrenceAlgorithmComplexity
(T(n) = T(n-1) + 1)Factorial (recursive)(O(n))
(T(n) = T(n-1) + n)Sum of n numbers(O(n^2))
(T(n) = T(n/2) + 1)Binary Search(O(\log n))
(T(n) = 2T(n/2) + n)Merge Sort(O(n \log n))
(T(n) = 2T(n/2) + 1)Tree Traversal(O(n))
(T(n) = 2T(n-1) + 1)Towers of Hanoi(O(2^n))

Step-by-Step: Solving by Unwinding

Example: (T(n) = T(n/2) + 1) (Binary Search)
pseudo
T(n) = T(n/2) + 1
     = [T(n/4) + 1] + 1 = T(n/4) + 2
     = [T(n/8) + 1] + 2 = T(n/8) + 3
     = ...
     = T(n/2^k) + k
When (n/2^k = 1), (k = \log_2 n). So:
T(n)=T(1)+logn=1+logn=O(logn)T(n) = T(1) + \log n = 1 + \log n = O(\log n)
Example: (T(n) = 2T(n/2) + n) (Merge Sort)
pseudo
T(n) = 2T(n/2) + n
     = 2[2T(n/4) + n/2] + n = 4T(n/4) + 2n
     = 4[2T(n/8) + n/4] + 2n = 8T(n/8) + 3n
     = ...
     = 2^k T(n/2^k) + kn
When (n/2^k = 1), (k = \log_2 n). So:
T(n)=nT(1)+nlogn=n+nlogn=O(nlogn)T(n) = n \cdot T(1) + n \log n = n + n \log n = O(n \log n)

5. Sum and Max Rules

Sum Rule: If (f_1(n) = O(g_1(n))) and (f_2(n) = O(g_2(n))), then:
f1(n)+f2(n)=O(max(g1(n),g2(n)))f_1(n) + f_2(n) = O(\max(g_1(n), g_2(n)))
python
# runnable
# Phase 1: Sort (O(n log n))
# Phase 2: Search (O(log n))
# Total: O(n log n) — the larger term dominates
def process(L):
    L.sort()                          # O(n log n)
    target = 42
    # Binary search
    left, right = 0, len(L) - 1
    while left <= right:              # O(log n)
        mid = (left + right) // 2
        if L[mid] == target:
            return True
        elif L[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return False
# Total: O(n log n) + O(log n) = O(n log n)
Product Rule: (f_1(n) \cdot f_2(n) = O(g_1(n) \cdot g_2(n)))

6. Common Pitfalls

python
# PITFALL 1: O(n) + O(n) ≠ O(2n) ... it's O(n)
def two_loops(L):
    for x in L:    # O(n)
        print(x)
    for y in L:    # O(n)
        print(y)
# Total: O(n) + O(n) = O(n)
# PITFALL 2: Hidden O(n) operations
def bad_duplicate_check(L):
    result = []
    for x in L:              # n iterations
        if x not in result:  # 'not in' on list is O(n)!
            result.append(x)
    return result
# Total: O(n²) — not O(n)!
# PITFALL 3: Python slicing creates copies
def bad_average(L):
    if len(L) <= 1:
        return L
    # L[:len(L)//2] creates a copy — O(n)!
    left_sum = sum(L[:len(L)//2])  # O(n)
    right_sum = sum(L[len(L)//2:]) # O(n)
    return (left_sum + right_sum) / len(L)
# Total: O(n) but creates O(n) memory
# PITFALL 4: Integer input size
def is_prime(n):
    """n is the number itself, not number of digits."""
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False
    return True
# If n is the magnitude, input size = log₂(n) bits
# Complexity: O(√n) = O(2^(bits/2)) — EXPONENTIAL in input size!

7. Complexity Comparison Table

AlgorithmBest CaseAverage CaseWorst CaseSpace
Linear Search(O(1))(O(n))(O(n))(O(1))
Binary Search(O(1))(O(\log n))(O(\log n))(O(1))
Selection Sort(O(n²))(O(n²))(O(n²))(O(1))
Insertion Sort(O(n))(O(n²))(O(n²))(O(1))
Merge Sort(O(n \log n))(O(n \log n))(O(n \log n))(O(n))
Quick Sort(O(n \log n))(O(n \log n))(O(n²))(O(\log n))
Heap Sort(O(n \log n))(O(n \log n))(O(n \log n))(O(1))

Practice Questions

Q1. What is the tightest Big-O complexity of this code?
python
def f(n):
    s = 0
    for i in range(n):
        for j in range(i, n):
            s += 1
    return s
Q2. Show that (f(n) = 3n^2 + 2n + 1) is (O(n^2)) by finding constants (c) and (n_0). Q3. What's the complexity of this recursive function?
python
def g(n):
    if n <= 1:
        return 1
    return g(n-1) + g(n-1) + 1
Q4. Solve the recurrence: (T(n) = 4T(n/2) + n) Q5. Compare the growth rates: (n \log n) vs (n^{1.5}) vs (2^n) vs (n^3). Order from slowest to fastest growth. Q6. What is the complexity of accessing the middle element of a linked list? Q7. Is (n^2 + 100n + 1000) equal to (\Theta(n^2))? Prove it. Q8. What's wrong with this complexity analysis?
python
def find(L, target):
    for x in L:
        if x == target:
            return True
    return False
# Claimed: O(log n) because "we stop early if found"
Q9. If (T(n) = T(n/2) + n), what is the complexity? What common algorithm has this recurrence? Q10. Give an (O(n)) algorithm to check if a string has all unique characters (without using extra data structures).
Answers
A1. (O(n^2)). The inner loop runs n, n-1, n-2, ..., 1 times. Sum = n(n+1)/2 = O(n²).
A2. (3n^2 + 2n + 1 \le 3n^2 + 2n^2 + n^2 = 6n^2) for (n \ge 1). So (c = 6), (n_0 = 1).
A3. (T(n) = 2T(n-1) + 1). Unwinding: (T(n) = 2^n - 1 = O(2^n)).
A4. Unwinding:
pseudo
T(n) = 4T(n/2) + n
     = 4[4T(n/4) + n/2] + n = 16T(n/4) + 2n + n
     = 16[4T(n/8) + n/4] + 3n = 64T(n/8) + 4n + 2n + n
     = ...
     = 4^k T(n/2^k) + n(2^k - 1)
When (n/2^k = 1), (k = \log_2 n), (4^k = n^2). (T(n) = n² \cdot T(1) + n(2^{\log n} - 1) = n² + n² - n = O(n²))
A5. Slowest to fastest: (n \log n < n^{1.5} < n^3 < 2^n)
A6. (O(n)) — linked lists have no random access; you must traverse from the head.
A7. Yes. Upper bound: (n^2 + 100n + 1000 \le 3n^2) for n ≥ 101. Lower bound: (n^2 + 100n + 1000 \ge n^2) for all n ≥ 0. So (c_1 = 1, c_2 = 3, n_0 = 101).
A8. Worst-case analysis must consider the worst-case input. In worst case, target isn't in the list, so we scan all n elements — O(n). Best-case isn't used as the standard measure.
A9. (T(n) = T(n/2) + n). Unwinding:
pseudo
T(n) = T(n/2) + n
     = T(n/4) + n/2 + n
     = T(n/8) + n/4 + n/2 + n
     = ...
     = T(1) + n(1 + 1/2 + 1/4 + ...) = 1 + 2n = O(n)
This is Quick Select / Fast Select (finding kth smallest element).
A10.
python
def all_unique(s):
    # Without extra space: O(n²) compare all pairs
    for i in range(len(s)):
        for j in range(i + 1, len(s)):
            if s[i] == s[j]:
                return False
    return True
Document outline

Keep your place and jump directly to a heading.

Table of Contents
System Normal // Awaiting Context

Intelligence Hub

Navigate the knowledge graph to generate context. The Hub adapts dynamically to surface backlinks, related notes, and metadata insights.