Time Complexity (Big-O)
Time Complexity (Big-O)
When you search for a contact in your phone, it happens instantly. But what if your phone had to check every single contact one by one? With 1,000 contacts, that would take forever! This difference in speed is what time complexity measures.
Time complexity tells us how the runtime of an algorithm grows as the input size increases.
Why Time Complexity Matters
Imagine two algorithms that both find a number in a list:
Algorithm A: Checks every number one by one
Algorithm B: Uses a smart approach to check half the list each time
For a list of 1,000 numbers:
- ›Algorithm A: Might take 1,000 steps
- ›Algorithm B: Takes only 10 steps
Algorithm B is 100x faster! Time complexity helps us measure and compare this difference.
Key Idea: Time complexity measures how an algorithm's speed changes when we increase the input size, not the exact time in seconds.
What is Big-O Notation?
Big-O notation is the language we use to describe time complexity. It answers the question: "How does runtime grow as input size grows?"
Format: O(expression)
Examples:
- ›O(1) - Constant time
- ›O(n) - Linear time
- ›O(n²) - Quadratic time
- ›O(log n) - Logarithmic time
The "n" represents the input size (like the length of an array).
Understanding O(1) - Constant Time
Definition: Runtime stays the same no matter how big the input is.
Real-World Example: Accessing a book by page number. Whether the book has 100 pages or 10,000 pages, flipping to page 42 takes the same time.
Code Example
1# Accessing array element - O(1)
2numbers = [10, 20, 30, 40, 50]
3
4# Direct access - always takes same time
5first = numbers[0] # O(1)
6last = numbers[4] # O(1)
7
8print(first) # 10
9print(last) # 50Analysis:
- ›Array size: 5, 100, or 1,000,000
- ›Access time: Always 1 step
- ›Time Complexity: O(1)
O(1) means: No matter how large the input, the algorithm takes the same number of steps.
Understanding O(n) - Linear Time
Definition: Runtime grows directly with input size. If input doubles, runtime doubles.
Real-World Example: Reading a book page by page. If the book has 100 pages, it takes 100 units of time. If it has 200 pages, it takes 200 units.
Code Example
1# Print all elements - O(n)
2numbers = [10, 20, 30, 40, 50]
3
4# Must visit each element once
5for num in numbers:
6 print(num)
7
8# If array has 5 elements: 5 steps
9# If array has 100 elements: 100 steps
10# Time Complexity: O(n)Analysis:
- ›Array size: n
- ›Steps: n (visit each element once)
- ›Time Complexity: O(n)
Understanding O(n²) - Quadratic Time
Definition: Runtime grows with the square of input size. If input doubles, runtime quadruples.
Real-World Example: Comparing every person with every other person in a room. With 10 people, that's 10 × 10 = 100 comparisons. With 20 people, it's 20 × 20 = 400 comparisons.
Code Example
1# Nested loops - O(n²)
2numbers = [1, 2, 3, 4, 5]
3
4# Outer loop runs n times
5for i in numbers:
6 # Inner loop runs n times for each outer iteration
7 for j in numbers:
8 print(f"({i}, {j})")
9
10# Total iterations: n × n = n²
11# Time Complexity: O(n²)Analysis:
- ›Array size: n = 5
- ›Outer loop: 5 iterations
- ›Inner loop: 5 iterations per outer loop
- ›Total: 5 × 5 = 25 steps
- ›Time Complexity: O(n²)
Understanding O(log n) - Logarithmic Time
Definition: Runtime grows slowly as input increases. Doubling input adds only one more step.
Real-World Example: Finding a name in a phone book by opening to the middle, checking if you need to go left or right, then repeating. Each step eliminates half the remaining pages.
Code Example: Binary Search
1# Binary Search - O(log n)
2def binary_search(arr, target):
3 left = 0
4 right = len(arr) - 1
5
6 while left <= right:
7 mid = (left + right) // 2
8
9 if arr[mid] == target:
10 return mid # Found
11 elif arr[mid] < target:
12 left = mid + 1 # Search right half
13 else:
14 right = mid - 1 # Search left half
15
16 return -1 # Not found
17
18# Example
19numbers = [10, 20, 30, 40, 50, 60, 70, 80]
20result = binary_search(numbers, 40)
21print(f"Found at index: {result}") # 3Analysis:
- ›Array size: 8 elements
- ›Step 1: Check middle (index 4) → not found
- ›Step 2: Check middle of left half (index 2) → not found
- ›Step 3: Check middle of new range (index 3) → found!
- ›Total steps: 3 = log₂(8)
- ›Time Complexity: O(log n)
Common Time Complexities Compared
| Notation | Name | Example | n=10 | n=100 | n=1000 |
|---|---|---|---|---|---|
| O(1) | Constant | Array access | 1 | 1 | 1 |
| O(log n) | Logarithmic | Binary search | 3 | 7 | 10 |
| O(n) | Linear | Loop through array | 10 | 100 | 1000 |
| O(n log n) | Linearithmic | Merge sort | 30 | 700 | 10,000 |
| O(n²) | Quadratic | Nested loops | 100 | 10,000 | 1,000,000 |
| O(2ⁿ) | Exponential | Recursive Fibonacci | 1024 | 10³⁰ | Very large |
Key Insight: As input grows, O(1) and O(log n) scale well, while O(n²) and O(2ⁿ) become impractical.
How to Calculate Time Complexity
Rule 1: Drop Constants
1# Example 1
2def example1(arr):
3 print(arr[0]) # O(1)
4 print(arr[0]) # O(1)
5 print(arr[0]) # O(1)
6
7# Total: O(1) + O(1) + O(1) = O(3)
8# Simplified: O(1) ← Drop the constant 3Why? Constants don't affect growth rate as input size increases.
Rule 2: Drop Non-Dominant Terms
1# Example 2
2def example2(arr):
3 # First loop
4 for num in arr:
5 print(num) # O(n)
6
7 # Nested loops
8 for i in arr:
9 for j in arr:
10 print(i, j) # O(n²)
11
12# Total: O(n) + O(n²)
13# Simplified: O(n²) ← Drop O(n), keep dominant termWhy? For large n, n² grows much faster than n.
Rule 3: Different Inputs = Different Variables
1# Example 3
2def example3(arr1, arr2):
3 for num in arr1:
4 print(num) # O(a) where a = len(arr1)
5
6 for num in arr2:
7 print(num) # O(b) where b = len(arr2)
8
9# Total: O(a + b) ← NOT O(n) because different arraysPractice Problems
Problem 1: Find Time Complexity
1def mystery_function(arr):
2 n = len(arr)
3
4 for i in range(n):
5 print(arr[i])
6
7 for i in range(n):
8 for j in range(n):
9 print(arr[i] + arr[j])
10
11# What is the time complexity?
12# Answer: O(n²)
13# Explanation: O(n) + O(n²) = O(n²)Problem 2: Compare Algorithms
Which is faster for n = 1000?
Algorithm A: O(n²)
Algorithm B: O(n log n)
Answer: Algorithm B
Calculation:
- ›Algorithm A: 1000² = 1,000,000 steps
- ›Algorithm B: 1000 × log(1000) ≈ 1000 × 10 = 10,000 steps
Algorithm B is 100x faster!
Real-World Impact
Example: Sorting Algorithms
| Algorithm | Time Complexity | Time for 1,000 items |
|---|---|---|
| Bubble Sort | O(n²) | 1,000,000 steps |
| Merge Sort | O(n log n) | 10,000 steps |
| Quick Sort (average) | O(n log n) | 10,000 steps |
Impact: For large datasets, choosing the right algorithm makes the difference between instant results and waiting hours.
Best, Average, and Worst Case
Some algorithms perform differently based on input:
Linear Search Example
1def linear_search(arr, target):
2 for i in range(len(arr)):
3 if arr[i] == target:
4 return i
5 return -1
6
7# Best case: O(1) - target is first element
8# Average case: O(n/2) → O(n) - target in middle
9# Worst case: O(n) - target is last or not presentNote: We usually talk about worst-case complexity unless specified otherwise.
Key Takeaways
Big-O Notation describes how algorithm runtime grows with input size.
Common Complexities: O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)
Rules: Drop constants, drop non-dominant terms, use different variables for different inputs.
Focus on worst case unless otherwise specified.
Choose algorithms wisely: For large inputs, O(n log n) is far better than O(n²).
What's Next?
Now that you understand time complexity:
- ›Space Complexity - Learn about memory usage
- ›Asymptotic Notation - Dive deeper into Big-O, Big-Θ, Big-Ω
- ›Array Basics - Apply time complexity to real data structures
Practice Exercises
Determine the time complexity of these code snippets:
Exercise 1
1def exercise1(n):
2 for i in range(n):
3 for j in range(n):
4 for k in range(n):
5 print(i, j, k)
6
7# Answer: O(n³) - three nested loopsExercise 2
1def exercise2(arr):
2 if len(arr) == 0:
3 return
4 print(arr[0])
5 print(arr[len(arr) - 1])
6
7# Answer: O(1) - constant operationsReady to continue? Let's explore Space Complexity next!