Divide and Conquer Strategy and Recurrence Relations in DSA Python - Time & Space Complexity
We want to understand how dividing a problem into smaller parts affects the time it takes to solve it.
How does breaking tasks down and combining results change the work needed as input grows?
Analyze the time complexity of the following code snippet.
def divide_and_conquer(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = divide_and_conquer(arr[:mid])
right = divide_and_conquer(arr[mid:])
return merge(left, right)
# Assume merge merges two sorted lists in linear time
This code splits the list into halves, solves each half, then merges the results.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursive calls splitting the array into halves and merging sorted halves.
- How many times: The array is split until size 1, so about log2(n) levels of recursion, with merging at each level.
Each level splits the array and merges all parts, doing work proportional to n per level.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 * 3 = 30 |
| 100 | About 100 * 7 = 700 |
| 1000 | About 1000 * 10 = 10,000 |
Pattern observation: Operations grow roughly as n times log n, increasing faster than linear but slower than quadratic.
Time Complexity: O(n log n)
This means the work grows a bit faster than the input size, because we split and merge multiple times.
[X] Wrong: "Since we split the array in half each time, the time must be O(log n)."
[OK] Correct: Although splitting is fast, merging takes linear time at each level, so total work adds up to n log n.
Understanding how dividing problems reduces complexity is a key skill for solving many algorithm challenges efficiently.
"What if the merge step took constant time instead of linear? How would the time complexity change?"
Practice
Divide and Conquer strategy in algorithms?Solution
Step 1: Understand the divide and conquer approach
It splits a big problem into smaller parts that are easier to solve.Step 2: Combine the solutions of subproblems
After solving smaller parts, their results are merged to get the final answer.Final Answer:
Break the problem into smaller subproblems, solve them independently, then combine results -> Option AQuick Check:
Divide and conquer = split, solve, combine [OK]
- Confusing divide and conquer with brute force
- Thinking it uses only loops without recursion
- Mixing it up with dynamic programming
Solution
Step 1: Recall Merge Sort recurrence
Merge Sort splits array into two halves, sorts each half, then merges in linear time.Step 2: Write recurrence relation
It calls itself twice on half size (2T(n/2)) plus O(n) for merging.Final Answer:
T(n) = 2T(n/2) + O(n) -> Option AQuick Check:
Merge Sort recurrence = 2T(n/2) + n [OK]
- Using T(n-1) instead of T(n/2)
- Confusing merge cost as O(n^2)
- Choosing wrong number of recursive calls
def sum_list(arr):
if len(arr) == 1:
return arr[0]
mid = len(arr) // 2
left_sum = sum_list(arr[:mid])
right_sum = sum_list(arr[mid:])
return left_sum + right_sum
print(sum_list([2, 4, 6, 8]))Solution
Step 1: Trace recursive calls
List splits into [2,4] and [6,8], then further into single elements.Step 2: Calculate sums bottom-up
Sum of [2,4] = 6, sum of [6,8] = 14, total = 6 + 14 = 20.Final Answer:
20 -> Option BQuick Check:
2+4+6+8 = 20 [OK]
- Confusing mid index slicing
- Returning partial sums incorrectly
- Forgetting base case for single element
def max_element(arr):
if len(arr) == 1:
return arr[0]
mid = len(arr) // 2
left_max = max_element(arr[:mid])
right_max = max_element(arr[mid:])
if left_max > right_max:
return left_max
else:
return arr[right_max]Solution
Step 1: Check return statements
Left max returns value correctly, right max returns index incorrectly by arr[right_max].Step 2: Fix return for right max comparison
Should return right_max directly, not arr[right_max], because right_max is already a value.Final Answer:
Incorrect return statement: should return right_max, not arr[right_max] -> Option DQuick Check:
Return max values, not index or wrong element [OK]
- Returning element by index instead of value
- Wrong slicing causing infinite recursion
- Ignoring base case for single element
T(n) = 3T(n/3) + n, what is the time complexity using the Master Theorem?Solution
Step 1: Identify parameters for Master Theorem
Here, a=3, b=3, f(n)=n.Step 2: Calculate n^log_b(a)
log_3(3) = 1, so n^1 = n.Step 3: Compare f(n) with n^log_b(a)
f(n) = n matches n^log_b(a) = n, so case 2 applies.Step 4: Apply Master Theorem case 2
Time complexity is O(n log n).Final Answer:
O(n log n) -> Option CQuick Check:
Master Theorem case 2 = O(n log n) [OK]
- Confusing log base or value
- Choosing wrong Master Theorem case
- Ignoring the additive term f(n)
Ready to prove it?
Pass the test and this pattern moves unknown
