Divide and Conquer Strategy and Recurrence Relations in DSA Go - Time & Space Complexity
We want to understand how breaking a problem into smaller parts affects the time it takes to solve it.
How does the total work grow when we divide and conquer?
Analyze the time complexity of the following code snippet.
func divideAndConquer(n int) int {
if n <= 1 {
return 1
}
left := divideAndConquer(n / 2)
right := divideAndConquer(n / 2)
return left + right + n
}
This function splits the problem into two halves, solves each recursively, and does some work proportional to n after.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Two recursive calls each on half the input size.
- How many times: Recursion continues until input size is 1, splitting in half each time.
- Additional work: After recursion, does work proportional to n (like adding n).
Each time we split, the problem halves, but we do work proportional to the current size.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 * log2(10) ≈ 33 |
| 100 | About 100 * log2(100) ≈ 664 |
| 1000 | About 1000 * log2(1000) ≈ 9966 |
Pattern observation: The total work grows a bit faster than n but much slower than n squared, roughly n times log n.
Time Complexity: O(n log n)
This means the time grows a little more than linearly, increasing by the input size times the number of times we can split it in half.
[X] Wrong: "Because we split the problem in half twice, the time is just O(n)."
[OK] Correct: Even though the problem halves each time, the work done at each level adds up, making the total more than just n.
Understanding how divide and conquer affects time helps you explain many common algorithms clearly and confidently.
"What if the function only made one recursive call instead of two? How would the time complexity change?"
Practice
Divide and Conquer strategy in algorithms?Solution
Step 1: Understand Divide and Conquer concept
It splits a big problem into smaller parts that are easier to solve.Step 2: Combine solutions of subproblems
After solving smaller parts, their results are combined to get the final answer.Final Answer:
Break the problem into smaller subproblems, solve them independently, then combine results -> Option DQuick Check:
Divide and Conquer = Break, Solve, Combine [OK]
- Confusing divide and conquer with brute force
- Thinking it solves without breaking problem
- Mixing with dynamic programming concepts
Solution
Step 1: Recall Merge Sort process
Merge Sort divides array into two halves, sorts each half, then merges.Step 2: Write recurrence relation
It calls itself twice on half size (2T(n/2)) plus merging cost O(n).Final Answer:
T(n) = 2T(n/2) + O(n) -> Option CQuick Check:
Merge Sort recurrence = 2T(n/2) + n [OK]
- Using T(n-1) instead of T(n/2)
- Ignoring merge cost O(n)
- Confusing with other divide and conquer algorithms
func sum(arr []int) int {
if len(arr) == 1 {
return arr[0]
}
mid := len(arr) / 2
left := sum(arr[:mid])
right := sum(arr[mid:])
return left + right
}
fmt.Println(sum([]int{2, 4, 6, 8}))
What is the output of this code?Solution
Step 1: Understand the function behavior
The function splits the array into halves recursively and sums elements.Step 2: Trace the calls for input [2,4,6,8]
Sum of 2+4=6, sum of 6+8=14, total 6+14=20.Final Answer:
20 -> Option AQuick Check:
2+4+6+8 = 20 [OK]
- Confusing mid index slicing
- Assuming sum returns average
- Thinking it causes runtime error
func max(arr []int) int {
if len(arr) == 1 {
return arr[0]
}
mid := len(arr) / 2
left := max(arr[:mid])
right := max(arr[mid:])
if left > right {
return left
} else {
return right
}
}
fmt.Println(max([]int{}))Solution
Step 1: Analyze base case
The base case handles only len(arr) == 1, missing handling for empty slices (len(arr) == 0).Step 2: Trace execution on empty input
len(arr) == 0 skips base case, mid = 0, recurses on arr[:0] and arr[0:], both empty, causing infinite recursion and stack overflow panic.Final Answer:
The code will panic due to empty slice input -> Option BQuick Check:
Infinite recursion on empty input causes panic [OK]
- Ignoring empty slice input
- Assuming base case handles empty slice
- Thinking mid calculation causes error
T(n) = 3T(n/3) + n, which of the following is the correct 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 log_b(a)
log_3(3) = 1, so n^{log_b(a)} = 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 AQuick Check:
Master Theorem case 2 = O(n log n) [OK]
- Confusing log base or a and b values
- Choosing wrong Master Theorem case
- Ignoring the extra n factor in recurrence
Ready to prove it?
Pass the test and this pattern moves unknown
