ExamplesBy LevelBy TopicLearning Paths
075 Intermediate

075 — Merge Sort

Functional Programming

Tutorial

The Problem

Merge sort (John von Neumann, 1945) is the canonical divide-and-conquer sorting algorithm: split the list in half, sort each half recursively, merge the sorted halves. It guarantees O(n log n) in all cases — unlike quicksort which degrades to O(n²) on sorted input. It is the algorithm used in Rust's Vec::sort (TimSort) and Java's Arrays.sort for objects.

Merge sort is naturally recursive and maps directly to functional programming patterns: the merge function is a fold over two sorted lists, and the sort function is a structural recursion on list halves. Understanding merge sort deeply unlocks intuition for all divide-and-conquer algorithms.

🎯 Learning Outcomes

  • • Implement the two-phase merge sort: split, recurse, merge
  • • Write a merge function for combining two sorted slices in O(n+m)
  • • Implement the generic version with a custom comparator F: Fn(&T, &T) -> Ordering
  • • Understand why the base case is len <= 1 (a list of 0 or 1 elements is already sorted)
  • • Compare recursive merge sort with Rust's built-in sort (TimSort)
  • Code Example

    #![allow(clippy::all)]
    // 075: Merge Sort
    
    // Approach 1: Classic recursive merge sort
    fn merge(l1: &[i32], l2: &[i32]) -> Vec<i32> {
        let mut result = Vec::with_capacity(l1.len() + l2.len());
        let (mut i, mut j) = (0, 0);
        while i < l1.len() && j < l2.len() {
            if l1[i] <= l2[j] {
                result.push(l1[i]);
                i += 1;
            } else {
                result.push(l2[j]);
                j += 1;
            }
        }
        result.extend_from_slice(&l1[i..]);
        result.extend_from_slice(&l2[j..]);
        result
    }
    
    fn merge_sort(v: &[i32]) -> Vec<i32> {
        if v.len() <= 1 {
            return v.to_vec();
        }
        let mid = v.len() / 2;
        let left = merge_sort(&v[..mid]);
        let right = merge_sort(&v[mid..]);
        merge(&left, &right)
    }
    
    // Approach 2: Generic with comparator
    fn merge_sort_by<T: Clone, F>(v: &[T], cmp: &F) -> Vec<T>
    where
        F: Fn(&T, &T) -> std::cmp::Ordering,
    {
        if v.len() <= 1 {
            return v.to_vec();
        }
        let mid = v.len() / 2;
        let left = merge_sort_by(&v[..mid], cmp);
        let right = merge_sort_by(&v[mid..], cmp);
        let mut result = Vec::with_capacity(v.len());
        let (mut i, mut j) = (0, 0);
        while i < left.len() && j < right.len() {
            if cmp(&left[i], &right[j]) != std::cmp::Ordering::Greater {
                result.push(left[i].clone());
                i += 1;
            } else {
                result.push(right[j].clone());
                j += 1;
            }
        }
        result.extend_from_slice(&left[i..]);
        result.extend_from_slice(&right[j..]);
        result
    }
    
    // Approach 3: Functional style with iterators
    fn merge_sort_fn(v: &[i32]) -> Vec<i32> {
        if v.len() <= 1 {
            return v.to_vec();
        }
        let mid = v.len() / 2;
        merge(&merge_sort_fn(&v[..mid]), &merge_sort_fn(&v[mid..]))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_merge_sort() {
            assert_eq!(
                merge_sort(&[5, 3, 8, 1, 9, 2, 7]),
                vec![1, 2, 3, 5, 7, 8, 9]
            );
            assert_eq!(merge_sort(&[]), Vec::<i32>::new());
            assert_eq!(merge_sort(&[1]), vec![1]);
            assert_eq!(merge_sort(&[2, 1]), vec![1, 2]);
        }
    
        #[test]
        fn test_merge_sort_by() {
            let v = vec![5, 3, 8, 1];
            assert_eq!(
                merge_sort_by(&v, &|a: &i32, b: &i32| a.cmp(b)),
                vec![1, 3, 5, 8]
            );
            assert_eq!(
                merge_sort_by(&v, &|a: &i32, b: &i32| b.cmp(a)),
                vec![8, 5, 3, 1]
            );
        }
    
        #[test]
        fn test_merge_sort_fn() {
            assert_eq!(
                merge_sort_fn(&[5, 3, 8, 1, 9, 2, 7]),
                vec![1, 2, 3, 5, 7, 8, 9]
            );
        }
    }

    Key Differences

  • Slice vs list: Rust sorts slices (&[i32]) and produces new Vec<i32>. OCaml sorts list and produces new lists. List-based merge sort is purely functional; slice-based requires cloning.
  • In-place option: Rust can implement in-place merge sort using Vec::split_at_mut and rotate_left. OCaml's immutable lists cannot be sorted in place.
  • Stability: Rust's sort is stable (preserves relative order of equal elements). sort_unstable is faster but not stable. OCaml's List.sort is not stable; List.stable_sort is.
  • **split_at**: Rust's v.split_at(mid) is O(1). OCaml must traverse to the midpoint: let rec split_at n = function ... | x :: t -> let (l, r) = split_at (n-1) t in (x :: l, r) — O(n/2).
  • OCaml Approach

    OCaml's merge sort uses recursive pattern matching on lists:

    let rec merge l1 l2 = match l1, l2 with
      | [], l | l, [] -> l
      | x :: t1, y :: t2 ->
        if x <= y then x :: merge t1 l2
        else y :: merge l1 t2
    
    let rec merge_sort = function
      | ([] | [_]) as lst -> lst
      | lst ->
        let mid = List.length lst / 2 in
        let left = List.filteri (fun i _ -> i < mid) lst in
        let right = List.filteri (fun i _ -> i >= mid) lst in
        merge (merge_sort left) (merge_sort right)
    

    The list-based version is purely functional with no mutation. List.stable_sort in OCaml's stdlib is an optimized merge sort.

    Full Source

    #![allow(clippy::all)]
    // 075: Merge Sort
    
    // Approach 1: Classic recursive merge sort
    fn merge(l1: &[i32], l2: &[i32]) -> Vec<i32> {
        let mut result = Vec::with_capacity(l1.len() + l2.len());
        let (mut i, mut j) = (0, 0);
        while i < l1.len() && j < l2.len() {
            if l1[i] <= l2[j] {
                result.push(l1[i]);
                i += 1;
            } else {
                result.push(l2[j]);
                j += 1;
            }
        }
        result.extend_from_slice(&l1[i..]);
        result.extend_from_slice(&l2[j..]);
        result
    }
    
    fn merge_sort(v: &[i32]) -> Vec<i32> {
        if v.len() <= 1 {
            return v.to_vec();
        }
        let mid = v.len() / 2;
        let left = merge_sort(&v[..mid]);
        let right = merge_sort(&v[mid..]);
        merge(&left, &right)
    }
    
    // Approach 2: Generic with comparator
    fn merge_sort_by<T: Clone, F>(v: &[T], cmp: &F) -> Vec<T>
    where
        F: Fn(&T, &T) -> std::cmp::Ordering,
    {
        if v.len() <= 1 {
            return v.to_vec();
        }
        let mid = v.len() / 2;
        let left = merge_sort_by(&v[..mid], cmp);
        let right = merge_sort_by(&v[mid..], cmp);
        let mut result = Vec::with_capacity(v.len());
        let (mut i, mut j) = (0, 0);
        while i < left.len() && j < right.len() {
            if cmp(&left[i], &right[j]) != std::cmp::Ordering::Greater {
                result.push(left[i].clone());
                i += 1;
            } else {
                result.push(right[j].clone());
                j += 1;
            }
        }
        result.extend_from_slice(&left[i..]);
        result.extend_from_slice(&right[j..]);
        result
    }
    
    // Approach 3: Functional style with iterators
    fn merge_sort_fn(v: &[i32]) -> Vec<i32> {
        if v.len() <= 1 {
            return v.to_vec();
        }
        let mid = v.len() / 2;
        merge(&merge_sort_fn(&v[..mid]), &merge_sort_fn(&v[mid..]))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_merge_sort() {
            assert_eq!(
                merge_sort(&[5, 3, 8, 1, 9, 2, 7]),
                vec![1, 2, 3, 5, 7, 8, 9]
            );
            assert_eq!(merge_sort(&[]), Vec::<i32>::new());
            assert_eq!(merge_sort(&[1]), vec![1]);
            assert_eq!(merge_sort(&[2, 1]), vec![1, 2]);
        }
    
        #[test]
        fn test_merge_sort_by() {
            let v = vec![5, 3, 8, 1];
            assert_eq!(
                merge_sort_by(&v, &|a: &i32, b: &i32| a.cmp(b)),
                vec![1, 3, 5, 8]
            );
            assert_eq!(
                merge_sort_by(&v, &|a: &i32, b: &i32| b.cmp(a)),
                vec![8, 5, 3, 1]
            );
        }
    
        #[test]
        fn test_merge_sort_fn() {
            assert_eq!(
                merge_sort_fn(&[5, 3, 8, 1, 9, 2, 7]),
                vec![1, 2, 3, 5, 7, 8, 9]
            );
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_merge_sort() {
            assert_eq!(
                merge_sort(&[5, 3, 8, 1, 9, 2, 7]),
                vec![1, 2, 3, 5, 7, 8, 9]
            );
            assert_eq!(merge_sort(&[]), Vec::<i32>::new());
            assert_eq!(merge_sort(&[1]), vec![1]);
            assert_eq!(merge_sort(&[2, 1]), vec![1, 2]);
        }
    
        #[test]
        fn test_merge_sort_by() {
            let v = vec![5, 3, 8, 1];
            assert_eq!(
                merge_sort_by(&v, &|a: &i32, b: &i32| a.cmp(b)),
                vec![1, 3, 5, 8]
            );
            assert_eq!(
                merge_sort_by(&v, &|a: &i32, b: &i32| b.cmp(a)),
                vec![8, 5, 3, 1]
            );
        }
    
        #[test]
        fn test_merge_sort_fn() {
            assert_eq!(
                merge_sort_fn(&[5, 3, 8, 1, 9, 2, 7]),
                vec![1, 2, 3, 5, 7, 8, 9]
            );
        }
    }

    Deep Comparison

    Core Insight

    Merge sort: split into halves, recursively sort each half, merge sorted halves. O(n log n) guaranteed. The functional version is particularly elegant because merging sorted lists is a natural recursive operation.

    OCaml Approach

  • • Pattern match on list to split
  • • Recursive merge of two sorted lists
  • • No mutation — creates new lists at each step
  • Rust Approach

  • • Split slice at midpoint
  • • Recursive sort on sub-slices
  • • Merge into new Vec — or use sort() (introsort)
  • Comparison Table

    FeatureOCamlRust
    SplitPattern match / List.nthsplit_at(mid)
    MergeRecursive consPush to Vec
    In-placeNo (functional)Possible but complex
    Built-inList.sort.sort() (introsort)

    Exercises

  • In-place merge sort: Implement an in-place merge sort for &mut Vec<i32> using rotate_left for the merge step. This avoids cloning but is O(n² log n) due to rotation cost.
  • External sort: Merge sort naturally extends to external sorting (data too large for RAM). Design an algorithm that reads sorted chunks from disk and merges them. What data structure manages the merge heap?
  • Natural merge sort: Detect pre-sorted runs and use them as the base for merging (this is the core of TimSort). Implement timsort_lite that detects ascending runs and merges them.
  • Open Source Repos