Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sort by non-lazy lambda expression / projection

I have an array of element of some type T. For some complex function enter image description here I would like to sort the array by the value of that function. Efficiently.

When I did some research on how to do such a thing, I quickly found that range::v3::sort, from the range-v3 library, could be used with the help of projections. In that context, the value T can be projected onto a new value to be used by a comparator. The problem is, that it is done lazily.

Consider the following example:

#include <range/v3/algorithm/sort.hpp>
#include <vector>
#include <iostream>

int main() {
    int invocations=0;
    std::vector<int> data{1,5,2,7,6,3,4,8,9,0};
    auto f = [&](int val){
        ++invocations;
        return val%2 ? val+100 : val;
    };
    ranges::v3::sort(data, std::less<int>{}, f);
    for (int v : data) {
        std::cout << v << ' ';
    }
    std::cout << "Invocations " << invocations << std::endl;
}

Here T and f are kept simple for the sake of brevity. This gives me the output:

0 2 4 6 8 1 3 5 7 9 Invocations 60

But envision that f is some complex function that I do not want to be executed repeatedly, each time it is used in the comparator (otherwise I could just write a custom comparator and use regular std::sort). I would expect f to be called exactly once for each of the values. However, once the array is sorted, the results of f can be discarded.

Also, the real values of T themselves are relatively complex. I can quickly swap two elements around, but I shouldn't copy them into a new, temporary container (e.g. std::vector<std::pair<T,int>>) for sorting.

Is there some brief approach to that, besides manually sorting my input array?

like image 649
CygnusX1 Avatar asked Dec 07 '25 06:12

CygnusX1


2 Answers

You might store evaluation, and use it as projection (I don't project actually as order of tuple is fine, and original data is also comparable):

std::vector<int> data{1,5,2,7,6,3,4,8,9,0};
auto values = data | ranges::view::transform(f) | ranges::to_vector;
// to_vector needed to do evaluation **now**.
ranges::v3::sort(ranges::view::zip(values, data)); // Values first to avoid real projection
                                                   // else use projection on `get<I>`.

Demo

like image 81
Jarod42 Avatar answered Dec 09 '25 19:12

Jarod42


You evidently need to store the function invocations. If you don't want to do this in a map (so you can index by the data value) but in a vector (much less overhead than a map) then you can't sort the original array directly (because you have no link from each data value to its function value, you need the index). So, we sort indices into the data array instead:

int invocations=0;
std::vector<int> data{1,5,2,7,6,3,4,8,9,0};
auto f = [&](int val){
    ++invocations;
    return val%2 ? val+100 : val;
};

std::vector<int> fValues(data.size());
std::vector<int> indices(data.size());

std::transform(data.begin(), data.end(), fValues.begin(), f);

std::iota(indices.begin(), indices.end(), 0);
std::sort(indices.begin(), indices.end(), [&](auto i, auto j) {
    return fValues[i] < fValues[j];
});

for (int sortedIndex : indices) {
    std::cout << data[sortedIndex] << ' ';
}
std::cout << "Invocations " << invocations << std::endl;

You'd still have to apply the permutation to get the exact same effect as sorting directly with comparing f values, but maybe that's unnecessary for you.

Demo

like image 36
Max Langhof Avatar answered Dec 09 '25 18:12

Max Langhof



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!