Merge Sort as Divide and Conquer in DSA Rust - Time & Space Complexity
We want to understand how the time taken by merge sort changes as the list size grows.
How does splitting and merging affect the total work done?
Analyze the time complexity of the following code snippet.
fn merge_sort(arr: &mut [i32]) {
let n = arr.len();
if n <= 1 { return; }
let mid = n / 2;
merge_sort(&mut arr[..mid]);
merge_sort(&mut arr[mid..]);
merge(arr, mid);
}
fn merge(arr: &mut [i32], mid: usize) {
// merges two sorted halves
}
This code splits the list into halves, sorts each half recursively, then merges them.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursive splitting and merging of sublists.
- How many times: The list is split into halves repeatedly until size 1, then merged back.
Each split divides the list in half, creating more sublists to sort and merge.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 40 merges and splits combined |
| 100 | About 700 merges and splits combined |
| 1000 | About 10,000 merges and splits combined |
Pattern observation: The work grows a bit faster than just doubling; it grows roughly by n times log n.
Time Complexity: O(n log n)
This means if the list size doubles, the work grows a bit more than double, because of repeated splitting and merging.
[X] Wrong: "Merge sort is just like a simple loop, so it should be O(n)."
[OK] Correct: Merge sort splits the list many times and merges repeatedly, so the total work adds up more than a single pass.
Understanding merge sort's time complexity helps you explain how divide and conquer algorithms efficiently handle large data.
"What if we changed merge sort to split the list into three parts instead of two? How would the time complexity change?"
Practice
Merge Sort algorithm in the context of divide and conquer?Solution
Step 1: Understand divide and conquer in Merge Sort
Merge Sort splits the array into two halves recursively until single elements remain.Step 2: Merge sorted halves
After splitting, it merges the sorted halves back together to form a sorted array.Final Answer:
Divide the array into halves, sort each half, then merge them -> Option DQuick Check:
Divide and conquer = split, sort, merge [OK]
- Confusing Merge Sort with Bubble Sort
- Thinking Merge Sort uses a pivot like Quick Sort
- Ignoring the merge step after dividing
Solution
Step 1: Identify mutable slice parameter
Merge sort modifies the array in place, so it needs a mutable slice: &mut [i32].Step 2: Check function return type
In-place sorting returns nothing, so return type is omitted (unit type).Final Answer:
fn merge_sort(arr: &mut [i32]) -> Option BQuick Check:
Mutable slice input, no return [OK]
- Using immutable slice &[i32] when mutation is needed
- Returning a new vector instead of sorting in place
- Incorrect return type for in-place function
[4, 1, 3, 2]?
fn main() {
let mut arr = [4, 1, 3, 2];
merge_sort(&mut arr);
println!("{:?}", arr);
}
fn merge_sort(arr: &mut [i32]) {
let mid = arr.len() / 2;
if mid == 0 { return; }
merge_sort(&mut arr[..mid]);
merge_sort(&mut arr[mid..]);
let mut merged = arr.to_vec();
merge(&arr[..mid], &arr[mid..], &mut merged[..]);
arr.copy_from_slice(&merged);
}
fn merge(left: &[i32], right: &[i32], merged: &mut [i32]) {
let mut i = 0;
let mut j = 0;
let mut k = 0;
while i < left.len() && j < right.len() {
if left[i] <= right[j] {
merged[k] = left[i];
i += 1;
} else {
merged[k] = right[j];
j += 1;
}
k += 1;
}
while i < left.len() {
merged[k] = left[i];
i += 1;
k += 1;
}
while j < right.len() {
merged[k] = right[j];
j += 1;
k += 1;
}
}Solution
Step 1: Trace recursive splitting
Array splits into [4,1] and [3,2], then further into single elements.Step 2: Merge sorted halves
Merge [4] and [1] into [1,4], merge [3] and [2] into [2,3], then merge [1,4] and [2,3] into [1,2,3,4].Final Answer:
[1, 2, 3, 4] -> Option AQuick Check:
Sorted array = [1, 2, 3, 4] [OK]
- Confusing merge order causing wrong output
- Forgetting to copy merged array back
- Incorrect base case stopping recursion too early
fn merge_sort(arr: &mut [i32]) {
let mid = arr.len() / 2;
if arr.len() <= 1 { return; }
merge_sort(&mut arr[..mid]);
merge_sort(&mut arr[mid..]);
let mut merged = arr.to_vec();
merge(&arr[..mid], &arr[mid..], &mut merged[..]);
arr.copy_from_slice(&merged);
}Solution
Step 1: Check base case
The conditionif arr.len() <= 1 { return; }correctly handles arrays of length 0 or 1.Step 2: Verify recursive calls
Disjoint mutable slicesarr[..mid]andarr[mid..]are sorted independently.Step 3: Examine merge
Immutable borrows of disjoint slices tomergeare temporary; writes to separatemergedvec. No overlap or borrow issues.Final Answer:
The base case condition is correct; no bug here -> Option AQuick Check:
Disjoint slices + temporary borrows = safe [OK]
- Ignoring slice borrowing rules in Rust
- Confusing base case condition with bug
- Assuming immutable slices cause errors with mutable writes
Solution
Step 1: Understand stable merge importance
Stable merge preserves order of equal elements and can optimize by referencing indices instead of copying repeatedly.Step 2: Avoid unnecessary copying of duplicates
By using indices and stable merge logic, duplicates are merged efficiently without extra copying.Final Answer:
Use a stable merge that avoids copying duplicates by referencing indices -> Option CQuick Check:
Stable merge optimizes duplicate handling [OK]
- Trying to merge duplicates as single element (loses data)
- Skipping copying equal elements breaks stability
- Sorting only unique elements changes array content
Ready to prove it?
Pass the test and this pattern moves unknown
