075 — Merge Sort
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
merge function for combining two sorted slices in O(n+m)F: Fn(&T, &T) -> Orderinglen <= 1 (a list of 0 or 1 elements is already sorted)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
&[i32]) and produces new Vec<i32>. OCaml sorts list and produces new lists. List-based merge sort is purely functional; slice-based requires cloning.Vec::split_at_mut and rotate_left. OCaml's immutable lists cannot be sorted in place.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]
);
}
}#[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
Rust Approach
sort() (introsort)Comparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Split | Pattern match / List.nth | split_at(mid) |
| Merge | Recursive cons | Push to Vec |
| In-place | No (functional) | Possible but complex |
| Built-in | List.sort | .sort() (introsort) |
Exercises
&mut Vec<i32> using rotate_left for the merge step. This avoids cloning but is O(n² log n) due to rotation cost.timsort_lite that detects ascending runs and merges them.