1004 — Quicksort
Tutorial
The Problem
Implement functional quicksort that mirrors the OCaml version: take a comparator gt, remove the pivot, partition the rest into smaller and larger halves, recursively sort both, and concatenate. Also provide an idiomatic Rust version using Vec::sort. Compare the allocation-heavy functional approach with the in-place standard library sort.
🎯 Learning Outcomes
.partition(|x| gt(&pivot, x))VecsVec::sort() and Vec::sort_by() as the idiomatic production alternativeF: Fn(&T, &T) -> bool + Copy for recursive usepartition to OCaml's List.partition (gt x) xs.sort() is O(n log n) with O(log n) stack — far better than functional O(n²) worst caseCode Example
pub fn quicksort<T: Clone + Ord, F: Fn(&T, &T) -> bool + Copy>(
gt: F,
mut xs: Vec<T>,
) -> Vec<T> {
if xs.is_empty() {
return xs;
}
let pivot = xs.remove(0);
let (ys, zs): (Vec<T>, Vec<T>) = xs.into_iter().partition(|x| gt(&pivot, x));
let mut left = quicksort(gt, ys);
let mut result = quicksort(gt, zs);
left.push(pivot);
left.append(&mut result);
left
}
// Usage
let sorted = quicksort(|a, b| a > b, vec![4, 65, 2, -31, 0, 99, 83, 782, 1]);
// Result: vec![-31, 0, 1, 2, 4, 65, 83, 99, 782]Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Pivot extraction | xs.remove(0) (O(n)) | x :: xs pattern (O(1)) |
| Partition | .into_iter().partition(pred) | List.partition (gt x) xs |
| Concatenation | left.push(pivot); left.append(&mut result) | ys @ (x :: zs) |
| Comparator bound | F: Copy for recursion | Passed naturally |
| Idiomatic sort | Vec::sort() (in-place) | List.sort compare lst |
| Allocation | O(n log n) new Vecs | O(n log n) new lists |
The functional quicksort is educational — it shows the algorithm clearly. The standard library sort is for production. Rust's Vec::sort uses pattern-defeating quicksort (pdqsort), which is cache-friendly and avoids worst-case O(n²) behaviour.
OCaml Approach
let rec quicksort gt = function | [] -> [] | x::xs -> let ys, zs = List.partition (gt x) xs in (quicksort gt ys) @ (x :: (quicksort gt zs)) is the complete OCaml implementation. List.partition (gt x) creates two lists in one pass. The @ operator concatenates the sorted halves. OCaml's pattern matching on lists is more concise than Rust's remove(0) approach.
Full Source
#![allow(clippy::all)]
/// Recursive quicksort matching the OCaml functional style.
///
/// This implementation closely mirrors the OCaml version:
/// - Takes a comparator function `gt` (for "greater than")
/// - Recursively partitions the list into smaller and larger elements
/// - Concatenates results using allocation (not in-place)
///
/// **Note:** This is functionally equivalent to OCaml but NOT idiomatic Rust.
/// See `quicksort_idiomatic` for the idiomatic approach.
///
/// The comparator must implement `Copy` to allow recursive calls.
pub fn quicksort<T: Clone + Ord, F: Fn(&T, &T) -> bool + Copy>(gt: F, mut xs: Vec<T>) -> Vec<T> {
if xs.is_empty() {
return xs;
}
let pivot = xs.remove(0);
let (ys, zs): (Vec<T>, Vec<T>) = xs.into_iter().partition(|x| gt(&pivot, x));
let mut left = quicksort(gt, ys);
let mut result = quicksort(gt, zs);
left.push(pivot);
left.append(&mut result);
left
}
/// Idiomatic Rust quicksort using the standard library's `sort_by`.
///
/// This is the **preferred approach** in production Rust:
/// - Uses proven, optimized std::sort (introsort: quicksort → heapsort)
/// - In-place, single-pass, O(n) space complexity advantage
/// - Leverages Rust's stability and compiler optimizations
///
/// **Trade-off:** Less educational about the algorithm itself, but vastly superior
/// performance and correctness in real-world code.
pub fn quicksort_idiomatic<T: Ord>(xs: Vec<T>) -> Vec<T> {
let mut result = xs;
result.sort();
result
}
/// Idiomatic Rust quicksort with custom comparator.
///
/// Like `quicksort_idiomatic`, but accepts a custom comparison function.
/// Still uses the optimized stdlib implementation under the hood.
pub fn quicksort_idiomatic_by<T, F: Fn(&T, &T) -> std::cmp::Ordering>(
mut xs: Vec<T>,
f: F,
) -> Vec<T> {
xs.sort_by(f);
xs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
let result: Vec<i32> = quicksort(|a, b| a > b, vec![]);
assert_eq!(result, vec![]);
}
#[test]
fn test_single_element() {
let result = quicksort(|a, b| a > b, vec![42]);
assert_eq!(result, vec![42]);
}
#[test]
fn test_multiple_elements() {
let result = quicksort(|a, b| a > b, vec![4, 65, 2, -31, 0, 99, 83, 782, 1]);
assert_eq!(result, vec![-31, 0, 1, 2, 4, 65, 83, 99, 782]);
}
#[test]
fn test_already_sorted() {
let result = quicksort(|a, b| a > b, vec![1, 2, 3, 4, 5]);
assert_eq!(result, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_reverse_sorted() {
let result = quicksort(|a, b| a > b, vec![5, 4, 3, 2, 1]);
assert_eq!(result, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_duplicates() {
let result = quicksort(|a, b| a > b, vec![3, 1, 3, 2, 1, 3]);
assert_eq!(result, vec![1, 1, 2, 3, 3, 3]);
}
#[test]
fn test_with_custom_comparator_descending() {
let result = quicksort(|a, b| a < b, vec![4, 65, 2, -31, 0, 99, 83, 782, 1]);
assert_eq!(result, vec![782, 99, 83, 65, 4, 2, 1, 0, -31]);
}
#[test]
fn test_idiomatic_multiple_elements() {
let result = quicksort_idiomatic(vec![4, 65, 2, -31, 0, 99, 83, 782, 1]);
assert_eq!(result, vec![-31, 0, 1, 2, 4, 65, 83, 99, 782]);
}
#[test]
fn test_idiomatic_by_descending() {
let result =
quicksort_idiomatic_by(vec![4, 65, 2, -31, 0, 99, 83, 782, 1], |a, b| b.cmp(a));
assert_eq!(result, vec![782, 99, 83, 65, 4, 2, 1, 0, -31]);
}
#[test]
fn test_strings_ascending() {
let result = quicksort_idiomatic(vec!["zebra", "apple", "banana", "cherry"]);
assert_eq!(result, vec!["apple", "banana", "cherry", "zebra"]);
}
#[test]
fn test_negative_numbers() {
let result = quicksort(|a, b| a > b, vec![-5, -1, -10, 0, 5, 1]);
assert_eq!(result, vec![-10, -5, -1, 0, 1, 5]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
let result: Vec<i32> = quicksort(|a, b| a > b, vec![]);
assert_eq!(result, vec![]);
}
#[test]
fn test_single_element() {
let result = quicksort(|a, b| a > b, vec![42]);
assert_eq!(result, vec![42]);
}
#[test]
fn test_multiple_elements() {
let result = quicksort(|a, b| a > b, vec![4, 65, 2, -31, 0, 99, 83, 782, 1]);
assert_eq!(result, vec![-31, 0, 1, 2, 4, 65, 83, 99, 782]);
}
#[test]
fn test_already_sorted() {
let result = quicksort(|a, b| a > b, vec![1, 2, 3, 4, 5]);
assert_eq!(result, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_reverse_sorted() {
let result = quicksort(|a, b| a > b, vec![5, 4, 3, 2, 1]);
assert_eq!(result, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_duplicates() {
let result = quicksort(|a, b| a > b, vec![3, 1, 3, 2, 1, 3]);
assert_eq!(result, vec![1, 1, 2, 3, 3, 3]);
}
#[test]
fn test_with_custom_comparator_descending() {
let result = quicksort(|a, b| a < b, vec![4, 65, 2, -31, 0, 99, 83, 782, 1]);
assert_eq!(result, vec![782, 99, 83, 65, 4, 2, 1, 0, -31]);
}
#[test]
fn test_idiomatic_multiple_elements() {
let result = quicksort_idiomatic(vec![4, 65, 2, -31, 0, 99, 83, 782, 1]);
assert_eq!(result, vec![-31, 0, 1, 2, 4, 65, 83, 99, 782]);
}
#[test]
fn test_idiomatic_by_descending() {
let result =
quicksort_idiomatic_by(vec![4, 65, 2, -31, 0, 99, 83, 782, 1], |a, b| b.cmp(a));
assert_eq!(result, vec![782, 99, 83, 65, 4, 2, 1, 0, -31]);
}
#[test]
fn test_strings_ascending() {
let result = quicksort_idiomatic(vec!["zebra", "apple", "banana", "cherry"]);
assert_eq!(result, vec!["apple", "banana", "cherry", "zebra"]);
}
#[test]
fn test_negative_numbers() {
let result = quicksort(|a, b| a > b, vec![-5, -1, -10, 0, 5, 1]);
assert_eq!(result, vec![-10, -5, -1, 0, 1, 5]);
}
}
Deep Comparison
Side-by-Side: OCaml vs. Rust Quicksort
Code Comparison
OCaml Version
(* Recursive quicksort with higher-order comparator *)
let rec quicksort gt = function
| [] -> []
| x::xs ->
let ys, zs = List.partition (gt x) xs in
(quicksort gt ys) @ (x :: (quicksort gt zs))
(* Usage *)
let _ = quicksort (>) [4; 65; 2; -31; 0; 99; 83; 782; 1]
(* Result: [-31; 0; 1; 2; 4; 65; 83; 99; 782] *)
Line count: 5 lines (excluding comments) Syntax highlights:
| [] -> [] and | x::xs ->List.partition for filtering@ operator for list concatenationgt passed implicitlyRust Version (Functional Style)
pub fn quicksort<T: Clone + Ord, F: Fn(&T, &T) -> bool + Copy>(
gt: F,
mut xs: Vec<T>,
) -> Vec<T> {
if xs.is_empty() {
return xs;
}
let pivot = xs.remove(0);
let (ys, zs): (Vec<T>, Vec<T>) = xs.into_iter().partition(|x| gt(&pivot, x));
let mut left = quicksort(gt, ys);
let mut result = quicksort(gt, zs);
left.push(pivot);
left.append(&mut result);
left
}
// Usage
let sorted = quicksort(|a, b| a > b, vec![4, 65, 2, -31, 0, 99, 83, 782, 1]);
// Result: vec![-31, 0, 1, 2, 4, 65, 83, 99, 782]
Line count: 18 lines (vs. 5 in OCaml) Syntax highlights:
<T: Clone + Ord, F: Fn(...) + Copy>.partition() instead of pattern matching.append(&mut result)|x| gt(&pivot, x) closure syntaxRust Version (Idiomatic)
pub fn quicksort_idiomatic<T: Ord>(xs: Vec<T>) -> Vec<T> {
let mut result = xs;
result.sort();
result
}
// Usage
let sorted = quicksort_idiomatic(vec![4, 65, 2, -31, 0, 99, 83, 782, 1]);
// Result: vec![-31, 0, 1, 2, 4, 65, 83, 99, 782]
Line count: 5 lines (matches OCaml in brevity, but uses stdlib) Syntax highlights:
T: OrdType Signature Comparison
| Aspect | OCaml | Rust (Functional) | Rust (Idiomatic) |
|---|---|---|---|
| Function name | quicksort | quicksort | quicksort_idiomatic |
| Type params | 'a (implicit) | T: Clone + Ord, F: Fn(...) + Copy | T: Ord |
| Comparator | gt (first param) | F: Fn(&T, &T) -> bool + Copy | Not needed (uses Ord) |
| Input | 'a list | Vec<T> | Vec<T> |
| Output | 'a list | Vec<T> | Vec<T> |
| Complexity | O(n²) worst, O(n log n) avg | O(n²) worst, O(n log n) avg | O(n log n) worst (introsort) |
| Memory model | Immutable, GC-managed | Owned, Clone-able elements | Owned, Ord elements |
| Generic | Polymorphic over any 'a | Requires Clone trait | Requires Ord trait only |
Execution Flow Comparison
OCaml Execution (Immutable)
quicksort (>) [4, 65, 2, -31, 0, 99, 83, 782, 1]
├─ pivot = 4
├─ ys = [...] (elements > 4, from List.partition)
├─ zs = [...] (elements ≤ 4)
├─ left = quicksort (>) ys
├─ right = quicksort (>) zs
└─ left @ [4] @ right = [-31, 0, 1, 2, 4, 65, 83, 99, 782]
Memory behavior:
@ operationRust Execution (Functional Style)
quicksort(|a, b| a > b, vec![4, 65, 2, -31, 0, 99, 83, 782, 1])
├─ pivot = xs.remove(0) // moves 4 out
├─ xs.into_iter().partition(...) // creates (ys, zs)
├─ left = quicksort(..., ys) // recursive call
├─ result = quicksort(..., zs) // recursive call
├─ left.push(pivot) // mutate in-place
└─ left.append(&mut result) // mutate in-place
Memory behavior:
.remove(0) shifts all elements (O(n) operation).partition() iterates once, creates two new vecs.push() and .append() mutate existing vecsRust Execution (Idiomatic)
quicksort_idiomatic(vec![4, 65, 2, -31, 0, 99, 83, 782, 1])
└─ result.sort() // introsort algorithm
├─ Phase 1: quicksort (depth = log n)
├─ Phase 2: heapsort (if too deep)
├─ Phase 3: insertion sort (small arrays < 16)
└─ In-place mutation of result
Memory behavior:
5 Key Insights
1. OCaml's Elegance vs. Rust's Explicitness
OCaml:
let rec quicksort gt = function
| [] -> []
| x::xs -> let ys, zs = List.partition (gt x) xs in
(quicksort gt ys) @ (x :: (quicksort gt zs))
Rust:
pub fn quicksort<T: Clone + Ord, F: Fn(&T, &T) -> bool + Copy>(gt: F, mut xs: Vec<T>) -> Vec<T> {
if xs.is_empty() { return xs; }
let pivot = xs.remove(0);
let (ys, zs): (Vec<T>, Vec<T>) = xs.into_iter().partition(|x| gt(&pivot, x));
let mut left = quicksort(gt, ys);
let mut result = quicksort(gt, zs);
left.push(pivot);
left.append(&mut result);
left
}
Key difference: OCaml achieves 5 lines with pattern matching and implicit polymorphism. Rust requires 18 lines because ownership, trait bounds, and explicit type parameters must be declared. However, Rust's explicitness catches more errors at compile-time.
Insight: Elegance doesn't scale. Rust trades brevity for correctness.
2. The Comparator Pattern: Functions as Data
| Language | Pattern | Flexibility |
|---|---|---|
| OCaml | let quicksort gt = ... where gt is a function | High: supports any binary predicate |
| Rust (Functional) | <F: Fn(...) + Copy> generic parameter | High but constrained: must implement Copy |
| Rust (Idiomatic) | Uses Ord trait | Medium: only ascending order by default, but sort_by() available |
Insight: *Rust's trait system enforces semantics. The Copy requirement on F isn't a limitation—it's a guarantee that the comparator is thread-safe and copyable.*
3. Allocation Patterns: Stack vs. Heap
OCaml:
Rust (Functional):
Vecs at each recursion level (allocations on heap)Rust (Idiomatic):
Insight: Even Rust's "functional" implementation is more imperative than OCaml due to vector allocations. The idiomatic version eliminates this overhead entirely.
4. Composability: Chaining vs. Pipeline
OCaml: Easy composition with other list operations
quicksort (>) input
|> List.filter (fun x -> x > 0)
|> List.map (fun x -> x * 2)
Rust (Functional): Possible but requires iterator adapters
quicksort(|a, b| a > b, input)
.into_iter()
.filter(|x| x > &0)
.map(|x| x * 2)
.collect()
Rust (Idiomatic): Can be chained before sorting
let sorted: Vec<_> = input
.into_iter()
.filter(|x| x > &0)
.map(|x| x * 2)
.collect::<Vec<_>>();
sorted.sort();
Insight: Rust's iterator paradigm is different but equally composable once you understand it. Immediate actions (like sorting) can't be lazily chained like in OCaml.
5. When to Break the Pattern: Pragmatism Over Purity
The Problem:
@ (list concatenation) is O(n) on the left list.remove(0) (removing head) is O(n) on the vectorThe Solution (Rust only):
std::sort (introsort) which is O(n log n) guaranteedOCaml's Option:
Rust's Advantage:
// One line. Battle-tested. O(n log n) worst-case. Done.
vec.sort();
Insight: Rust's ecosystem provides optimized primitives. Use them. The functional style is educational but idiomatic Rust chooses performance through library design, not through clever algorithms.
Summary Table
| Property | OCaml | Rust (Functional) | Rust (Idiomatic) |
|---|---|---|---|
| Lines of code | 5 | 18 | 5 |
| Type safety | Compile-time (ML type system) | Compile-time (Rust type system) | Compile-time + runtime traits |
| Memory model | GC, immutable | Owned, mutable | Owned, mutable |
| Worst-case perf | O(n²) | O(n²) | O(n log n) |
| Typical use | Algorithm teaching | Algorithm teaching | Production code |
| Composability | Excellent (pipeline) | Good (iterators) | Excellent (iterators) |
| Learning curve | Medium | High (ownership + traits) | Medium (stdlib knowledge) |
| Recommended for | Learning, teaching | Porting OCaml | All Rust code |
Lessons for Rust Developers
std::sort is better than any manual implementation.F: Fn(...) + Copy is powerful but requires understanding.mut keyword signals where mutation happens, making code review easier.Exercises
quicksort_desc<T: Ord>(xs: Vec<T>) -> Vec<T> that sorts in descending order using .sort_by(|a, b| b.cmp(a)).quicksort vs Vec::sort on a 10,000-element random Vec and report the speedup.quicksort_stable using sort_by with index tie-breaking to maintain relative order of equal elements.quicksort on sorted and reverse-sorted inputs.