Quick Sort as Divide and Conquer in DSA Rust - Time & Space Complexity
We want to understand how the time to sort a list using Quick Sort changes as the list gets bigger.
How does the work grow when we divide and conquer the list?
Analyze the time complexity of the following code snippet.
fn quick_sort(arr: &mut [i32]) {
if arr.len() <= 1 {
return;
}
let pivot_index = partition(arr);
quick_sort(&mut arr[..pivot_index]);
quick_sort(&mut arr[pivot_index + 1..]);
}
fn partition(arr: &mut [i32]) -> usize {
let pivot = arr[arr.len() - 1];
let mut i = 0;
for j in 0..arr.len() - 1 {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, arr.len() - 1);
i
}
This code sorts an array by picking a pivot, dividing the array around it, and sorting each part recursively.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The partition loop that compares and swaps elements around the pivot.
- How many times: This loop runs once per recursive call, processing the current subarray.
- Recursion: The function calls itself twice on smaller parts after partitioning.
When the list is split evenly, each step does work proportional to the list size, then splits into halves.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 30 comparisons and swaps |
| 100 | About 700 operations |
| 1000 | About 10,000 operations |
Pattern observation: The work grows a bit faster than linear but much slower than quadratic, roughly n times log n.
Time Complexity: O(n log n)
This means the time to sort grows a little more than linearly as the list gets bigger, thanks to dividing the problem.
[X] Wrong: "Quick Sort always takes the same time no matter the input."
[OK] Correct: If the pivot choices are poor, Quick Sort can take much longer, even quadratic time, because it doesn't split evenly.
Understanding Quick Sort's time complexity shows you how divide and conquer helps solve problems efficiently, a key skill in many coding challenges.
"What if we always picked the first element as pivot instead of the last? How would the time complexity change?"
Practice
Solution
Step 1: Understand divide and conquer in Quick Sort
Quick Sort divides the list into smaller parts based on a pivot, sorts each part, then combines results.Step 2: Compare options with this idea
Divide the list into smaller parts, sort them, then combine matches this process exactly, others describe different sorting methods.Final Answer:
Divide the list into smaller parts, sort them, then combine -> Option DQuick Check:
Divide and conquer = Divide, sort, combine [OK]
- Confusing Quick Sort with Bubble Sort
- Thinking Quick Sort merges sorted lists like Merge Sort
- Assuming Quick Sort sorts by largest element first
Solution
Step 1: Identify correct parameter type for in-place sorting
Quick Sort usually sorts in place, so it needs a mutable slice reference: &mut [i32].Step 2: Check return type and parameter correctness
fn quick_sort(arr: &mut [i32]) matches this: it takes &mut [i32] and returns nothing (unit). Others either take immutable slices or return new vectors.Final Answer:
fn quick_sort(arr: &mut [i32]) -> Option CQuick Check:
Mutable slice parameter = fn quick_sort(arr: &mut [i32]) [OK]
- Using immutable slice when mutation is needed
- Returning a new vector unnecessarily
- Returning wrong type like i32
fn quick_sort(arr: &mut [i32]) {
if arr.len() <= 1 { return; }
let pivot = arr[0];
let mut left = 1;
let mut right = arr.len() - 1;
while left <= right {
if arr[left] <= pivot {
left += 1;
} else if arr[right] > pivot {
right -= 1;
} else {
arr.swap(left, right);
left += 1;
right -= 1;
}
}
arr.swap(0, right);
quick_sort(&mut arr[0..right]);
quick_sort(&mut arr[right+1..]);
}
fn main() {
let mut data = [3, 6, 2, 7, 5];
quick_sort(&mut data);
println!("{:?}", data);
}Solution
Step 1: Trace partitioning with pivot 3
Pivot is 3. Elements <=3 go left, >3 go right. After partition, array becomes [2, 3, 5, 7, 6] with pivot at index 1.Step 2: Recursively sort left and right parts
Left part [2] is sorted (single element). Right part [5,7,6] sorts to [5,6,7]. Final sorted array is [2, 3, 5, 6, 7].Final Answer:
[2, 3, 5, 6, 7] -> Option AQuick Check:
Sorted array = [2, 3, 5, 6, 7] [OK]
- Not swapping pivot correctly
- Confusing left and right pointers
- Missing recursive calls on subarrays
fn quick_sort(arr: &mut [i32]) {
if arr.len() <= 1 { return; }
let pivot = arr[0];
let mut left = 0;
let mut right = arr.len() - 1;
while left < right {
while arr[left] <= pivot && left < right {
left += 1;
}
while arr[right] > pivot {
right -= 1;
}
if left < right {
arr.swap(left, right);
}
}
arr.swap(0, right);
quick_sort(&mut arr[0..right]);
quick_sort(&mut arr[right+1..]);
}Solution
Step 1: Analyze left pointer initialization
Left starts at 0, same as pivot index, causing infinite loop because arr[left] <= pivot is always true initially.Step 2: Fix by starting left at 1
Starting left at 1 avoids comparing pivot with itself and allows pointers to move correctly.Final Answer:
Initialize left to 1 instead of 0 -> Option AQuick Check:
Left pointer start = 1 fixes infinite loop [OK]
- Leaving left at 0 causing no progress
- Changing conditions without fixing pointer start
- Swapping with wrong index causing wrong partition
Solution
Step 1: Understand problem with duplicates in Quick Sort
Duplicates cause unbalanced partitions leading to worst-case O(n²) time if pivot is always chosen poorly.Step 2: Apply three-way partitioning
Three-way partitioning divides array into elements less than, equal to, and greater than pivot, handling duplicates efficiently and improving performance.Final Answer:
Use a three-way partitioning to separate less, equal, and greater elements -> Option BQuick Check:
Three-way partitioning handles duplicates well [OK]
- Ignoring duplicates causing slow sorting
- Choosing fixed pivot without randomization or partitioning
- Removing duplicates changes data, not always allowed
Ready to prove it?
Pass the test and this pattern moves unknown
