13

Given a vector<T> vec{...} what's the best way to extract its minimum, maximum and median assuming T is one of the numeric types? I know of std::nth_element as well as std::minmax_element but they seem to do redundant work if called one after another.

The best idea I came up with so far is to just call std::nth_element 3 times one after another. But this still needs 3N comparisons, right? Is there any way to reuse the partial sorting done in previous iterations?

Tony Delroy
  • 94,554
  • 11
  • 158
  • 229
user1709708
  • 1,507
  • 2
  • 13
  • 23
  • 3
    The most efficient way will be to manually loop and calculate all at the same time. – freakish Jun 05 '19 at 07:20
  • 3
    @freakish How would you get a median by iterating over elements? – Daniel Langr Jun 05 '19 at 07:21
  • It doesn't solve the median problem, but the Standard Library does provide [`std::minmax_element`](https://en.cppreference.com/w/cpp/algorithm/minmax_element). Perhaps you could model your own algorithm on this - finding the median will involve checking every element anyway, so it is reasonably straightforward to extend a median algorithm to also return min/max. – BoBTFish Jun 05 '19 at 07:22
  • 2
    `std::partial_sort()`? – Shawn Jun 05 '19 at 07:22
  • @DanielLangr right, so median can be solved recursively and so can min/max. You can do it in one go I think. So not literally "loop" but equivalent linear time. – freakish Jun 05 '19 at 07:22
  • Relevant question: [What is the right approach when using STL container for median calculation?](https://stackoverflow.com/q/1719070/580083). – Daniel Langr Jun 05 '19 at 07:23
  • 6
    `std::nth_element`, then `std::min_element` in the left half and `std::max_element` in the right one. – Evg Jun 05 '19 at 07:23
  • Fastest way might be to `std::sort` the entire array, and pick the values after that. – vll Jun 05 '19 at 07:26
  • @Evg: Indeed, you can extract the median with a partition as your comment (which I've converted to an answer) states. – Bathsheba Jun 05 '19 at 07:28
  • 1
    @DanielLangr, https://en.wikipedia.org/wiki/Quickselect – Evg Jun 05 '19 at 07:32
  • Is there a specific reason it needs to be "the most efficient"? If not, just go with the simplest, most readable solution to start off with, and you can worry about optimizing this if the need arises. – Bernhard Barker Jun 05 '19 at 15:57

2 Answers2

11

Use std::nth_element to partition which yields the median, then std::min_element in the left half and std::max_element in the right one.

If you need it to be faster than that then roll your own version based on std::nth_element.

Bathsheba
  • 220,365
  • 33
  • 331
  • 451
3

Another option is to specify a custom comparison for std::nth_element, which captures the min and max. It will likely end up doing a lot more comparisons and branches, so this might be slower on some specific hardware, possibly depending on how much of your data is cached etc., so - as always - benchmark if you've reason to care, but for a non-empty vector a the technique looks like this:

int min = a[0], max = a[0];
std::nth_element(a.begin(), a.begin() + n, a.end(),
    [&](int lhs, int rhs) {
        min = std::min(min, std::min(lhs, rhs));
        max = std::max(max, std::max(lhs, rhs));
        return lhs < rhs;
    });

For what little it's worth, on my (~10yo i5-660) HTPC using GCC 7.4 with 1 million random ints between 0 and 1000, nth_element takes about 36% longer with the min/max comparison than without.

Tony Delroy
  • 94,554
  • 11
  • 158
  • 229
  • 1
    You are assuming that `std::nth_element` will check all elements at least once. That's reasonable, but technically not guaranteed. – BiagioF Jun 05 '19 at 09:00
  • @BiagioFesta: interesting point; not guaranteed by the language spec, but I think it'd be logically impossible for it to work otherwise...? – Tony Delroy Jun 05 '19 at 09:01
  • Sure, there are some proofs in Theoretical Computer Science regarding the complexity of algorithms. However, the C++ standard does not even specify which algorithm is used to implement `std::nth_element`. If I had to write production code, I would implement the full algorithm by myself. – BiagioF Jun 05 '19 at 09:06
  • 1
    @BiagioFesta: correctness of this code is different from its time complexity. I'd say it's self evident that it would be literally impossible for `nth_element` to rearrange the input data as it's required to do without invoking the provided comparison functor *at least* once for every element, but you're right that when it invokes it more than once it may adversely performance / can't predict how badly when implementation is opaque; but at worst it'll double the comparisons so no big-*O* change. Can measure though, and that will give you a sense for whether it's worth worrying about. – Tony Delroy Jun 05 '19 at 09:13
  • As you are calculating (lhs – CSM Jun 05 '19 at 18:10
  • @CSM: yeah, likely - lots of things you could try, e.g. `std::minmax`; there's also the branchless bit hacks for min (`y ^ ((x ^ y) & -(x < y));`) and max (`x ^ ((x ^ y) & -(x < y))`) - what's best will likely vary with CPU. My main point is that `nth_element`'s comparison argument's usable for this. – Tony Delroy Jun 06 '19 at 04:01
  • 1 less comparison: `auto[low, high] = std::minmax(lhs, rhs); if (low < min) min = low; if (high > max) max = high; return rhs != low;` – Robert Andrzejuk Jun 07 '19 at 11:20
  • @RobertAndrzejuk: yup - I mentioned `minmax` in my comment above yours - wasn't noticeably faster on my CPU though, did you benchmark it to see if it was faster on your CPU? by how much? What's your CPU? And `if (low < min) min = low;` didn't buy me anything over `std::min` - from memory, godbolt showed GCC 8.2 using cmovle either way. – Tony Delroy Jun 08 '19 at 01:07
  • @TonyDelroy Your assertion about correctness is valid only for the parts of the compare function that actually affect the final arrangement of the elements. Although not likely with any of the current implementations, in theory the compiler might recognize that your heavyweight functor is no different from the default "less than" comparison for the purposes of the algorithm, and optimize it away completely. Another "smart" compiler might decide that it needs to pre-calculate and somehow cache the results of the comparison for all possible values (it's not that much for an 8bit integer). – namark Jan 16 '20 at 10:50
  • Just a note about 36% overhead mentioned in the answer. Starting from C++11, for simple types it would be good to use the initializer list form of `std::min` and `std::max`, something like the following `min = std::min( { min, std::min( { lhs, rhs } ) } );`. The point is `std::min(a,b)` returns a reference, and it's difficult for compilers to optimize it efficiently. OTOH `std::min({a,b})` returns a value (not a reference) which can be easily optimized. BTW the same is true for `std::minmax`. – Rom098 Apr 23 '20 at 14:24