ExamplesBy LevelBy TopicLearning Paths
1004 Intermediate

1004 — Quicksort

Functional Programming

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

  • • Implement recursive quicksort using .partition(|x| gt(&pivot, x))
  • • Understand why the functional version allocates: each recursive call creates new Vecs
  • • Use Vec::sort() and Vec::sort_by() as the idiomatic production alternative
  • • Pass comparators as F: Fn(&T, &T) -> bool + Copy for recursive use
  • • Map Rust's partition to OCaml's List.partition (gt x) xs
  • • Recognise that .sort() is O(n log n) with O(log n) stack — far better than functional O(n²) worst case
  • Code 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

    AspectRustOCaml
    Pivot extractionxs.remove(0) (O(n))x :: xs pattern (O(1))
    Partition.into_iter().partition(pred)List.partition (gt x) xs
    Concatenationleft.push(pivot); left.append(&mut result)ys @ (x :: zs)
    Comparator boundF: Copy for recursionPassed naturally
    Idiomatic sortVec::sort() (in-place)List.sort compare lst
    AllocationO(n log n) new VecsO(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]);
        }
    }
    ✓ Tests Rust test suite
    #[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:

  • • Pattern matching on list structure: | [] -> [] and | x::xs ->
  • • Built-in List.partition for filtering
  • • Infix @ operator for list concatenation
  • • Higher-order function gt passed implicitly

  • Rust 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:

  • • Explicit type parameters: <T: Clone + Ord, F: Fn(...) + Copy>
  • • Iterator-based .partition() instead of pattern matching
  • • Mutable references in .append(&mut result)
  • • Explicit |x| gt(&pivot, x) closure syntax

  • Rust 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:

  • • Single trait bound: T: Ord
  • • Delegates to proven stdlib implementation
  • • No recursive structure visible

  • Type Signature Comparison

    AspectOCamlRust (Functional)Rust (Idiomatic)
    Function namequicksortquicksortquicksort_idiomatic
    Type params'a (implicit)T: Clone + Ord, F: Fn(...) + CopyT: Ord
    Comparatorgt (first param)F: Fn(&T, &T) -> bool + CopyNot needed (uses Ord)
    Input'a listVec<T>Vec<T>
    Output'a listVec<T>Vec<T>
    ComplexityO(n²) worst, O(n log n) avgO(n²) worst, O(n log n) avgO(n log n) worst (introsort)
    Memory modelImmutable, GC-managedOwned, Clone-able elementsOwned, Ord elements
    GenericPolymorphic over any 'aRequires Clone traitRequires 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:

  • • New list created at each @ operation
  • • Garbage collector reclaims old list nodes
  • • Immutability ensures no aliasing issues

  • Rust 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 vecs
  • • No garbage collection; RAII cleanup on scope exit

  • Rust 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:

  • • Single allocation; no recursive copies
  • • All work done in-place
  • • Automatic switch to heapsort prevents O(n²) worst-case

  • 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

    LanguagePatternFlexibility
    OCamllet quicksort gt = ... where gt is a functionHigh: supports any binary predicate
    Rust (Functional)<F: Fn(...) + Copy> generic parameterHigh but constrained: must implement Copy
    Rust (Idiomatic)Uses Ord traitMedium: 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:

  • • Creates intermediate list structures at each recursion level
  • • Garbage collector manages cleanup
  • • Multiple pointer indirections (linked list)
  • Rust (Functional):

  • • Creates two new Vecs at each recursion level (allocations on heap)
  • • Automatic RAII cleanup on scope exit
  • • Vectors are contiguous (better cache locality than linked lists)
  • Rust (Idiomatic):

  • • Single allocation; all work in-place
  • • No intermediate vectors
  • • Minimal memory overhead
  • 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:

  • • OCaml's @ (list concatenation) is O(n) on the left list
  • • Rust's .remove(0) (removing head) is O(n) on the vector
  • • Both are inefficient for quicksort!
  • The Solution (Rust only):

  • Don't use functional quicksort in production
  • • Use std::sort (introsort) which is O(n log n) guaranteed
  • OCaml's Option:

  • • Could use a mutable array-based quicksort (less idiomatic)
  • • Or accept the functional overhead for algorithmic clarity
  • 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

    PropertyOCamlRust (Functional)Rust (Idiomatic)
    Lines of code5185
    Type safetyCompile-time (ML type system)Compile-time (Rust type system)Compile-time + runtime traits
    Memory modelGC, immutableOwned, mutableOwned, mutable
    Worst-case perfO(n²)O(n²)O(n log n)
    Typical useAlgorithm teachingAlgorithm teachingProduction code
    ComposabilityExcellent (pipeline)Good (iterators)Excellent (iterators)
    Learning curveMediumHigh (ownership + traits)Medium (stdlib knowledge)
    Recommended forLearning, teachingPorting OCamlAll Rust code

    Lessons for Rust Developers

  • Know your standard library. std::sort is better than any manual implementation.
  • Trait bounds enable generic programming. F: Fn(...) + Copy is powerful but requires understanding.
  • Ownership isn't a limitation; it's a feature. Rust prevents entire classes of bugs through ownership semantics.
  • Mutability is intentional. The mut keyword signals where mutation happens, making code review easier.
  • Performance and safety are not trade-offs in Rust. They're unified through the type system.
  • Exercises

  • Add a quicksort_desc<T: Ord>(xs: Vec<T>) -> Vec<T> that sorts in descending order using .sort_by(|a, b| b.cmp(a)).
  • Implement three-way partition quicksort that handles equal elements more efficiently.
  • Benchmark the functional quicksort vs Vec::sort on a 10,000-element random Vec and report the speedup.
  • Add a quicksort_stable using sort_by with index tie-breaking to maintain relative order of equal elements.
  • In OCaml, implement merge sort and compare its performance with quicksort on sorted and reverse-sorted inputs.
  • Open Source Repos