Skip to content

Commit 3d11b65

Browse files
committed
Add Median of Medians fallback to introselect
1 parent f3d597b commit 3d11b65

File tree

3 files changed

+301
-140
lines changed

3 files changed

+301
-140
lines changed

library/core/src/slice/mod.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ mod index;
4242
mod iter;
4343
mod raw;
4444
mod rotate;
45+
mod select;
4546
mod specialize;
4647

4748
#[unstable(feature = "str_internals", issue = "none")]
@@ -2776,7 +2777,7 @@ impl<T> [T] {
27762777
where
27772778
T: Ord,
27782779
{
2779-
sort::partition_at_index(self, index, T::lt)
2780+
select::partition_at_index(self, index, T::lt)
27802781
}
27812782

27822783
/// Reorder the slice with a comparator function such that the element at `index` is at its
@@ -2831,7 +2832,7 @@ impl<T> [T] {
28312832
where
28322833
F: FnMut(&T, &T) -> Ordering,
28332834
{
2834-
sort::partition_at_index(self, index, |a: &T, b: &T| compare(a, b) == Less)
2835+
select::partition_at_index(self, index, |a: &T, b: &T| compare(a, b) == Less)
28352836
}
28362837

28372838
/// Reorder the slice with a key extraction function such that the element at `index` is at its
@@ -2887,7 +2888,7 @@ impl<T> [T] {
28872888
F: FnMut(&T) -> K,
28882889
K: Ord,
28892890
{
2890-
sort::partition_at_index(self, index, |a: &T, b: &T| f(a).lt(&f(b)))
2891+
select::partition_at_index(self, index, |a: &T, b: &T| f(a).lt(&f(b)))
28912892
}
28922893

28932894
/// Moves all consecutive repeated elements to the end of the slice according to the

library/core/src/slice/select.rs

+292
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
//! Slice selection
2+
//!
3+
//! This module contains the implementation for `slice::select_nth_unstable`.
4+
//! It uses an introselect algorithm based on Orson Peters' pattern-defeating quicksort,
5+
//! published at: <https://github.com/orlp/pdqsort>
6+
//!
7+
//! The fallback algorithm used for introselect is Median of Medians using Tukey's Ninther
8+
//! for pivot selection. Using this as a fallback ensures O(n) worst case running time with
9+
//! better performance than one would get using heapsort as fallback.
10+
11+
use crate::cmp;
12+
use crate::mem::{self, SizedTypeProperties};
13+
use crate::slice::sort::{
14+
break_patterns, choose_pivot, insertion_sort_shift_left, partition, partition_equal,
15+
};
16+
17+
// For slices of up to this length it's probably faster to simply sort them.
18+
// Defined at the module scope because it's used in multiple functions.
19+
const MAX_INSERTION: usize = 10;
20+
21+
fn partition_at_index_loop<'a, T, F>(
22+
mut v: &'a mut [T],
23+
mut index: usize,
24+
is_less: &mut F,
25+
mut pred: Option<&'a T>,
26+
) where
27+
F: FnMut(&T, &T) -> bool,
28+
{
29+
// Limit the amount of iterations and fall back to fast deterministic selection
30+
// to ensure O(n) worst case running time. This limit needs to be constant, because
31+
// using `ilog2(len)` like in `sort` would result in O(n log n) time complexity.
32+
// The exact value of the limit is chosen somewhat arbitrarily, but for most inputs bad pivot
33+
// selections should be relatively rare, so the limit usually shouldn't be reached
34+
// anyways.
35+
let mut limit = 16;
36+
37+
// True if the last partitioning was reasonably balanced.
38+
let mut was_balanced = true;
39+
40+
loop {
41+
if v.len() <= MAX_INSERTION {
42+
if v.len() > 1 {
43+
insertion_sort_shift_left(v, 1, is_less);
44+
}
45+
return;
46+
}
47+
48+
if limit == 0 {
49+
median_of_medians(v, is_less, index);
50+
return;
51+
}
52+
53+
// If the last partitioning was imbalanced, try breaking patterns in the slice by shuffling
54+
// some elements around. Hopefully we'll choose a better pivot this time.
55+
if !was_balanced {
56+
break_patterns(v);
57+
limit -= 1;
58+
}
59+
60+
// Choose a pivot
61+
let (pivot, _) = choose_pivot(v, is_less);
62+
63+
// If the chosen pivot is equal to the predecessor, then it's the smallest element in the
64+
// slice. Partition the slice into elements equal to and elements greater than the pivot.
65+
// This case is usually hit when the slice contains many duplicate elements.
66+
if let Some(p) = pred {
67+
if !is_less(p, &v[pivot]) {
68+
let mid = partition_equal(v, pivot, is_less);
69+
70+
// If we've passed our index, then we're good.
71+
if mid > index {
72+
return;
73+
}
74+
75+
// Otherwise, continue sorting elements greater than the pivot.
76+
v = &mut v[mid..];
77+
index = index - mid;
78+
pred = None;
79+
continue;
80+
}
81+
}
82+
83+
let (mid, _) = partition(v, pivot, is_less);
84+
was_balanced = cmp::min(mid, v.len() - mid) >= v.len() / 8;
85+
86+
// Split the slice into `left`, `pivot`, and `right`.
87+
let (left, right) = v.split_at_mut(mid);
88+
let (pivot, right) = right.split_at_mut(1);
89+
let pivot = &pivot[0];
90+
91+
if mid < index {
92+
v = right;
93+
index = index - mid - 1;
94+
pred = Some(pivot);
95+
} else if mid > index {
96+
v = left;
97+
} else {
98+
// If mid == index, then we're done, since partition() guaranteed that all elements
99+
// after mid are greater than or equal to mid.
100+
return;
101+
}
102+
}
103+
}
104+
105+
/// Reorder the slice such that the element at `index` is at its final sorted position.
106+
pub fn partition_at_index<T, F>(
107+
v: &mut [T],
108+
index: usize,
109+
mut is_less: F,
110+
) -> (&mut [T], &mut T, &mut [T])
111+
where
112+
F: FnMut(&T, &T) -> bool,
113+
{
114+
if index >= v.len() {
115+
panic!("partition_at_index index {} greater than length of slice {}", index, v.len());
116+
}
117+
118+
if T::IS_ZST {
119+
// Sorting has no meaningful behavior on zero-sized types. Do nothing.
120+
} else if index == v.len() - 1 {
121+
// Find max element and place it in the last position of the array. We're free to use
122+
// `unwrap()` here because we know v must not be empty.
123+
let (max_index, _) = v.iter().enumerate().max_by(from_is_less(&mut is_less)).unwrap();
124+
v.swap(max_index, index);
125+
} else if index == 0 {
126+
// Find min element and place it in the first position of the array. We're free to use
127+
// `unwrap()` here because we know v must not be empty.
128+
let (min_index, _) = v.iter().enumerate().min_by(from_is_less(&mut is_less)).unwrap();
129+
v.swap(min_index, index);
130+
} else {
131+
partition_at_index_loop(v, index, &mut is_less, None);
132+
}
133+
134+
let (left, right) = v.split_at_mut(index);
135+
let (pivot, right) = right.split_at_mut(1);
136+
let pivot = &mut pivot[0];
137+
(left, pivot, right)
138+
}
139+
140+
/// helper function used to find the index of the min/max element
141+
/// using e.g. `slice.iter().enumerate().min_by(from_is_less(&mut is_less)).unwrap()`
142+
fn from_is_less<T>(
143+
is_less: &mut impl FnMut(&T, &T) -> bool,
144+
) -> impl FnMut(&(usize, &T), &(usize, &T)) -> cmp::Ordering + '_ {
145+
|&(_, x), &(_, y)| {
146+
if is_less(x, y) { cmp::Ordering::Less } else { cmp::Ordering::Greater }
147+
}
148+
}
149+
150+
/// Selection algorithm to select the k-th element from the slice in guaranteed O(n) time.
151+
/// This is essentially a quickselect that uses Tukey's Ninther for pivot selection
152+
fn median_of_medians<T, F: FnMut(&T, &T) -> bool>(mut v: &mut [T], is_less: &mut F, mut k: usize) {
153+
// Since this function isn't public, it should never be called with an out-of-bounds index.
154+
debug_assert!(k < v.len());
155+
156+
// If T is as ZST, `partition_at_index` will already return early.
157+
debug_assert!(!T::IS_ZST);
158+
159+
// We now know that `k < v.len() <= isize::MAX`
160+
loop {
161+
if v.len() <= MAX_INSERTION {
162+
if v.len() > 1 {
163+
insertion_sort_shift_left(v, 1, is_less);
164+
}
165+
return;
166+
}
167+
168+
// `median_of_{minima,maxima}` can't handle the extreme cases of the first/last element,
169+
// so we catch them here and just do a linear search.
170+
if k == v.len() - 1 {
171+
// Find max element and place it in the last position of the array. We're free to use
172+
// `unwrap()` here because we know v must not be empty.
173+
let (max_index, _) = v.iter().enumerate().max_by(from_is_less(is_less)).unwrap();
174+
v.swap(max_index, k);
175+
return;
176+
} else if k == 0 {
177+
// Find min element and place it in the first position of the array. We're free to use
178+
// `unwrap()` here because we know v must not be empty.
179+
let (min_index, _) = v.iter().enumerate().min_by(from_is_less(is_less)).unwrap();
180+
v.swap(min_index, k);
181+
return;
182+
}
183+
184+
let p = median_of_ninthers(v, is_less);
185+
186+
if p == k {
187+
return;
188+
} else if p > k {
189+
v = &mut v[..p];
190+
} else {
191+
// Since `p < k < v.len()`, `p + 1` doesn't overflow and is
192+
// a valid index into the slice.
193+
v = &mut v[p + 1..];
194+
k -= p + 1;
195+
}
196+
}
197+
}
198+
199+
// Optimized for when `k` lies somewhere in the middle of the slice. Selects a pivot
200+
// as close as possible to the median of the slice. For more details on how the algorithm
201+
// operates, refer to the paper <https://drops.dagstuhl.de/opus/volltexte/2017/7612/pdf/LIPIcs-SEA-2017-24.pdf>.
202+
fn median_of_ninthers<T, F: FnMut(&T, &T) -> bool>(v: &mut [T], is_less: &mut F) -> usize {
203+
// use `saturating_mul` so the multiplication doesn't overflow on 16-bit platforms.
204+
let frac = if v.len() <= 1024 {
205+
v.len() / 12
206+
} else if v.len() <= 128_usize.saturating_mul(1024) {
207+
v.len() / 64
208+
} else {
209+
v.len() / 1024
210+
};
211+
212+
let pivot = frac / 2;
213+
let lo = v.len() / 2 - pivot;
214+
let hi = frac + lo;
215+
let gap = (v.len() - 9 * frac) / 4;
216+
let mut a = lo - 4 * frac - gap;
217+
let mut b = hi + gap;
218+
for i in lo..hi {
219+
ninther(v, is_less, a, i - frac, b, a + 1, i, b + 1, a + 2, i + frac, b + 2);
220+
a += 3;
221+
b += 3;
222+
}
223+
224+
median_of_medians(&mut v[lo..lo + frac], is_less, pivot);
225+
partition(v, lo + pivot, is_less).0
226+
}
227+
228+
/// Moves around the 9 elements at the indices a..i, such that
229+
/// `v[d]` contains the median of the 9 elements and the other
230+
/// elements are partitioned around it.
231+
fn ninther<T, F: FnMut(&T, &T) -> bool>(
232+
v: &mut [T],
233+
is_less: &mut F,
234+
a: usize,
235+
mut b: usize,
236+
c: usize,
237+
mut d: usize,
238+
e: usize,
239+
mut f: usize,
240+
g: usize,
241+
mut h: usize,
242+
i: usize,
243+
) {
244+
b = median_idx(v, is_less, a, b, c);
245+
h = median_idx(v, is_less, g, h, i);
246+
if is_less(&v[h], &v[b]) {
247+
mem::swap(&mut b, &mut h);
248+
}
249+
if is_less(&v[f], &v[d]) {
250+
mem::swap(&mut d, &mut f);
251+
}
252+
if is_less(&v[e], &v[d]) {
253+
// do nothing
254+
} else if is_less(&v[f], &v[e]) {
255+
d = f;
256+
} else {
257+
if is_less(&v[e], &v[b]) {
258+
v.swap(e, b);
259+
} else if is_less(&v[h], &v[e]) {
260+
v.swap(e, h);
261+
}
262+
return;
263+
}
264+
if is_less(&v[d], &v[b]) {
265+
d = b;
266+
} else if is_less(&v[h], &v[d]) {
267+
d = h;
268+
}
269+
270+
v.swap(d, e);
271+
}
272+
273+
/// returns the index pointing to the median of the 3
274+
/// elements `v[a]`, `v[b]` and `v[c]`
275+
fn median_idx<T, F: FnMut(&T, &T) -> bool>(
276+
v: &[T],
277+
is_less: &mut F,
278+
mut a: usize,
279+
b: usize,
280+
mut c: usize,
281+
) -> usize {
282+
if is_less(&v[c], &v[a]) {
283+
mem::swap(&mut a, &mut c);
284+
}
285+
if is_less(&v[c], &v[b]) {
286+
return c;
287+
}
288+
if is_less(&v[b], &v[a]) {
289+
return a;
290+
}
291+
b
292+
}

0 commit comments

Comments
 (0)