Quick Sort Algorithm in DSA Rust - Time & Space Complexity
We want to understand how the time taken by Quick Sort changes as the list size grows.
How does the number of steps grow when sorting bigger lists?
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);
let (left, right) = arr.split_at_mut(pivot_index);
quick_sort(left);
quick_sort(&mut right[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 choosing a pivot, dividing the array around it, and sorting the parts recursively.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The partition function loops through the array once to rearrange elements.
- How many times: Quick Sort calls itself recursively on smaller parts until the whole array is sorted.
When the pivot divides the array evenly, the work splits in half each time, leading to a balanced pattern.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 30 to 40 steps |
| 100 | About 700 to 800 steps |
| 1000 | About 10,000 to 12,000 steps |
Pattern observation: The steps grow a bit faster than n times but much slower than n squared, roughly n times log n.
Time Complexity: O(n log n)
This means sorting takes longer as the list grows, but the increase is moderate and efficient for most cases.
[X] Wrong: "Quick Sort always sorts in the same fast time no matter the input."
[OK] Correct: If the pivot is chosen poorly, like always the smallest or largest element, Quick Sort can slow down to much longer times.
Understanding Quick Sort's time complexity helps you explain why it is fast on average and what can cause it to slow down, showing your grasp of sorting efficiency.
"What if we always picked the middle element as pivot? How would the time complexity change?"
Practice
Quick Sort algorithm?Solution
Step 1: Understand Quick Sort's approach
Quick Sort picks a pivot element and divides the list into parts smaller and larger than the pivot.Step 2: Recursive sorting of parts
It then sorts these parts recursively, combining them to get the sorted list.Final Answer:
Divide the list into smaller parts using a pivot and sort them recursively -> Option DQuick Check:
Quick Sort = Divide and conquer [OK]
- Confusing Quick Sort with Bubble Sort
- Thinking Quick Sort merges lists like Merge Sort
- Believing Quick Sort always picks the first element as pivot
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 function signature correctness
fn quick_sort(arr: &mut [i32]) matches this: it takes &mut [i32] and returns nothing (unit type implied).Final Answer:
fn quick_sort(arr: &mut [i32]) -> Option AQuick Check:
Mutable slice parameter = fn quick_sort(arr: &mut [i32]) [OK]
- Using immutable slice &[] which cannot be changed
- Returning a new vector instead of sorting in place
- Using Vec<i32> without mutability for in-place sort
quick_sort(&mut arr)?
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 {
arr.swap(left, right);
if right == 0 { break; }
right -= 1;
}
}
arr.swap(0, right);
quick_sort(&mut arr[0..right]);
quick_sort(&mut arr[right+1..]);
}
fn main() {
let mut arr = [4, 2, 7, 1, 3];
quick_sort(&mut arr);
println!("{:?}", arr);
}Solution
Step 1: Understand partitioning around pivot
The pivot is 4. Elements <=4 move left, >4 move right. After partition, array becomes [3, 2, 1, 4, 7] with pivot at index 3.Step 2: Recursive calls sort left and right parts
Left part [3, 2, 1] sorts to [1, 2, 3], right part [7] is already sorted. Combined final array is [1, 2, 3, 4, 7].Final Answer:
[1, 2, 3, 4, 7] -> Option BQuick Check:
Sorted array = [1, 2, 3, 4, 7] [OK]
- Ignoring pivot swaps and recursive sorting
- Assuming array remains unchanged
- Mixing up left and right partitions
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 {
arr.swap(left, right);
right -= 1;
}
}
arr.swap(0, right);
quick_sort(&mut arr[0..right]);
quick_sort(&mut arr[right+1..]);
}Solution
Step 1: Analyze the while loop condition
The loop useswhile left < right, but it must include the case when left == right to correctly partition.Step 2: Effect of incorrect loop condition
Stopping early misses some comparisons, causing incorrect partition and sorting errors.Final Answer:
The while loop condition should be <= instead of < -> Option AQuick Check:
Loop condition must cover all indices [OK]
- Ignoring edge case when left equals right
- Changing pivot choice unnecessarily
- Misusing slice ranges in recursive calls
Solution
Step 1: Understand worst-case in Quick Sort
Picking the first element as pivot on sorted input causes unbalanced partitions, leading to worst-case O(n²) time.Step 2: Use random pivot to avoid worst-case
Choosing a random pivot spreads partitions more evenly, improving average performance and avoiding worst-case.Final Answer:
Pick a random element as pivot each time -> Option CQuick Check:
Random pivot = better average case [OK]
- Always picking first element as pivot
- Switching to Bubble Sort unnecessarily
- Sorting only part of the list
Ready to prove it?
Pass the test and this pattern moves unknown
