Skip to content

Commit 80ee2ad

Browse files
0xDevNinja0xDevNinja
authored andcommitted
feat(sorting): add inversion count via merge sort
1 parent 99672ad commit 80ee2ad

2 files changed

Lines changed: 160 additions & 0 deletions

File tree

src/sorting/inversion_count.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
//! Inversion count via merge sort.
2+
//!
3+
//! An inversion is a pair of indices `(i, j)` with `i < j` and `arr[i] > arr[j]`.
4+
//! Counting them naively is `O(n^2)`; piggy-backing the count onto a merge sort
5+
//! brings it down to `O(n log n)` time with `O(n)` auxiliary space.
6+
//!
7+
//! Key insight: during the merge step, when we pick an element from the right
8+
//! half because it is strictly smaller than the current left-half element,
9+
//! every remaining element in the left half forms an inversion with it. So we
10+
//! add `left.len() - left_idx` to the running counter.
11+
//!
12+
//! Equal elements are not counted as inversions.
13+
//!
14+
//! Complexity: `O(n log n)` time, `O(n)` extra space.
15+
//! Returns `u64` so counts up to `n*(n-1)/2` fit comfortably for `n` up to a
16+
//! few billion.
17+
18+
/// Counts the number of inversions in `arr` — pairs `(i, j)` with `i < j` and
19+
/// `arr[i] > arr[j]`.
20+
///
21+
/// Runs a non-mutating merge sort over a cloned working buffer and tallies
22+
/// inversions during each merge.
23+
pub fn inversion_count<T: Ord + Clone>(arr: &[T]) -> u64 {
24+
if arr.len() < 2 {
25+
return 0;
26+
}
27+
let mut buf: Vec<T> = arr.to_vec();
28+
let mut scratch: Vec<T> = arr.to_vec();
29+
sort_count(&mut buf, &mut scratch)
30+
}
31+
32+
fn sort_count<T: Ord + Clone>(slice: &mut [T], scratch: &mut [T]) -> u64 {
33+
let n = slice.len();
34+
if n < 2 {
35+
return 0;
36+
}
37+
let mid = n / 2;
38+
let (left, right) = slice.split_at_mut(mid);
39+
let (sl, sr) = scratch.split_at_mut(mid);
40+
let mut count = sort_count(left, sl);
41+
count += sort_count(right, sr);
42+
count += merge_count(left, right, scratch);
43+
slice.clone_from_slice(&scratch[..n]);
44+
count
45+
}
46+
47+
fn merge_count<T: Ord + Clone>(left: &[T], right: &[T], out: &mut [T]) -> u64 {
48+
let (mut i, mut j, mut k) = (0, 0, 0);
49+
let mut inv: u64 = 0;
50+
while i < left.len() && j < right.len() {
51+
if left[i] <= right[j] {
52+
out[k] = left[i].clone();
53+
i += 1;
54+
} else {
55+
out[k] = right[j].clone();
56+
// every remaining element in `left` is greater than `right[j]`.
57+
inv += (left.len() - i) as u64;
58+
j += 1;
59+
}
60+
k += 1;
61+
}
62+
while i < left.len() {
63+
out[k] = left[i].clone();
64+
i += 1;
65+
k += 1;
66+
}
67+
while j < right.len() {
68+
out[k] = right[j].clone();
69+
j += 1;
70+
k += 1;
71+
}
72+
inv
73+
}
74+
75+
#[cfg(test)]
76+
mod tests {
77+
use super::inversion_count;
78+
79+
fn brute_force<T: Ord>(arr: &[T]) -> u64 {
80+
let mut count: u64 = 0;
81+
for i in 0..arr.len() {
82+
for j in (i + 1)..arr.len() {
83+
if arr[i] > arr[j] {
84+
count += 1;
85+
}
86+
}
87+
}
88+
count
89+
}
90+
91+
#[test]
92+
fn empty() {
93+
let a: Vec<i32> = vec![];
94+
assert_eq!(inversion_count(&a), 0);
95+
}
96+
97+
#[test]
98+
fn single() {
99+
assert_eq!(inversion_count(&[42]), 0);
100+
}
101+
102+
#[test]
103+
fn sorted() {
104+
assert_eq!(inversion_count(&[1, 2, 3, 4, 5, 6]), 0);
105+
}
106+
107+
#[test]
108+
fn reverse_sorted() {
109+
for n in 0..=10usize {
110+
let v: Vec<i32> = (0..n as i32).rev().collect();
111+
let expected = (n as u64) * (n.saturating_sub(1) as u64) / 2;
112+
assert_eq!(inversion_count(&v), expected);
113+
}
114+
}
115+
116+
#[test]
117+
fn small_known() {
118+
assert_eq!(inversion_count(&[2, 4, 1, 3, 5]), 3);
119+
}
120+
121+
#[test]
122+
fn duplicates_only() {
123+
assert_eq!(inversion_count(&[1, 1, 1]), 0);
124+
assert_eq!(inversion_count(&[7, 7, 7, 7, 7]), 0);
125+
}
126+
127+
#[test]
128+
fn duplicates_mixed() {
129+
// (3,1),(3,2),(3,2) -> 3 inversions
130+
assert_eq!(inversion_count(&[3, 1, 2, 2]), 3);
131+
}
132+
133+
#[test]
134+
fn strings() {
135+
let v = vec!["pear", "apple", "banana"];
136+
assert_eq!(inversion_count(&v), 2);
137+
}
138+
139+
#[test]
140+
fn matches_brute_force_small() {
141+
// Deterministic LCG for reproducibility — no rand dependency.
142+
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
143+
for _ in 0..40 {
144+
state = state
145+
.wrapping_mul(6_364_136_223_846_793_005)
146+
.wrapping_add(1);
147+
let len = ((state >> 32) as usize) % 12;
148+
let mut v: Vec<i32> = Vec::with_capacity(len);
149+
for _ in 0..len {
150+
state = state
151+
.wrapping_mul(6_364_136_223_846_793_005)
152+
.wrapping_add(1);
153+
v.push(((state >> 40) as i32) % 7);
154+
}
155+
assert_eq!(inversion_count(&v), brute_force(&v), "mismatch on {v:?}");
156+
}
157+
}
158+
}

src/sorting/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ pub mod bit_vector_sort;
4444
pub mod fisher_yates_shuffle;
4545

4646
pub mod rotate_matrix;
47+
48+
pub mod inversion_count;

0 commit comments

Comments
 (0)