DSA Notes
Master time complexity analysis through 25 carefully chosen worked examples, from single loops to complex recursive algorithms. Each example provides step-by-step analysis, the final Big O answer, and key insights. An essential practice resource for interviews and deeper understanding.
Introduction
Reading about Big O notation and understanding the theory is one thing. Being able to look at any piece of code and immediately determine its time complexity is a different skill — one that only develops through practice.
This lesson provides 25 worked examples, organized from simplest to most complex. For each example, you will see:
- The code
- A step-by-step analysis
- The final Big O answer
- A key insight or pattern to remember
Work through each example yourself before reading the solution. This active engagement is what builds genuine skill.
Example 2 — Print All Pairs (Including Self)
Analysis:
- Outer loop: n iterations
- Inner loop: n iterations for each outer iteration
- Total pairs: n × n = n²
- Each pair: O(1) work
Answer: O(n²)
Key insight: Nested loops over the same n-element collection = O(n²).
Example 3 — Print Unique Pairs (Triangle)
Analysis:
- i=0: j runs from 1 to n-1 → n-1 iterations
- i=1: j runs from 2 to n-1 → n-2 iterations
- i=2: j runs from 3 to n-1 → n-3 iterations
- ...
- i=n-2: j runs from n-1 to n-1 → 1 iteration
- Total: (n-1) + (n-2) + ... + 1 = n(n-1)/2
Answer: O(n²) — the constant factor 1/2 is dropped
Key insight: Triangular iterations (j starts after i) still give O(n²), not O(n²/2).
Example 4 — Two Sequential Loops
def two_passes(array):
# First pass — O(n)
for x in array:
print(x)
# Second pass — O(n)
for x in array:
print(x * 2)Analysis:
- First loop: n iterations
- Second loop: n iterations
- Total: n + n = 2n
Answer: O(n) — the constant 2 is dropped
Key insight: Sequential (non-nested) loops add their complexities. Constants disappear.
Example 5 — Loop with Halving
def count_halvings(n):
count = 0
i = n
while i > 0:
i = i // 2 # Halve i each iteration
count += 1
return countAnalysis:
- Iteration 1: i = n
- Iteration 2: i = n/2
- Iteration 3: i = n/4
- ...
- After k iterations: i = n/2^k
- Loop ends when n/2^k < 1, i.e., 2^k > n, i.e., k > log₂(n)
Answer: O(log n)
Key insight: Any loop that halves (or multiplies by any constant < 1) the controlling variable = O(log n).
Example 6 — Loop with Doubling
def count_doublings(n):
count = 0
i = 1
while i < n:
i = i * 2 # Double i each iteration
count += 1
return countAnalysis:
- Iteration 1: i = 1
- Iteration 2: i = 2
- Iteration 3: i = 4
- ...
- After k iterations: i = 2^k
- Loop ends when 2^k ≥ n, i.e., k ≥ log₂(n)
Answer: O(log n)
Key insight: Same as halving but in the other direction. Both doubling and halving give O(log n).
Intermediate Examples — Mixed Patterns
Example 7 — Mixed Complexity
Analysis:
- Block A: O(n²)
- Block B: O(n)
- Block C: O(log n)
- Total: O(n²) + O(n) + O(log n)
- Dominant term: n²
Answer: O(n²)
Key insight: When adding complexities, keep only the dominant term.
Example 8 — Inner Loop Depends on Outer
def triangle_sum(n):
total = 0
for i in range(n):
for j in range(i): # j goes from 0 to i-1
total += 1
return totalAnalysis:
- i=0: 0 iterations (j from 0 to -1, loop doesn't run)
- i=1: 1 iteration
- i=2: 2 iterations
- ...
- i=n-1: n-1 iterations
- Total: 0 + 1 + 2 + ... + (n-1) = n(n-1)/2
Answer: O(n²)
Key insight: Even when the inner loop bound depends on the outer variable, if it results in a triangular sum, the complexity is O(n²).
Example 9 — Logarithmic Inner Loop
def log_inner(array):
n = len(array)
for i in range(n): # n iterations
j = 1
while j < n: # log n iterations
j *= 2Analysis:
- Outer loop: n iterations
- Inner loop: log n iterations (doubling)
- Total: n × log n
Answer: O(n log n)
Key insight: A loop of n iterations, each containing a log n inner loop = O(n log n). This is the pattern of many efficient algorithms.
Example 10 — Constant Work in Nested Function
def outer_calls_binary_search(array):
"""For each element, check if it exists in the sorted array."""
sorted_arr = sorted(array) # O(n log n) preprocessing
for x in array: # n iterations
result = binary_search(sorted_arr, x) # O(log n) each
print(result)
# binary_search is O(log n)Analysis:
- Preprocessing: O(n log n)
- Loop: n × O(log n) = O(n log n)
- Total: O(n log n) + O(n log n) = O(2 × n log n)
Answer: O(n log n)
Key insight: Calling an O(log n) function inside an O(n) loop = O(n log n).
Example 11 — Different Input Sizes
def process_two_arrays(a, b):
"""
a and b are arrays with different sizes m and n.
"""
for x in a: # m iterations
for y in b: # n iterations
print(x, y)Analysis:
- This is NOT O(n²)
- The inputs have different sizes: m = len(a), n = len(b)
- Total: m × n iterations
Answer: O(m × n)
If m = n, this simplifies to O(n²). But if they are different (e.g., m = 10, n = 1,000,000), calling it O(n²) is wrong.
Key insight: Use different variables for genuinely different input sizes.
Example 12 — Fibonacci without Memoization
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)Analysis:
Each call makes 2 calls. Depth = n. Total nodes ≈ 2ⁿ.
Answer: O(2ⁿ)
Key insight: Two recursive calls on inputs of size n-1 and n-2 (not n/2!) = exponential.
Example 13 — Fibonacci with Memoization
Analysis:
- Each unique value of n is computed exactly once
- Unique values: 0, 1, 2, ..., n → n+1 values
- Each computation: O(1) (just one addition after sub-results are cached)
- Total: (n+1) × O(1)
Answer: O(n)
Key insight: Memoization eliminates recomputation. The complexity becomes (number of unique subproblems) × (work per subproblem).
Advanced Examples — Recursion and Complex Patterns
Example 14 — Merge Sort
Recurrence: T(n) = 2T(n/2) + n
Analysis via recursion tree:
| Level 0: 1 subproblem of size n | n work |
| Level 1: 2 subproblems of n/2 | n/2 + n/2 = n work |
| Level 2: 4 subproblems of n/4 | n work |
| Level log n: n subproblems of 1 | n × O(1) = n work |
| Levels | log n + 1 |
| Work per level | n |
| Total | n × (log n + 1) ≈ n log n |
Answer: O(n log n)
Example 15 — Power Function (Naive)
def power_naive(base, exponent):
if exponent == 0:
return 1
return base * power_naive(base, exponent - 1)Analysis:
- Each call reduces exponent by 1
- Starting from exponent, ending at 0
- Total calls: exponent + 1
Answer: O(n) where n = exponent
Example 16 — Power Function (Fast Exponentiation)
def power_fast(base, exponent):
if exponent == 0:
return 1
if exponent % 2 == 0:
half = power_fast(base, exponent // 2) # Halves exponent!
return half * half
else:
return base * power_fast(base, exponent - 1)Analysis:
- When exponent is even: recurse with exponent/2 (halving)
- When exponent is odd: one decrement, then next call will halve
- Maximum halvings from n to 1: log₂(n)
- Each halving can be preceded by at most one decrement
Answer: O(log n) — Binary Exponentiation
Key insight: Halving the input at each recursive step = O(log n), even with occasional +1 decrements.
Example 17 — Tower of Hanoi
def hanoi(n, source, target, auxiliary):
if n == 1:
print(f"Move disk 1 from {source} to {target}")
return
hanoi(n - 1, source, auxiliary, target) # Move n-1 disks to auxiliary
print(f"Move disk {n} from {source} to {target}")
hanoi(n - 1, auxiliary, target, source) # Move n-1 disks to targetRecurrence: T(n) = 2T(n-1) + 1
Solving by expansion:
Answer: O(2ⁿ)
Key insight: Two recursive calls on input n-1 (not n/2) = exponential growth.
Example 18 — All Subsets of an Array
Analysis:
- At each index position, we make 2 choices (include or exclude)
- n positions × 2 choices each = 2ⁿ total subsets
- Each subset takes O(n) to process (creating a copy)
Answer: O(n × 2ⁿ)
Key insight: Generating all subsets requires O(2ⁿ) time — you cannot do it faster.
Example 19 — All Permutations
Analysis:
- At position 0: n choices
- At position 1: n-1 choices (already placed one element)
- At position 2: n-2 choices
- ...
- Total: n × (n-1) × (n-2) × ... × 1 = n!
Answer: O(n! × n) — n! permutations, each taking O(n) to record
Key insight: Permutations grow factorially. Even n=20 gives over 2 quadrillion permutations.
Example 20 — Graph BFS (Breadth-First Search)
Analysis:
- Each vertex is enqueued and dequeued at most once: O(V) total
- For each vertex, we process all its edges: total edge processing = O(E)
- Total: O(V + E)
Answer: O(V + E) where V = vertices, E = edges
Key insight: Graph algorithms typically depend on both the number of vertices AND edges. O(V + E) is the standard for BFS and DFS.
Example 21 — Matrix Multiply (Naive)
Analysis:
- Three nested loops, each running n times
- Total: n × n × n = n³
Answer: O(n³)
Key insight: Triple nested loops with the same bound = O(n³). Strassen's algorithm reduces this to O(n^2.807) using clever decomposition.
Example 22 — Detect Cycle in Linked List (Floyd's Algorithm)
def has_cycle(head):
"""Floyd's tortoise and hare algorithm."""
if not head:
return False
slow = head # Moves 1 step at a time
fast = head # Moves 2 steps at a time
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return FalseAnalysis:
- Both pointers move forward through the list
- If there's a cycle of length c, fast catches slow in O(c) steps after both enter the cycle
- Total nodes visited: proportional to the list length n
Answer: O(n)
Key insight: Even though two pointers move at different speeds, the total traversal is bounded by O(n).
Example 23 — Binary Search on Answer
Analysis:
- Binary search on speed from 1 to 10^7: log(10^7) ≈ 23 iterations
- Each check: O(n) where n = len(dist)
- Total: O(n log(max_speed))
Answer: O(n log S) where S = maximum speed
Key insight: "Binary Search on Answer" is a powerful technique where you binary search over the answer space and for each candidate answer, verify it in O(n). Total: O(n log range).
Example 24 — Sliding Window Maximum
Analysis:
- Each element is added to the deque exactly once: O(n) additions
- Each element is removed from the deque at most once: O(n) removals
- Total operations on deque: O(2n) = O(n)
- Main loop: n iterations
Answer: O(n) — linear despite the while loops inside
Key insight: Amortized analysis — "at most once" operations inside loops make the total O(n) even when loops appear to suggest otherwise.
Example 25 — Finding Number of Inversions (Merge Sort Based)
Analysis:
- This is Merge Sort with one extra O(1) operation per comparison
- Recurrence: T(n) = 2T(n/2) + O(n) — same as Merge Sort
Answer: O(n log n)
Key insight: Augmenting Merge Sort with additional O(1) work per step preserves the O(n log n) complexity.
Quick Reference — Pattern to Complexity
| Code Pattern | Complexity | Example Algorithm |
|---|---|---|
| Single loop over n | O(n) | Linear search |
| Two nested loops over n | O(n²) | Bubble sort |
| Triple nested loops over n | O(n³) | Naive matrix multiply |
| Loop halving/doubling | O(log n) | Binary search |
| Loop + halving inner loop | O(n log n) | Most efficient sorts |
| Recursive, halves input | O(log n) | Binary search recursive |
| Recursive, two calls halving | O(n log n) | Merge sort |
| Recursive, two calls subtracting 1 | O(2ⁿ) | Naive Fibonacci |
| Recursive, memoized | O(unique states) | DP Fibonacci |
| All subsets | O(n × 2ⁿ) | Power set generation |
| All permutations | O(n × n!) | Permutation generation |
| Graph BFS/DFS | O(V + E) | Traversal |
Summary
Analyzing time complexity is a skill developed through practice. The key rules to remember:
- Drop constants: O(3n) = O(n)
- Keep dominant term: O(n² + n) = O(n²)
- Sequential blocks add: O(n) + O(n) = O(n)
- Nested blocks multiply: O(n) × O(n) = O(n²)
- Halving loop: O(log n)
- Loop + inner halving: O(n log n)
- Two recursive calls on n-1: O(2ⁿ)
- Two recursive calls on n/2 + O(n) work: O(n log n)
- Memoization: (unique states) × (work per state)
- Graph algorithms: O(V + E)
Practice these 25 examples until the analysis becomes automatic. At that point, you are ready to handle any complexity analysis question in an interview.
*This completes the Algorithm Analysis section. Next section: Arrays — The most fundamental data structure, the foundation of all linear storage.*
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Time Complexity Examples — 25 Worked Problems from Basic to Advanced.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Data Structures & Algorithms topic.
Search Terms
data-structures-algorithms, data structures & algorithms, data, structures, algorithms, algorithm, analysis, time
Related Data Structures & Algorithms Topics