Analyze Code Complexity
Analyze Code Complexity
You've learned the theory of Big-O, Big-Omega, and Big-Theta. Now it's time to practice the most important skill: analyzing real code.
When you see a piece of code, can you determine its time and space complexity? This tutorial will teach you a systematic approach to analyze any algorithm.
Goal: By the end of this tutorial, you'll be able to look at any code and determine its complexity in seconds.
Key Skill: Analyzing code complexity is essential for technical interviews, optimizing performance, and writing efficient algorithms.
The Step-by-Step Analysis Method
Follow this process for every code analysis:
Step 1: Identify the Input Size
Question: What is 'n' in this problem?
Common patterns:
- ›Array/list length
- ›String length
- ›Number value
- ›Matrix dimensions (m × n)
- ›Tree height
- ›Graph vertices/edges
Step 2: Count the Operations
Look for:
- ›Loops (for, while)
- ›Recursive calls
- ›Nested structures
- ›Data structure operations
Step 3: Express as Mathematical Function
Convert to:
- ›Single loop → n
- ›Nested loops → n²
- ›Recursive calls → depends on recursion tree
- ›Halving each time → log n
Step 4: Simplify Using Big-O Rules
Rules:
- ›Drop constants: O(3n) → O(n)
- ›Drop non-dominant terms: O(n² + n) → O(n²)
- ›Different variables: O(a + b) stays as O(a + b)
Step 5: Determine Space Complexity
Count:
- ›Extra arrays/lists created
- ›Recursion call stack depth
- ›Hash maps/sets
- ›Variables (usually O(1))
Example 1: Simple Loop
Let's analyze this code step-by-step:
1def print_numbers(n):
2 for i in range(n):
3 print(i)Analysis:
Step 1 - Input size: n (the parameter)
Step 2 - Operations:
- ›Loop runs from 0 to n-1
- ›Total iterations: n
- ›Each iteration: 1 print operation
Step 3 - Mathematical expression:
- ›f(n) = n iterations × 1 operation = n
Step 4 - Big-O simplification:
- ›Time Complexity: O(n)
Step 5 - Space complexity:
- ›Variables: i (constant)
- ›Space Complexity: O(1)
Answer: Time O(n), Space O(1)
Example 2: Nested Loops
1def print_pairs(n):
2 for i in range(n):
3 for j in range(n):
4 print(i, j)Analysis:
Step 1: Input size = n
Step 2: Operations count
- ›Outer loop: n iterations
- ›Inner loop: n iterations (for each outer iteration)
- ›Total: n × n = n²
Step 3: f(n) = n²
Step 4: Time Complexity: O(n²)
Step 5: Space: Only variables i, j → O(1)
Answer: Time O(n²), Space O(1)
Example 3: Sequential Loops
1def two_loops(arr):
2 # First loop
3 for i in range(len(arr)):
4 print(arr[i])
5
6 # Second loop
7 for i in range(len(arr)):
8 print(arr[i] * 2)Analysis:
Step 1: Input size = n (array length)
Step 2: Operations
- ›First loop: n iterations
- ›Second loop: n iterations
- ›Total: n + n = 2n
Step 3: f(n) = 2n
Step 4: Drop constant: O(2n) → O(n)
Step 5: Space: O(1)
Answer: Time O(n), Space O(1)
Key Point: Sequential loops add, not multiply: O(n + n) = O(n)
Example 4: Different Input Sizes
1def process_two_arrays(arr1, arr2):
2 for item in arr1:
3 print(item)
4
5 for item in arr2:
6 print(item)Analysis:
Step 1: Two input sizes: a = len(arr1), b = len(arr2)
Step 2: Operations
- ›First loop: a iterations
- ›Second loop: b iterations
- ›Total: a + b
Step 3: f(a, b) = a + b
Step 4: Cannot simplify - different variables!
Answer: Time O(a + b), Space O(1)
Important: Don't write O(n) when you have different input sizes!
Example 5: Halving the Input
1def binary_search(arr, target):
2 left = 0
3 right = len(arr) - 1
4
5 while left <= right:
6 mid = (left + right) // 2
7
8 if arr[mid] == target:
9 return mid
10 elif arr[mid] < target:
11 left = mid + 1
12 else:
13 right = mid - 1
14
15 return -1Analysis:
Step 1: Input size = n (array length)
Step 2: Operations
- ›Each iteration: search space halves
- ›n → n/2 → n/4 → n/8 → ... → 1
- ›How many times can you divide n by 2? log₂(n)
Step 3: f(n) = log₂(n)
Step 4: Time Complexity: O(log n)
Step 5: Space: Variables only → O(1)
Answer: Time O(log n), Space O(1)
Pattern: Halving input size each time = O(log n)
Example 6: Recursion - Linear
1def factorial(n):
2 if n <= 1:
3 return 1
4 return n * factorial(n - 1)
5
6# Call tree:
7# factorial(5)
8# → factorial(4)
9# → factorial(3)
10# → factorial(2)
11# → factorial(1)Analysis:
Step 1: Input size = n
Step 2: Recursion tree
- ›factorial(n) → factorial(n-1) → ... → factorial(1)
- ›Depth: n levels
- ›Work per level: O(1) multiplication
Step 3: f(n) = n levels × 1 work = n
Step 4: Time: O(n)
Step 5: Space: Call stack depth = n → O(n)
Answer: Time O(n), Space O(n)
Key Point: Recursion uses space for the call stack!
Example 7: Recursion - Binary Tree
1def fibonacci(n):
2 if n <= 1:
3 return n
4 return fibonacci(n - 1) + fibonacci(n - 2)
5
6# Call tree for fib(5):
7# fib(5)
8# / \
9# fib(4) fib(3)
10# / \ / \
11# fib(3) fib(2) fib(2) fib(1)
12# ...Analysis:
Step 1: Input size = n
Step 2: Recursion tree
- ›Each call makes 2 recursive calls
- ›Height: n
- ›Total nodes: 2⁰ + 2¹ + 2² + ... + 2ⁿ ≈ 2ⁿ
Step 3: f(n) ≈ 2ⁿ
Step 4: Time: O(2ⁿ) - Exponential! Very slow!
Step 5: Space: Maximum call stack depth = n → O(n)
Answer: Time O(2ⁿ), Space O(n)
Example 8: Creating New Arrays
1def reverse_array(arr):
2 reversed_arr = []
3
4 for i in range(len(arr) - 1, -1, -1):
5 reversed_arr.append(arr[i])
6
7 return reversed_arrAnalysis:
Time:
- ›Loop: n iterations
- ›Time: O(n)
Space:
- ›New array: size n
- ›Space: O(n)
Answer: Time O(n), Space O(n)
Example 9: Hash Map Operations
1def find_duplicates(arr):
2 seen = set()
3 duplicates = []
4
5 for num in arr:
6 if num in seen:
7 duplicates.append(num)
8 else:
9 seen.add(num)
10
11 return duplicatesAnalysis:
Time:
- ›Loop: n iterations
- ›Hash set lookup/insert: O(1) average
- ›Total: n × 1 = O(n)
Space:
- ›Hash set: worst case n elements
- ›Duplicates list: worst case n elements
- ›Total: O(n)
Answer: Time O(n), Space O(n)
Key Point: Hash map operations are O(1) on average!
Example 10: Matrix Operations
1def print_matrix(matrix):
2 rows = len(matrix)
3 cols = len(matrix[0])
4
5 for i in range(rows):
6 for j in range(cols):
7 print(matrix[i][j])Analysis:
Step 1: Input sizes: m = rows, n = cols
Step 2: Operations
- ›Outer loop: m iterations
- ›Inner loop: n iterations (for each outer)
- ›Total: m × n
Step 3: f(m, n) = m × n
Step 4: Time: O(m × n) or O(n²) if square matrix
Step 5: Space: O(1)
Answer: Time O(m × n), Space O(1)
Common Complexity Patterns
Pattern 1: Single Loop
for i in range(n):
# O(1) operations
Time: O(n)
Pattern 2: Nested Loops (Same Size)
for i in range(n):
for j in range(n):
# O(1) operations
Time: O(n²)
Pattern 3: Nested Loops (Different Sizes)
for i in range(m):
for j in range(n):
# O(1) operations
Time: O(m × n)
Pattern 4: Halving Each Time
while n > 1:
n = n // 2
Time: O(log n)
Pattern 5: Two Branches Recursion
def recursive(n):
if n <= 1:
return
recursive(n - 1)
recursive(n - 1)
Time: O(2ⁿ)
Pattern 6: Divide and Conquer
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)
Time: O(n log n)
Practice Problems
Problem 1: Analyze This Code
1def mystery1(n):
2 count = 0
3 i = 1
4
5 while i < n:
6 count += 1
7 i *= 2
8
9 return countYour Turn: What's the time and space complexity?
Answer:
- ›i doubles each time: 1 → 2 → 4 → 8 → ... → n
- ›How many times? log₂(n)
- ›Time: O(log n)
- ›Space: O(1)
Problem 2: Analyze This Code
1def mystery2(arr):
2 result = []
3
4 for i in range(len(arr)):
5 for j in range(i, len(arr)):
6 result.append(arr[i] + arr[j])
7
8 return resultYour Turn: What's the complexity?
Answer:
- ›Outer loop: n times
- ›Inner loop: starts at i, goes to n
- ›i=0: n iterations
- ›i=1: n-1 iterations
- ›i=2: n-2 iterations
- ›Total: n + (n-1) + (n-2) + ... + 1 = n(n+1)/2
- ›Time: O(n²)
- ›Space: Result array has n² elements → O(n²)
Problem 3: Analyze This Code
1def mystery3(arr):
2 if len(arr) <= 1:
3 return arr
4
5 mid = len(arr) // 2
6 left = mystery3(arr[:mid])
7 right = mystery3(arr[mid:])
8
9 return left + rightYour Turn: What's the complexity?
Answer:
- ›Divides array in half each level: log n levels
- ›Each level processes n elements
- ›Time: O(n log n)
- ›Space: Creates new arrays at each level → O(n log n)
Quick Reference Table
| Code Pattern | Time | Space |
|---|---|---|
| Single loop | O(n) | O(1) |
| Two nested loops | O(n²) | O(1) |
| Three nested loops | O(n³) | O(1) |
| Binary search | O(log n) | O(1) |
| Merge sort | O(n log n) | O(n) |
| Quick sort (average) | O(n log n) | O(log n) |
| Linear recursion | O(n) | O(n) |
| Binary tree recursion | O(2ⁿ) | O(n) |
| Hash map lookup | O(1) avg | O(n) |
| Creating new array | O(n) | O(n) |
Tips for Technical Interviews
Tip 1: State Your Assumptions
"I'm assuming the input is an array of length n..."
Tip 2: Walk Through Examples
"For n = 4, the loop runs 4 times..."
Tip 3: Explain Your Reasoning
"The outer loop runs n times, and for each iteration, the inner loop also runs n times, so that's n × n = n²"
Tip 4: Consider Best and Worst Cases
"In the best case when the element is first, it's O(1). In the worst case, it's O(n)."
Tip 5: Don't Forget Space Complexity
"Time is O(n) but we're also creating a new array, so space is O(n) too."
Common Mistakes to Avoid
Mistake 1: Forgetting Recursion Stack Space
Wrong: "This recursive function uses O(1) space"
Right: "O(n) space for the call stack"
Mistake 2: Confusing Sequential and Nested Loops
Wrong: Two loops = O(n²)
Right: Sequential = O(n + n) = O(n), Nested = O(n × n) = O(n²)
Mistake 3: Not Considering All Inputs
Wrong: "Time is O(n)"
Right: "Time is O(m × n) where m and n are different array sizes"
Mistake 4: Assuming Hash Operations Are Free
Wrong: "Hash map lookup is free, so O(1)"
Right: "Hash map lookup is O(1) average, but the space is O(n)"
Key Takeaways
Systematic approach: Follow the 5-step method for every analysis
Common patterns: Recognize loops, recursion, halving, and data structures
Multiple inputs: Use different variables (a, b, m, n) for different inputs
Don't forget space: Analyze both time and space complexity
Practice makes perfect: The more you analyze, the faster you'll get
Explain your reasoning: In interviews, talk through your thought process
What's Next?
Now that you can analyze code complexity:
- ›Arrays - Apply complexity analysis to array algorithms
- ›Sorting Algorithms - Analyze different sorting approaches
- ›Searching Algorithms - Compare search techniques
Final Practice Challenge
Analyze this complete algorithm:
1def challenge(arr):
2 n = len(arr)
3 result = []
4
5 # Part 1: Nested loops
6 for i in range(n):
7 for j in range(i, n):
8 if arr[i] + arr[j] > 10:
9 result.append((i, j))
10
11 # Part 2: Sort result
12 result.sort()
13
14 # Part 3: Binary search
15 target = (0, 5)
16 left, right = 0, len(result) - 1
17 while left <= right:
18 mid = (left + right) // 2
19 if result[mid] == target:
20 return mid
21 elif result[mid] < target:
22 left = mid + 1
23 else:
24 right = mid - 1
25
26 return -1
27
28# What's the time and space complexity?Answer:
- ›Part 1: O(n²) - nested loops
- ›Part 2: O(k log k) where k = size of result (worst case k = n²)
- ›Part 3: O(log k)
- ›Total Time: O(n²) + O(n² log n²) + O(log n²) = O(n² log n)
- ›Space: Result array stores up to n² pairs = O(n²)
Congratulations! You can now analyze any code's complexity. Keep practicing!