Skip to content

Commit 3883324

Browse files
committed
Add a quicksort_adversary utility (#178)
1 parent 393ac87 commit 3883324

File tree

5 files changed

+150
-0
lines changed

5 files changed

+150
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ discussion](https://stackoverflow.com/q/2786899/1364752) on StackOverflow and ar
229229
backed by the article [*Applying Sorting Networks to Synthesize Optimized Sorting
230230
Libraries*](https://arxiv.org/abs/1505.01962).
231231

232+
* The algorithm behind `utility::quicksort_adversary` is a fairly straightforward adaptation of the
233+
one provided by M. D. McIlroy in [*A Killer Adversary for Quicksort*](https://www.cs.dartmouth.edu/~doug/mdmspe.pdf).
234+
232235
* The test suite reimplements random number algorithms originally found in the following places:
233236
- [xoshiro256\*\*](https://prng.di.unimi.it/)
234237
- [*Optimal Discrete Uniform Generation from Coin Flips, and Applications*](https://arxiv.org/abs/1304.1916)

docs/Miscellaneous-utilities.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,31 @@ auto m = get<foo_tag>(mm);
367367

368368
`utility::metrics` is still mostly experimental and unused in the rest of the library. As such this documentation is voluntarily thin.
369369

370+
### `quicksort_adversary`
371+
372+
```cpp
373+
#include <cpp-sort/utility/quicksort_adversary.h>
374+
```
375+
376+
`utility::quicksort_adversary` is a function template that implements an algorithm described by M. D. McIlroy in [*A Killer Adversary for Quicksort*][quicksort-adversary], which attempts to trigger the quadratic case of many quicksort implementations by trying to guess the pivot and forcing the testing comparison to perform a certain set of comparisons.
377+
378+
```cpp
379+
template<typename Sorter, typename Integer>
380+
auto quicksort_adversary(Sorter&& sorter, Integer size);
381+
```
382+
383+
The function accepts a sorter to test, and a parameter corresponding to the size of the input for which we wish to test the sorter. It then instantiates a collection of `size` elements of `Integer` type that it passes to `sorter`, and returns the result of the operation. It additionally passes a custom comparison function to `sorter`, which means that it only works with *comparison sorters*.
384+
385+
It can be used together with [`metrics::comparisons`][metrics-comparisons] or some other metrics to analyze the number of operations performed, and attempt to detect quadratic behavior in quicksort-like sorters:
386+
387+
```cpp
388+
auto sorter = cppsort::metrics::comparisons(cppsort::quick_sort);
389+
auto comps = cppsort::utility::quicksort_adversary(sorter, 1000);
390+
std::print("Comparisons: {}", comps.value());
391+
```
392+
393+
*New in version 2.1.0*
394+
370395
### `size`
371396

372397
```cpp
@@ -474,9 +499,11 @@ auto swap_index_pairs_force_unroll(RandomAccessIterator first,
474499
[fixed-size-sorters]: Fixed-size-sorters.md
475500
[is-stable]: Sorter-traits.md#is_stable
476501
[metrics]: Metrics.md
502+
[metrics-comparisons]: Metrics.md#comparisons
477503
[numpy-argsort]: https://numpy.org/doc/stable/reference/generated/numpy.argsort.html
478504
[p0022]: https://wg21.link/P0022
479505
[pdq-sorter]: Sorters.md#pdq_sorter
506+
[quicksort-adversary]: https://www.cs.dartmouth.edu/~doug/mdmspe.pdf
480507
[range-v3]: https://github.com/ericniebler/range-v3
481508
[sorter-adapters]: Sorter-adapters.md
482509
[sorters]: Sorters.md
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright (c) 2025 Morwenn
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
#ifndef CPPSORT_QUICKSORT_ADVERSARY_H_
6+
#define CPPSORT_QUICKSORT_ADVERSARY_H_
7+
8+
////////////////////////////////////////////////////////////
9+
// Headers
10+
////////////////////////////////////////////////////////////
11+
#include <numeric>
12+
#include <vector>
13+
14+
namespace cppsort::utility
15+
{
16+
// Implementation of a quicksort adversary as described by M. D. McIlroy
17+
// in *A Killer Adversary for Quicksort*
18+
19+
template<typename Sorter, typename Integer>
20+
auto quicksort_adversary(Sorter&& sorter, Integer size)
21+
{
22+
Integer solid = 0;
23+
auto gas = size - 1;
24+
std::vector<Integer> elements(size, gas);
25+
26+
std::vector<Integer> values(size, 0);
27+
std::iota(values.begin(), values.end(), 0);
28+
29+
int pivot_candidate = size; // Too big to match any
30+
return sorter(values, [&, gas](Integer lhs_idx, Integer rhs_idx) {
31+
int& lhs = elements[lhs_idx];
32+
int& rhs = elements[rhs_idx];
33+
if (lhs == gas && rhs == gas) {
34+
if (lhs_idx == pivot_candidate) {
35+
lhs = solid++;
36+
} else {
37+
rhs = solid++;
38+
}
39+
}
40+
if (lhs == gas) {
41+
pivot_candidate = lhs_idx;
42+
} else if (rhs == gas) {
43+
pivot_candidate = rhs_idx;
44+
}
45+
return lhs < rhs;
46+
});
47+
}
48+
}
49+
50+
#endif // CPPSORT_QUICKSORT_ADVERSARY_H_

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ add_executable(main-tests
266266
utility/chainable_projections.cpp
267267
utility/iter_swap.cpp
268268
utility/metric_tools.cpp
269+
utility/quicksort_adversary.cpp
269270
utility/sorted_indices.cpp
270271
utility/sorted_iterators.cpp
271272
utility/sorting_networks.cpp
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright (c) 2025 Morwenn
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
#include <algorithm>
6+
#include <iterator>
7+
#include <vector>
8+
#include <catch2/catch_template_test_macros.hpp>
9+
#include <cpp-sort/detail/iter_sort3.h>
10+
#include <cpp-sort/sorters.h>
11+
#include <cpp-sort/metrics/comparisons.h>
12+
#include <cpp-sort/utility/functional.h>
13+
#include <cpp-sort/utility/quicksort_adversary.h>
14+
15+
TEMPLATE_TEST_CASE( "test quicksort-based sorters with quicksort_adversary", "[utility][quicksort_adversary]",
16+
cppsort::pdq_sorter,
17+
cppsort::quick_merge_sorter,
18+
cppsort::quick_sorter,
19+
cppsort::std_sorter )
20+
{
21+
cppsort::metrics::comparisons<TestType> sorter;
22+
auto comps = cppsort::utility::quicksort_adversary(sorter, 1000);
23+
CHECK( comps < 100'000 ); // Guesstimate between n and n²
24+
}
25+
26+
namespace
27+
{
28+
// Median-of-three quicksort
29+
template<typename Iterator, typename Compare>
30+
auto median_of_3_quicksort(Iterator first, Iterator last, Compare comp)
31+
-> void
32+
{
33+
auto size = last - first;
34+
if (size < 2) return;
35+
36+
auto middle = first + size / 2;
37+
auto pivot_pos = cppsort::detail::iter_sort3(
38+
first, middle, last - 1,
39+
comp, cppsort::utility::identity{}
40+
);
41+
42+
iter_swap(pivot_pos, last - 1);
43+
auto middle1 = std::partition(
44+
first, last - 1,
45+
[&](int& value) { return comp(value, *(last - 1)); }
46+
);
47+
48+
iter_swap(middle1, last - 1);
49+
auto middle2 = std::partition(
50+
std::next(middle1), last,
51+
[&](int& value) { return not comp(*middle1, value); }
52+
);
53+
54+
median_of_3_quicksort(first, middle1, comp);
55+
median_of_3_quicksort(middle2, last, comp);
56+
}
57+
}
58+
59+
60+
TEST_CASE( "quicksort adversary over a simple quicksort",
61+
"[utility][quicksort_adversary]" )
62+
{
63+
auto do_sort = [](std::vector<int>& vec, auto comp) {
64+
return median_of_3_quicksort(vec.begin(), vec.end(), comp);
65+
};
66+
auto sorter = cppsort::metrics::comparisons(do_sort);
67+
auto comps = cppsort::utility::quicksort_adversary(sorter, 100);
68+
CHECK( comps > 5000 ); // Guesstimate over n²
69+
}

0 commit comments

Comments
 (0)