a/sort/insertion.rs
1use core::mem::{self, MaybeUninit};
2use core::ptr;
3
4/// Sorts a slice in place using pair insertion sort with galloping search.
5///
6/// A sorted prefix grows from the left two elements at a time. The two new elements are
7/// ordered with one comparison, the larger one's place in the prefix is searched for from
8/// the right, and the smaller one's place is searched for only to the left of that, since it
9/// cannot lie anywhere else. Both then move into place together. This is the pair insertion
10/// sort that Java's dual-pivot quicksort uses for short ranges, with a galloping search in
11/// place of its linear scan.
12///
13/// Three refinements keep the algorithm cheap:
14///
15/// * **Sorted fast path.** The smaller element of a pair is compared with the last element of
16/// the prefix before any search. If it is not smaller, neither is the larger one, and the
17/// pair is done after two comparisons, one per element: already-sorted input costs exactly
18/// `n - 1` comparisons, as with plain insertion sort.
19/// * **Galloping search.** An insertion point is found by probing 1, 2, 4, 8, ... slots to the
20/// left of where the search starts, then binary searching the last gap. An element that
21/// moves `d` slots left costs at most `2⌊log₂ d⌋ + 2` comparisons: logarithmic in how far it
22/// moves rather than linear (a plain scan) or logarithmic in the length of the prefix (a
23/// binary search), so nearly-sorted input stays nearly linear.
24/// * **Pair moves.** Prefix elements greater than both new elements shift right by two slots
25/// at once and those between them by one, and each new element is written once into its
26/// final slot. A prefix element that both new elements pass therefore moves once instead of
27/// twice. Both insertion points are found before anything moves, so no comparison ever
28/// runs while an element is out of the slice. Short moves of elements of up to 32 bytes are
29/// made in place, four elements at a time, rather than by a call to `memmove`, which costs
30/// them more than the copying.
31///
32/// Against inserting the same elements one at a time with the same galloping search, a pair
33/// spends one comparison on ordering itself, and in exchange the smaller element's search
34/// starts from the larger one's insertion point instead of from the end of the prefix, and
35/// the prefix elements past that point move once instead of twice. On 1000 elements, counting
36/// a move each time an element changes slot:
37///
38/// | Input (`n = 1000`) | Comparisons, pair | one at a time | Moves, pair | one at a time |
39/// |----------------------------|-------------------|---------------|-------------|---------------|
40/// | Sorted | 999 | 999 | 0 | 0 |
41/// | Random, distinct (mean) | ≈ 14,580 | ≈ 14,920 | ≈ 167,400 | ≈ 250,600 |
42/// | Reverse sorted | 5,986 | 9,966 | 250,998 | 500,499 |
43/// | Worst case, per column | 17,020 | 17,169 | 250,999 | 500,499 |
44///
45/// On random input the two searches of a pair together cost slightly less than two single
46/// ones, and a third of the moves are saved (`≈ n²/6` against `≈ n²/4`). On reverse-sorted
47/// input both halve: the larger element goes to the front, which leaves the smaller one
48/// nothing to search and every prefix element to pass only once.
49///
50/// # Properties
51///
52/// * **Stable**: equal elements keep their relative order. A pair is reordered only when its
53/// second element is strictly smaller, and each element is inserted after every prefix
54/// element that is not greater than it.
55/// * **In place**: `O(1)` auxiliary memory, no allocation.
56/// * **Adaptive**: comparisons grow with the logarithm of how far elements move, moves with
57/// the distance itself.
58///
59/// Let `dₓ` be the number of elements before `x` in the input that are greater than `x`, which
60/// is how many slots `x` moves left, and `I = Σ dₓ` the number of inversions.
61///
62/// | Case | Comparisons | Moves |
63/// |--------------------------|------------------------------------|--------------------------|
64/// | Best (already sorted) | `n - 1` | `0` |
65/// | Any input | `≤ n - 1 + Σₓ 2⌈log₂(dₓ + 1)⌉` | `≤ I + n` |
66/// | Reverse sorted | `≈ (n/2)·log₂ n + n` | `≈ n²/4` (the worst) |
67/// | Worst | `≤ 2n·log₂ n` (`≈ 2n·log₂ n - 3n`) | `≈ n²/4` |
68///
69/// The adaptive bound is tight: input such as `[1, 0, 3, 2, 5, 4, ...]` comes within two
70/// comparisons of it. By concavity it is below `3n + 2n·log₂(1 + I/n)`, so input with few
71/// inversions sorts in close to linear time. The worst case needs every element to move a long
72/// way, but not to the front, where a search is cut short. On reverse-sorted input, a pair
73/// inserted into a prefix of `i` elements costs exactly `⌈log₂ i⌉ + 3` comparisons and
74/// `i + 2` moves, the most a pair can move (only the first pair, whose larger element ends in
75/// the slot it started in, moves two), and the last element when `n` is even costs
76/// `⌈log₂(n - 1)⌉ + 1` comparisons and `n` moves.
77///
78/// Insertion sort moves `O(n²)` elements on random input and is mainly of educational value
79/// beyond short or nearly-sorted slices; prefer [`slice::sort`] for real workloads.
80///
81/// # Panics
82///
83/// Does not panic by itself. If the [`Ord`] implementation of `T` panics, the panic is
84/// propagated and `arr` is left in an unspecified order, but it still holds every original
85/// element exactly once: every comparison is made while all elements are in the slice, and
86/// the moves that follow cannot panic.
87///
88/// # Examples
89///
90/// ```
91/// use a::sort::insertion;
92///
93/// let mut v = [5, 1, 4, 2, 8, 0];
94/// insertion(&mut v);
95/// assert_eq!(v, [0, 1, 2, 4, 5, 8]);
96///
97/// // Equal keys keep their order: sorting by the first field leaves 'a' before 'b'.
98/// #[derive(Debug, PartialEq, Eq)]
99/// struct Card(u8, char);
100/// impl Ord for Card {
101/// fn cmp(&self, other: &Self) -> std::cmp::Ordering {
102/// self.0.cmp(&other.0)
103/// }
104/// }
105/// impl PartialOrd for Card {
106/// fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
107/// Some(self.cmp(other))
108/// }
109/// }
110/// let mut cards = [Card(2, 'a'), Card(1, 'x'), Card(2, 'b'), Card(0, 'y')];
111/// insertion(&mut cards);
112/// assert_eq!(cards, [Card(0, 'y'), Card(1, 'x'), Card(2, 'a'), Card(2, 'b')]);
113///
114/// let mut words = vec!["pear", "apple", "fig"];
115/// insertion(&mut words);
116/// assert_eq!(words, ["apple", "fig", "pear"]);
117/// ```
118pub fn insertion<T: Ord>(arr: &mut [T]) {
119 let len = arr.len();
120 // `arr[..i]` is sorted. A lone first element is, so the rest is taken in pairs, with one
121 // element left over when `len` is even.
122 let mut i = 1;
123 while i + 1 < len {
124 insert_pair(&mut arr[..i + 2]);
125 i += 2;
126 }
127 if i < len {
128 insert_last(arr);
129 }
130}
131
132/// Inserts the last two elements of `v` into the sorted rest of it, which is not empty.
133fn insert_pair<T: Ord>(v: &mut [T]) {
134 let i = v.len() - 2;
135 // Order the pair. Only a second element strictly smaller than the first counts as out of
136 // order, so equal elements keep their order.
137 let swapped = v[i + 1] < v[i];
138 let s = if swapped { i + 1 } else { i };
139
140 // Fast path: the smaller element does not go before the last element of the prefix, so
141 // the larger one does not either.
142 if v[s] >= v[i - 1] {
143 if swapped {
144 v.swap(i, i + 1);
145 }
146 return;
147 }
148 if mem::size_of::<T>() <= SHIFT_INLINE_SIZE {
149 insert_pair_far_out_of_line(v, swapped);
150 } else {
151 insert_pair_far(v, swapped);
152 }
153}
154
155/// [`insert_pair_far`] kept out of line, for elements whose short moves [`shift_up`] makes
156/// itself: inlined, those moves made the compiler keep their addresses up to date in the loop
157/// of [`insertion`], which cost sorted integers 1.3–1.45 times. Larger elements, whose moves
158/// all go to `memmove`, lose 1.05–1.1 times on short random slices to the call instead.
159#[inline(never)]
160fn insert_pair_far_out_of_line<T: Ord>(v: &mut [T], swapped: bool) {
161 insert_pair_far(v, swapped);
162}
163
164/// [`insert_pair`] past the fast path: the smaller element goes before the last element of the
165/// prefix.
166#[inline(always)]
167fn insert_pair_far<T: Ord>(v: &mut [T], swapped: bool) {
168 let i = v.len() - 2;
169 let (s, l) = if swapped { (i + 1, i) } else { (i, i + 1) };
170 let (prefix, pair) = v.split_at(i);
171 let (small, large) = (&pair[s - i], &pair[l - i]);
172 // Each element goes after every prefix element that is not greater than it, so it ends
173 // up `d` slots to the left of where its search starts, `d` being the number of prefix
174 // elements greater than it there.
175 let pl = i - gallop(prefix, large, 0);
176 // The smaller element cannot go past the larger one, so only `prefix[..pl]` is searched.
177 // If the larger element stays at the end, the fast-path comparison has already shown
178 // that the smaller one goes before `prefix[i - 1]`.
179 let ps = pl - gallop(&prefix[..pl], small, usize::from(pl == i));
180
181 if !swapped && pl == i {
182 // The larger element is already in its final slot; only the smaller one moves. The
183 // general path below would give the same result, at the cost of reading the larger
184 // element out and writing it back.
185 move_last_to_front(&mut v[ps..=i]);
186 return;
187 }
188 let base = v.as_mut_ptr();
189 // SAFETY: `ps <= pl <= i` and `i + 2 == v.len()`, so every pointer stays inside `v`.
190 // Both pair elements are read out first, which leaves `v[i]` and `v[i + 1]` logically
191 // uninitialised. Moving `v[pl..i]` two slots right into `v[pl + 2..i + 2]` fills those
192 // two slots and vacates `v[pl]` and `v[pl + 1]`; moving `v[ps..pl]` one slot right into
193 // `v[ps + 1..pl + 1]` fills `v[pl]` and vacates `v[ps]` (`ptr::copy` allows the overlap
194 // both times). The two elements read out are then written into the two vacant slots,
195 // `v[ps]` and `v[pl + 1]`. Every comparison has already been made and nothing in between
196 // can panic, so `v` holds every element exactly once again when the block ends.
197 unsafe {
198 let small = ptr::read(base.add(s));
199 let large = ptr::read(base.add(l));
200 shift_up(base.add(pl), 2, i - pl);
201 shift_up(base.add(ps), 1, pl - ps);
202 ptr::write(base.add(ps), small);
203 ptr::write(base.add(pl + 1), large);
204 }
205}
206
207/// Inserts the last element of `v` into the sorted rest of it.
208fn insert_last<T: Ord>(v: &mut [T]) {
209 let i = v.len() - 1;
210 let (prefix, last) = v.split_at(i);
211 let d = gallop(prefix, &last[0], 0);
212 if d > 0 {
213 move_last_to_front(&mut v[i - d..]);
214 }
215}
216
217/// Returns how many elements at the end of the sorted slice `v` are greater than `x`, given
218/// that the last `known` of them are (`known` is at most `v.len()`).
219///
220/// Probes 1, 2, 4, ... slots from the end, skipping those already known, until it meets an
221/// element that is not greater than `x` or reaches the front, then binary searches the gap
222/// between the last two probes. An answer `d >= 1` costs at most `2⌊log₂ d⌋ + 2` comparisons,
223/// one fewer when `known` is 1, and `d = 0` costs one.
224///
225/// The result never exceeds `v.len()`, whatever the [`Ord`] implementation answers.
226fn gallop<T: Ord>(v: &[T], x: &T, known: usize) -> usize {
227 let n = v.len();
228 // The answer lies in `lo..hi`.
229 let (mut lo, mut hi) = (known, n + 1);
230 let mut k = 1;
231 while k <= lo {
232 k = k.saturating_mul(2);
233 }
234 loop {
235 if k >= n {
236 // The probe would reach the front or past it: probe the front element instead.
237 if lo < n {
238 if *x < v[0] {
239 lo = n;
240 } else {
241 hi = n;
242 }
243 }
244 break;
245 }
246 if *x < v[n - k] {
247 lo = k;
248 k = k.saturating_mul(2);
249 } else {
250 hi = k;
251 break;
252 }
253 }
254 while hi - lo > 1 {
255 let mid = lo + (hi - lo) / 2;
256 if *x < v[n - mid] {
257 lo = mid;
258 } else {
259 hi = mid;
260 }
261 }
262 lo
263}
264
265/// Moves the last element of `v` to the front, shifting every other element one slot right.
266fn move_last_to_front<T>(v: &mut [T]) {
267 let Some(last) = v.len().checked_sub(1) else {
268 return;
269 };
270 let base = v.as_mut_ptr();
271 // SAFETY: `last < v.len()`, so every pointer stays inside `v`. Reading out `v[last]`
272 // leaves that slot logically uninitialised; moving `v[..last]` one slot right into
273 // `v[1..]` (`ptr::copy` allows the overlap) fills it and vacates `v[0]`, where the element
274 // read out is written. Nothing in between can panic, so `v` holds every element exactly
275 // once again when the block ends.
276 unsafe {
277 let x = ptr::read(base.add(last));
278 shift_up(base, 1, last);
279 ptr::write(base, x);
280 }
281}
282
283/// The largest element size, in bytes, for which [`shift_up`] moves short stretches itself.
284const SHIFT_INLINE_SIZE: usize = 32;
285
286/// The longest stretch [`shift_up`] moves itself; longer ones go to `memmove`.
287const SHIFT_INLINE_MAX: usize = 32;
288
289/// Moves the `count` elements at `src` up by `by` slots, as `ptr::copy(src, src.add(by),
290/// count)` does, but for short stretches of small elements by itself, four elements at a time
291/// from the top: a call to `memmove` costs such moves more than the copying. Measured for
292/// integers: 1.05–1.3 times faster on short or nearly sorted slices and up to 1.5 times on
293/// 16 reverse-sorted elements, within 1.03 times either way on long random input, whose moves
294/// are mostly long; strings (24 bytes) gain up to 1.08 times on short slices.
295///
296/// # Safety
297///
298/// As for `ptr::copy(src, src.add(by), count)`.
299#[inline(always)]
300unsafe fn shift_up<T>(src: *mut T, by: usize, count: usize) {
301 // SAFETY: as the caller guarantees, `src[..count + by]` is valid for reads and writes.
302 // Every block (or element) is read whole before it is written `by` slots higher, and they
303 // go from the top down: what has been written lies at or above the top of the block read,
304 // so every block is read before any of its slots is overwritten, as `ptr::copy` would.
305 // The elements are moved as `MaybeUninit` bits, which nothing drops.
306 unsafe {
307 if mem::size_of::<T>() <= SHIFT_INLINE_SIZE && count <= SHIFT_INLINE_MAX {
308 let mut j = count;
309 while j >= 4 {
310 j -= 4;
311 let block = ptr::read_unaligned(src.add(j).cast::<[MaybeUninit<T>; 4]>());
312 ptr::write_unaligned(src.add(j + by).cast::<[MaybeUninit<T>; 4]>(), block);
313 }
314 while j > 0 {
315 j -= 1;
316 let x = ptr::read(src.add(j).cast::<MaybeUninit<T>>());
317 ptr::write(src.add(j + by).cast::<MaybeUninit<T>>(), x);
318 }
319 } else {
320 ptr::copy(src, src.add(by), count);
321 }
322 }
323}