Divide and Conquer Strategy and Recurrence Relations in DSA Rust - 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 split the problem and combine results?
Analyze the time complexity of the following code snippet.
fn divide_and_conquer(arr: &[i32]) -> i32 {
if arr.len() == 1 {
return arr[0];
}
let mid = arr.len() / 2;
let left = divide_and_conquer(&arr[..mid]);
let right = divide_and_conquer(&arr[mid..]);
left + right
}
This function splits the array into halves recursively and sums all elements.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursive calls splitting the array into halves.
- How many times: Each call splits the array until size 1, so about log2(n) levels deep.
- Work per level: Combining results is a simple addition, constant time per call.
Each time we split, the problem size halves, but the number of calls doubles.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 19 calls (levels x calls per level) |
| 100 | About 199 calls |
| 1000 | About 1999 calls |
Pattern observation: Total work grows roughly proportional to n, even though calls grow exponentially, work per call is small.
Time Complexity: O(n)
This means the total work grows linearly with input size, because splitting and combining are balanced.
[X] Wrong: "Because the function calls itself twice, the time is O(2^n)."
[OK] Correct: The input size halves each time, so the number of levels is log n, not n, preventing exponential growth.
Understanding how dividing problems reduces work is key to solving many algorithm challenges efficiently.
What if the function combined results by looping through the entire array each time? 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 one loop
- Mixing with dynamic programming concepts
Solution
Step 1: Check for base case in recursion
fn divide_and_conquer(n: i32) -> i32 { if n == 1 { 1 } else { divide_and_conquer(n - 1) + 1 } } has a base case: if n == 1 returns 1, which stops recursion.Step 2: Verify recursive call reduces problem size
fn divide_and_conquer(n: i32) -> i32 { if n == 1 { 1 } else { divide_and_conquer(n - 1) + 1 } } calls divide_and_conquer(n - 1), reducing n each time.Final Answer:
fn divide_and_conquer(n: i32) -> i32 { if n == 1 { 1 } else { divide_and_conquer(n - 1) + 1 } } -> Option DQuick Check:
Recursive function needs base case and smaller input [OK]
- Missing base case causing infinite recursion
- Not reducing input in recursive call
- Using loops instead of recursion
merge_sort(vec![3, 1, 4, 2])?
fn merge_sort(mut arr: Vec<i32>) -> Vec<i32> {
if arr.len() <= 1 {
return arr;
}
let mid = arr.len() / 2;
let left = merge_sort(arr.drain(..mid).collect());
let right = merge_sort(arr);
merge(left, right)
}
fn merge(left: Vec<i32>, right: Vec<i32>) -> Vec<i32> {
let mut result = Vec::new();
let mut i = 0;
let mut j = 0;
while i < left.len() && j < right.len() {
if left[i] < right[j] {
result.push(left[i]);
i += 1;
} else {
result.push(right[j]);
j += 1;
}
}
result.extend_from_slice(&left[i..]);
result.extend_from_slice(&right[j..]);
result
}Solution
Step 1: Understand merge sort splits and sorts
The function splits the array into halves recursively until size 1, then merges sorted halves.Step 2: Trace sorting of input [3, 1, 4, 2]
Splitting: [3,1] and [4,2], then [3], [1], [4], [2]. Merging sorted pairs: [1,3] and [2,4]. Final merge: [1,2,3,4].Final Answer:
[1, 2, 3, 4] -> Option CQuick Check:
Merge sort output is sorted array ascending [OK]
- Confusing merge order causing reversed output
- Not understanding recursive splitting
- Ignoring that merge combines sorted halves
fn fib(n: u32) -> u32 {
if n <= 1 {
return n;
}
fib(n - 1) + fib(n - 1)
}Solution
Step 1: Check recursive calls in function
The function calls fib(n - 1) twice, which is incorrect for Fibonacci.Step 2: Recall Fibonacci formula
Fibonacci(n) = Fibonacci(n-1) + Fibonacci(n-2), so one call should be fib(n - 2).Final Answer:
It calls fib(n - 1) twice instead of fib(n - 1) and fib(n - 2) -> Option BQuick Check:
Fibonacci needs fib(n-1) + fib(n-2) [OK]
- Calling fib(n-1) twice causing wrong results
- Ignoring base cases
- Confusing parameter types
T(n) = 2T(n/2) + n, which of the following is the correct time complexity using the Master Theorem?Solution
Step 1: Identify parameters a, b, and f(n)
Here, a = 2, b = 2, and f(n) = n.Step 2: Calculate log_b(a)
log_2(2) = 1.Step 3: Compare f(n) with n^{log_b(a)}
f(n) = n and n^{log_b(a)} = n^1 = n, so f(n) = Theta(n^{log_b(a)}).Step 4: Apply Master Theorem case 2
When f(n) = Theta(n^{log_b(a)}), T(n) = Theta(n log n).Final Answer:
O(n log n) -> Option AQuick Check:
Master theorem case 2 gives O(n log n) [OK]
- Confusing log base or parameters a and b
- Choosing wrong Master Theorem case
- Ignoring the additive term f(n)
Ready to prove it?
Pass the test and this pattern moves unknown
