master
1const std = @import("../std.zig");
2const sort = std.sort;
3const mem = std.mem;
4const math = std.math;
5const testing = std.testing;
6
7/// Unstable in-place sort. n best case, n*log(n) worst case and average case.
8/// log(n) memory (no allocator required).
9///
10/// Sorts in ascending order with respect to the given `lessThan` function.
11pub fn pdq(
12 comptime T: type,
13 items: []T,
14 context: anytype,
15 comptime lessThanFn: fn (context: @TypeOf(context), lhs: T, rhs: T) bool,
16) void {
17 const Context = struct {
18 items: []T,
19 sub_ctx: @TypeOf(context),
20
21 pub fn lessThan(ctx: @This(), a: usize, b: usize) bool {
22 return lessThanFn(ctx.sub_ctx, ctx.items[a], ctx.items[b]);
23 }
24
25 pub fn swap(ctx: @This(), a: usize, b: usize) void {
26 return mem.swap(T, &ctx.items[a], &ctx.items[b]);
27 }
28 };
29 pdqContext(0, items.len, Context{ .items = items, .sub_ctx = context });
30}
31
32const Hint = enum {
33 increasing,
34 decreasing,
35 unknown,
36};
37
38/// Unstable in-place sort. O(n) best case, O(n*log(n)) worst case and average case.
39/// O(log(n)) memory (no allocator required).
40/// `context` must have methods `swap` and `lessThan`,
41/// which each take 2 `usize` parameters indicating the index of an item.
42/// Sorts in ascending order with respect to `lessThan`.
43pub fn pdqContext(a: usize, b: usize, context: anytype) void {
44 // slices of up to this length get sorted using insertion sort.
45 const max_insertion = 24;
46 // number of allowed imbalanced partitions before switching to heap sort.
47 const max_limit = std.math.floorPowerOfTwo(usize, b - a) + 1;
48
49 // set upper bound on stack memory usage.
50 const Range = struct { a: usize, b: usize, limit: usize };
51 const stack_size = math.log2(math.maxInt(usize) + 1);
52 var stack: [stack_size]Range = undefined;
53 var range = Range{ .a = a, .b = b, .limit = max_limit };
54 var top: usize = 0;
55
56 while (true) {
57 var was_balanced = true;
58 var was_partitioned = true;
59
60 while (true) {
61 const len = range.b - range.a;
62
63 // very short slices get sorted using insertion sort.
64 if (len <= max_insertion) {
65 break sort.insertionContext(range.a, range.b, context);
66 }
67
68 // if too many bad pivot choices were made, simply fall back to heapsort in order to
69 // guarantee O(n*log(n)) worst-case.
70 if (range.limit == 0) {
71 break sort.heapContext(range.a, range.b, context);
72 }
73
74 // if the last partitioning was imbalanced, try breaking patterns in the slice by shuffling
75 // some elements around. Hopefully we'll choose a better pivot this time.
76 if (!was_balanced) {
77 breakPatterns(range.a, range.b, context);
78 range.limit -= 1;
79 }
80
81 // choose a pivot and try guessing whether the slice is already sorted.
82 var pivot: usize = 0;
83 var hint = chosePivot(range.a, range.b, &pivot, context);
84
85 if (hint == .decreasing) {
86 // The maximum number of swaps was performed, so items are likely
87 // in reverse order. Reverse it to make sorting faster.
88 reverseRange(range.a, range.b, context);
89 pivot = (range.b - 1) - (pivot - range.a);
90 hint = .increasing;
91 }
92
93 // if the last partitioning was decently balanced and didn't shuffle elements, and if pivot
94 // selection predicts the slice is likely already sorted...
95 if (was_balanced and was_partitioned and hint == .increasing) {
96 // try identifying several out-of-order elements and shifting them to correct
97 // positions. If the slice ends up being completely sorted, we're done.
98 if (partialInsertionSort(range.a, range.b, context)) break;
99 }
100
101 // if the chosen pivot is equal to the predecessor, then it's the smallest element in the
102 // slice. Partition the slice into elements equal to and elements greater than the pivot.
103 // This case is usually hit when the slice contains many duplicate elements.
104 if (range.a > a and !context.lessThan(range.a - 1, pivot)) {
105 range.a = partitionEqual(range.a, range.b, pivot, context);
106 continue;
107 }
108
109 // partition the slice.
110 var mid = pivot;
111 was_partitioned = partition(range.a, range.b, &mid, context);
112
113 const left_len = mid - range.a;
114 const right_len = range.b - mid;
115 const balanced_threshold = len / 8;
116 if (left_len < right_len) {
117 was_balanced = left_len >= balanced_threshold;
118 stack[top] = .{ .a = range.a, .b = mid, .limit = range.limit };
119 top += 1;
120 range.a = mid + 1;
121 } else {
122 was_balanced = right_len >= balanced_threshold;
123 stack[top] = .{ .a = mid + 1, .b = range.b, .limit = range.limit };
124 top += 1;
125 range.b = mid;
126 }
127 }
128
129 top = math.sub(usize, top, 1) catch break;
130 range = stack[top];
131 }
132}
133
134/// partitions `items[a..b]` into elements smaller than `items[pivot]`,
135/// followed by elements greater than or equal to `items[pivot]`.
136///
137/// sets the new pivot.
138/// returns `true` if already partitioned.
139fn partition(a: usize, b: usize, pivot: *usize, context: anytype) bool {
140 // move pivot to the first place
141 context.swap(a, pivot.*);
142
143 var i = a + 1;
144 var j = b - 1;
145
146 while (i <= j and context.lessThan(i, a)) i += 1;
147 while (i <= j and !context.lessThan(j, a)) j -= 1;
148
149 // check if items are already partitioned (no item to swap)
150 if (i > j) {
151 // put pivot back to the middle
152 context.swap(j, a);
153 pivot.* = j;
154 return true;
155 }
156
157 context.swap(i, j);
158 i += 1;
159 j -= 1;
160
161 while (true) {
162 while (i <= j and context.lessThan(i, a)) i += 1;
163 while (i <= j and !context.lessThan(j, a)) j -= 1;
164 if (i > j) break;
165
166 context.swap(i, j);
167 i += 1;
168 j -= 1;
169 }
170
171 // TODO: Enable the BlockQuicksort optimization
172
173 context.swap(j, a);
174 pivot.* = j;
175 return false;
176}
177
178/// partitions items into elements equal to `items[pivot]`
179/// followed by elements greater than `items[pivot]`.
180///
181/// it assumed that `items[a..b]` does not contain elements smaller than the `items[pivot]`.
182fn partitionEqual(a: usize, b: usize, pivot: usize, context: anytype) usize {
183 // move pivot to the first place
184 context.swap(a, pivot);
185
186 var i = a + 1;
187 var j = b - 1;
188
189 while (true) {
190 while (i <= j and !context.lessThan(a, i)) i += 1;
191 while (i <= j and context.lessThan(a, j)) j -= 1;
192 if (i > j) break;
193
194 context.swap(i, j);
195 i += 1;
196 j -= 1;
197 }
198
199 return i;
200}
201
202/// partially sorts a slice by shifting several out-of-order elements around.
203///
204/// returns `true` if the slice is sorted at the end. This function is `O(n)` worst-case.
205fn partialInsertionSort(a: usize, b: usize, context: anytype) bool {
206 @branchHint(.cold);
207
208 // maximum number of adjacent out-of-order pairs that will get shifted
209 const max_steps = 5;
210 // if the slice is shorter than this, don't shift any elements
211 const shortest_shifting = 50;
212
213 var i = a + 1;
214 for (0..max_steps) |_| {
215 // find the next pair of adjacent out-of-order elements.
216 while (i < b and !context.lessThan(i, i - 1)) i += 1;
217
218 // are we done?
219 if (i == b) return true;
220
221 // don't shift elements on short arrays, that has a performance cost.
222 if (b - a < shortest_shifting) return false;
223
224 // swap the found pair of elements. This puts them in correct order.
225 context.swap(i, i - 1);
226
227 // shift the smaller element to the left.
228 if (i - a >= 2) {
229 var j = i - 1;
230 while (j > a) : (j -= 1) {
231 if (!context.lessThan(j, j - 1)) break;
232 context.swap(j, j - 1);
233 }
234 }
235
236 // shift the greater element to the right.
237 if (b - i >= 2) {
238 var j = i + 1;
239 while (j < b) : (j += 1) {
240 if (!context.lessThan(j, j - 1)) break;
241 context.swap(j, j - 1);
242 }
243 }
244 }
245
246 return false;
247}
248
249fn breakPatterns(a: usize, b: usize, context: anytype) void {
250 @branchHint(.cold);
251
252 const len = b - a;
253 if (len < 8) return;
254
255 var rand = @as(u64, @intCast(len));
256 const modulus = math.ceilPowerOfTwoAssert(u64, len);
257
258 var i = a + (len / 4) * 2 - 1;
259 while (i <= a + (len / 4) * 2 + 1) : (i += 1) {
260 // xorshift64
261 rand ^= rand << 13;
262 rand ^= rand >> 7;
263 rand ^= rand << 17;
264
265 var other = @as(usize, @intCast(rand & (modulus - 1)));
266 if (other >= len) other -= len;
267 context.swap(i, a + other);
268 }
269}
270
271/// chooses a pivot in `items[a..b]`.
272/// swaps likely_sorted when `items[a..b]` seems to be already sorted.
273fn chosePivot(a: usize, b: usize, pivot: *usize, context: anytype) Hint {
274 // minimum length for using the Tukey's ninther method
275 const shortest_ninther = 50;
276 // max_swaps is the maximum number of swaps allowed in this function
277 const max_swaps = 4 * 3;
278
279 const len = b - a;
280 const i = a + len / 4 * 1;
281 const j = a + len / 4 * 2;
282 const k = a + len / 4 * 3;
283 var swaps: usize = 0;
284
285 if (len >= 8) {
286 if (len >= shortest_ninther) {
287 // find medians in the neighborhoods of `i`, `j` and `k`
288 sort3(i - 1, i, i + 1, &swaps, context);
289 sort3(j - 1, j, j + 1, &swaps, context);
290 sort3(k - 1, k, k + 1, &swaps, context);
291 }
292
293 // find the median among `i`, `j` and `k` and stores it in `j`
294 sort3(i, j, k, &swaps, context);
295 }
296
297 pivot.* = j;
298 return switch (swaps) {
299 0 => .increasing,
300 max_swaps => .decreasing,
301 else => .unknown,
302 };
303}
304
305fn sort3(a: usize, b: usize, c: usize, swaps: *usize, context: anytype) void {
306 if (context.lessThan(b, a)) {
307 swaps.* += 1;
308 context.swap(b, a);
309 }
310
311 if (context.lessThan(c, b)) {
312 swaps.* += 1;
313 context.swap(c, b);
314 }
315
316 if (context.lessThan(b, a)) {
317 swaps.* += 1;
318 context.swap(b, a);
319 }
320}
321
322fn reverseRange(a: usize, b: usize, context: anytype) void {
323 var i = a;
324 var j = b - 1;
325 while (i < j) {
326 context.swap(i, j);
327 i += 1;
328 j -= 1;
329 }
330}
331
332test "pdqContext respects arbitrary range boundaries" {
333 // Regression test for issue #25250
334 // pdqsort should never access indices outside the specified [a, b) range
335 var data: [2000]i32 = @splat(0);
336
337 // Fill with data that triggers the partialInsertionSort path
338 for (0..data.len) |i| {
339 data[i] = @intCast(@mod(@as(i32, @intCast(i)) * 7, 100));
340 }
341
342 const TestContext = struct {
343 items: []i32,
344 range_start: usize,
345 range_end: usize,
346
347 pub fn lessThan(ctx: @This(), a: usize, b: usize) bool {
348 // Assert indices are within the expected range
349 testing.expect(a >= ctx.range_start and a < ctx.range_end) catch @panic("index a out of range");
350 testing.expect(b >= ctx.range_start and b < ctx.range_end) catch @panic("index b out of range");
351 return ctx.items[a] < ctx.items[b];
352 }
353
354 pub fn swap(ctx: @This(), a: usize, b: usize) void {
355 // Assert indices are within the expected range
356 testing.expect(a >= ctx.range_start and a < ctx.range_end) catch @panic("index a out of range");
357 testing.expect(b >= ctx.range_start and b < ctx.range_end) catch @panic("index b out of range");
358 mem.swap(i32, &ctx.items[a], &ctx.items[b]);
359 }
360 };
361
362 // Test sorting a sub-range that doesn't start at 0
363 const start = 1118;
364 const end = 1764;
365 const ctx = TestContext{
366 .items = &data,
367 .range_start = start,
368 .range_end = end,
369 };
370
371 pdqContext(start, end, ctx);
372
373 // Verify the range is sorted
374 for ((start + 1)..end) |i| {
375 try testing.expect(data[i - 1] <= data[i]);
376 }
377}