116

How can I sort two vectors in the same way, with criteria that uses only one of the vectors?

For example, suppose I have two vectors of the same size:

vector<MyObject> vectorA;
vector<int> vectorB;

I then sort vectorA using some comparison function. That sorting reordered vectorA. How can I have the same reordering applied to vectorB?


One option is to create a struct:

struct ExampleStruct {
    MyObject mo;
    int i;
};

and then sort a vector that contains the contents of vectorA and vectorB zipped up into a single vector:

// vectorC[i] is vectorA[i] and vectorB[i] combined
vector<ExampleStruct> vectorC;

This doesn't seem like an ideal solution. Are there other options, especially in C++11?

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
user2381422
  • 5,645
  • 14
  • 42
  • 56
  • Can you perhaps provide an example with some input and the corresponding desired output? I am having troubles understanding the question – Andy Prowl Jun 12 '13 at 20:06
  • I think he wants to (effectively) sort the contents `vectorA` and `vectorB` both by the contents of `vectorB`. – Mooing Duck Jun 12 '13 at 20:13
  • I think [this question](http://stackoverflow.com/questions/16874183/reusing-stdalgorithms-with-non-standard-containers/16905832) is a near duplicate if not an exact duplicate – Mooing Duck Jun 12 '13 at 20:16
  • What about sorting a third vector (of indices 0, ... vectorA.size()), based on the values in vectorA and "apply" those indices on vectorB? E.g. like in http://stackoverflow.com/a/10581051/417197 – André Jun 12 '13 at 20:26
  • Personally, i'd rather have a `vector>`. Then you wouldn't have to worry about the two lists getting out of sync; one sort reorders both sets of data simultaneously. And there's no extra struct to have to write. – cHao Jun 12 '13 at 21:03

9 Answers9

143

Finding a sort permutation

Given a std::vector<T> and a comparison for T's, we want to be able to find the permutation you would use if you were to sort the vector using this comparison.

template <typename T, typename Compare>
std::vector<std::size_t> sort_permutation(
    const std::vector<T>& vec,
    Compare& compare)
{
    std::vector<std::size_t> p(vec.size());
    std::iota(p.begin(), p.end(), 0);
    std::sort(p.begin(), p.end(),
        [&](std::size_t i, std::size_t j){ return compare(vec[i], vec[j]); });
    return p;
}

Applying a sort permutation

Given a std::vector<T> and a permutation, we want to be able to build a new std::vector<T> that is reordered according to the permutation.

template <typename T>
std::vector<T> apply_permutation(
    const std::vector<T>& vec,
    const std::vector<std::size_t>& p)
{
    std::vector<T> sorted_vec(vec.size());
    std::transform(p.begin(), p.end(), sorted_vec.begin(),
        [&](std::size_t i){ return vec[i]; });
    return sorted_vec;
}

You could of course modify apply_permutation to mutate the vector you give it rather than returning a new sorted copy. This approach is still linear time complexity and uses one bit per item in your vector. Theoretically, it's still linear space complexity; but, in practice, when sizeof(T) is large the reduction in memory usage can be dramatic. (See details)

template <typename T>
void apply_permutation_in_place(
    std::vector<T>& vec,
    const std::vector<std::size_t>& p)
{
    std::vector<bool> done(vec.size());
    for (std::size_t i = 0; i < vec.size(); ++i)
    {
        if (done[i])
        {
            continue;
        }
        done[i] = true;
        std::size_t prev_j = i;
        std::size_t j = p[i];
        while (i != j)
        {
            std::swap(vec[prev_j], vec[j]);
            done[j] = true;
            prev_j = j;
            j = p[j];
        }
    }
}

Example

vector<MyObject> vectorA;
vector<int> vectorB;

auto p = sort_permutation(vectorA,
    [](T const& a, T const& b){ /*some comparison*/ });

vectorA = apply_permutation(vectorA, p);
vectorB = apply_permutation(vectorB, p);

Resources

Timothy Shields
  • 75,459
  • 18
  • 120
  • 173
  • 5
    In my compiler, I had to change "Compare& compare" to "Compare compare". – ABCD Jan 17 '16 at 11:13
  • 3
    I needed also to include the header in order to run std::iota function (vs2012). – Kite Jul 31 '16 at 23:33
  • 1
    "*You could of course modify `apply_permutation` to mutate the vector you give it rather than returning a new sorted copy.*" I would find fleshing this out, at least conceptually, a useful addition to the answer. Would you implement this in the same way as `apply_permutation` itself? – ildjarn Sep 01 '16 at 21:44
  • 2
    @ildjarn I've updated the answer with your suggestion. – Timothy Shields Sep 02 '16 at 18:01
  • How would you write this if you did not have C++11 available? – BigBrownBear00 Oct 13 '16 at 19:54
  • This code is great, thank you ! Just a remark, the function `apply_permutation_in_place` has a little bug in the implementation compared to the link given, that make it not apply a permutation properly. The error is in `std::swap(vec[i], vec[j]);` where `vec[i]` should be the previous value of `vec[j]` in the loop, and not constantly vec[i]. I spend time finding this error, so I hope this comment will help other people falling on the same problem ! `size_t oldJ = i;` should be added before the inner loop, vec[i] replaced by vec[oldJ], and oldJ = j; at the end of the inner loop – beesleep Nov 24 '16 at 23:27
  • @beesleep Thank you for checking the code. :) Are you certain there is a bug? What is an example input for which it produces the wrong result? – Timothy Shields Nov 24 '16 at 23:57
  • Take a permutation vector vector p = {1, 3, 2, 0, 4, 5};, and apply p to {0, 1, 2, 3, 4, 5}. You are expecting, by definition, to find back the same values as p (0 on the third position, 1 in the 0, ...). If you test with your code, it is not the case. It's different from the implementation shown [here](http://blog.merovius.de/2014/08/12/applying-permutation-in-constant.html). You always switch vec[j] with vec[i], i staying the same in the inner loop. In fact, the value in vec[i] when you enter the inner loop, need be swaped until it reach its final position. I hpoe I'm clear... – beesleep Nov 25 '16 at 11:50
  • 1
    @beesleep Thank you for catching that. I fixed it. Does it look correct now? – Timothy Shields Nov 25 '16 at 17:22
  • 1
    @TimothyShields , yep, now it works like a charm ! Thanks for correcting ;-) – beesleep Nov 25 '16 at 18:06
  • One tiny improvement, if I may suggest, is to use [&vec] in the lambdas, better clarity. – ikku100 Dec 13 '16 at 15:33
  • There's a variation of apply_permutation (reorder in place), that doesn't require a third array of booleans, instead it "uncycles" the "cycles" in a permutation of sorted indices.. [sort 2 arrays according to one of them](https://stackoverflow.com/questions/44027808/sorting-two-arrays-based-on-one-with-standard-library-copy-steps-avoided/44030600#44030600) . – rcgldr May 31 '17 at 21:08
  • Got this compile error: invalid initialization of non-const reference of type ‘main()::&’ from an rvalue of type ‘main()::’ [](int const& a, int const& b){ return a – Anoroah Jun 08 '17 at 14:52
  • 2
    Thanks for the excellent snippet! the sort_permutation declaration's `Compare& compare` should be const however. Otherwise you can't use std::less or similar. – kotakotakota Jan 23 '18 at 17:27
  • On VS2017, I had to change `Compare& compare` to `Compare compare` to get it to compile. – Contango Oct 25 '19 at 21:15
  • Templated lambdas are a C++20 feature, so the comparison lambda will need explicit types. – Chris Jul 10 '21 at 03:22
26

With range-v3, it is simple, sort a zip view:

std::vector<MyObject> vectorA = /*..*/;
std::vector<int> vectorB = /*..*/;

ranges::v3::sort(ranges::view::zip(vectorA, vectorB));

or explicitly use projection:

ranges::v3::sort(ranges::view::zip(vectorA, vectorB),
                 std::less<>{},
                 [](const auto& t) -> decltype(auto) { return std::get<0>(t); });

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Tried on VS2017, nothing compiled or worked, no matter what I tried. This library is known to work on VS2019, GCC and LLVM. – Contango Oct 25 '19 at 20:44
  • This is the solution! The whole point is to use O(1) space, while the other solutions allocate a spearate vector. – Aykhan Hagverdili Apr 10 '23 at 10:20
5

I would like to contribute with a extension I came up with. The goal is to be able to sort multiple vectors at the same time using a simple syntax.

sortVectorsAscending(criteriaVec, vec1, vec2, ...)

The algorithm is the same as the one Timothy proposed but using variadic templates, so we can sort multiple vectors of arbitrary types at the same time.

Here's the code snippet:

template <typename T, typename Compare>
void getSortPermutation(
    std::vector<unsigned>& out,
    const std::vector<T>& v,
    Compare compare = std::less<T>())
{
    out.resize(v.size());
    std::iota(out.begin(), out.end(), 0);
 
    std::sort(out.begin(), out.end(),
        [&](unsigned i, unsigned j){ return compare(v[i], v[j]); });
}
 
template <typename T>
void applyPermutation(
    const std::vector<unsigned>& order,
    std::vector<T>& t)
{
    assert(order.size() == t.size());
    std::vector<T> st(t.size());
    for(unsigned i=0; i<t.size(); i++)
    {
        st[i] = t[order[i]];
    }
    t = st;
}
 
template <typename T, typename... S>
void applyPermutation(
    const std::vector<unsigned>& order,
    std::vector<T>& t,
    std::vector<S>&... s)
{
    applyPermutation(order, t);
    applyPermutation(order, s...);
}
 
template<typename T, typename Compare, typename... SS>
void sortVectors(
    const std::vector<T>& t,
    Compare comp,
    std::vector<SS>&... ss)
{
    std::vector<unsigned> order;
    getSortPermutation(order, t, comp);
    applyPermutation(order, ss...);
}
 
// make less verbose for the usual ascending order
template<typename T, typename... SS>
void sortVectorsAscending(
    const std::vector<T>& t,
    std::vector<SS>&... ss)
{
    sortVectors(t, std::less<T>(), ss...);
}

Test it in Ideone.

I explain this a little bit better in this blog post.

tuket
  • 3,232
  • 1
  • 26
  • 41
3

In-place sorting using permutation

I would use a permutation like Timothy, although if your data is too large and you don't want to allocate more memory for the sorted vector you should do it in-place. Here is a example of a O(n) (linear complexity) in-place sorting using permutation:

The trick is to get the permutation and the reverse permutation to know where to put the data overwritten by the last sorting step.

template <class K, class T> 
void sortByKey(K * keys, T * data, size_t size){
    std::vector<size_t> p(size,0);
    std::vector<size_t> rp(size);
    std::vector<bool> sorted(size, false);
    size_t i = 0;

    // Sort
    std::iota(p.begin(), p.end(), 0);
    std::sort(p.begin(), p.end(),
                    [&](size_t i, size_t j){ return keys[i] < keys[j]; });

    // ----------- Apply permutation in-place ---------- //

    // Get reverse permutation item>position
    for (i = 0; i < size; ++i){
        rp[p[i]] = i;
    }

    i = 0;
    K savedKey;
    T savedData;
    while ( i < size){
        size_t pos = i;
        // Save This element;
        if ( ! sorted[pos] ){
            savedKey = keys[p[pos]];
            savedData = data[p[pos]];
        }
        while ( ! sorted[pos] ){
            // Hold item to be replaced
            K heldKey  = keys[pos];
            T heldData = data[pos];
            // Save where it should go
            size_t heldPos = rp[pos];

            // Replace 
            keys[pos] = savedKey;
            data[pos] = savedData;

            // Get last item to be the pivot
            savedKey = heldKey;
            savedData = heldData;

            // Mark this item as sorted
            sorted[pos] = true;

            // Go to the held item proper location
            pos = heldPos;
        }
        ++i;
    }
}
MtCS
  • 305
  • 1
  • 9
  • 1
    Though it looks like it is O(N^2), it is not. The inner while is only executed if the item is not sorted. As the inner while sorts the data, it kinda skips outer while iterations... – MtCS Aug 02 '14 at 16:28
3

I have recently wrote a proper zip iterator which works with the stl algorithms. It allows you to produce code like this:

std::vector<int> a{3,1,4,2};
std::vector<std::string> b{"Alice","Bob","Charles","David"};

auto zip = Zip(a,b);
std::sort(zip.begin(), zip.end());

for (const auto & z: zip) std::cout << z << std::endl;

It is contained in a single header and the only requirement is C++17. Check it out on GitHub.

There is also a post on codereview which contains all the source code.

DarioP
  • 5,377
  • 1
  • 33
  • 52
2
  1. Make a vector of pairs out of your individual vectors.
    initialize vector of pairs
    Adding to a vector of pair

  2. Make a custom sort comparator:
    Sorting a vector of custom objects
    http://rosettacode.org/wiki/Sort_using_a_custom_comparator#C.2B.2B

  3. Sort your vector of pairs.

  4. Separate your vector of pairs into individual vectors.

  5. Put all of these into a function.

Code:

std::vector<MyObject> vectorA;
std::vector<int> vectorB;

struct less_than_int
{
    inline bool operator() (const std::pair<MyObject,int>& a, const std::pair<MyObject,int>& b)
    {
        return (a.second < b.second);
    }
};

sortVecPair(vectorA, vectorB, less_than_int());

// make sure vectorA and vectorB are of the same size, before calling function
template <typename T, typename R, typename Compare>
sortVecPair(std::vector<T>& vecA, std::vector<R>& vecB, Compare cmp)
{

    std::vector<pair<T,R>> vecC;
    vecC.reserve(vecA.size());
    for(int i=0; i<vecA.size(); i++)
     {
        vecC.push_back(std::make_pair(vecA[i],vecB[i]);   
     }

    std::sort(vecC.begin(), vecC.end(), cmp);

    vecA.clear();
    vecB.clear();
    vecA.reserve(vecC.size());
    vecB.reserve(vecC.size());
    for(int i=0; i<vecC.size(); i++)
     {
        vecA.push_back(vecC[i].first);
        vecB.push_back(vecC[i].second);
     }
}
Community
  • 1
  • 1
ruben2020
  • 1,549
  • 14
  • 24
  • vector of pairs of references? – Mooing Duck Jun 12 '13 at 20:31
  • I get what you mean, but pairs of references is not going to easily work. – ruben2020 Jun 12 '13 at 20:47
  • 1
    @ruben2020: [It can](http://en.cppreference.com/w/cpp/utility/functional/reference_wrapper), but doing so just band-aids the issue. If you have two pieces of data intertwined enough that sorting one should sort the other, it would seem that what you really have is a not-yet-integrated object. – cHao Jun 13 '13 at 15:06
0

I'm assuming that vectorA and vectorB have equal lengths. You could create another vector, let's call it pos, where:

pos[i] = the position of vectorA[i] after sorting phase

and then, you can sort vectorB using pos, i.e create vectorBsorted where:

vectorBsorted[pos[i]] = vectorB[i]

and then vectorBsorted is sorted by the same permutation of indexes as vectorA is.

pkacprzak
  • 5,537
  • 1
  • 17
  • 37
0

I am not sure if this works but i would use something like this. For example to sort two vectors i would use descending bubble sort method and vector pairs.

For descending bubble sort, i would create a function that requires a vector pair.

void bubbleSort(vector< pair<MyObject,int> >& a)
{
    bool swapp = true;
    while (swapp) {
        int key;
        MyObject temp_obj;
        swapp = false;
        for (size_t i = 0; i < a.size() - 1; i++) {
            if (a[i].first < a[i + 1].first) {
                temp_obj = a[i].first;
                key = a[i].second;

                a[i].first = a[i + 1].first;
                a[i + 1].first = temp_obj;

                a[i].second = a[i + 1].second;
                a[i + 1].second = key;

                swapp = true;
            }
        }
    }
}

After that i would put your 2 vector values into one vector pair. If you are able to add values at the same time use this one and than call the bubble sort function.

vector< pair<MyObject,int> > my_vector;

my_vector.push_back( pair<MyObject,int> (object_value,int_value));

bubbleSort(my_vector);

If you want to use values after adding to your 2 vectors, you can use this one and than call the bubble sort function.

vector< pair<MyObject,int> > temp_vector;

for (size_t i = 0; i < vectorA.size(); i++) {
            temp_vector.push_back(pair<MyObject,int> (vectorA[i],vectorB[i]));
        }

bubbleSort(temp_vector);

I hope this helps. Regards, Caner

Caner SAYGIN
  • 527
  • 3
  • 11
0

Based on Timothy Shields answer.
With a small tweak to apply_permutaion you can apply the permutation to multiple vectors of different types at once with use of a fold expression.

template <typename T, typename... Ts>
void apply_permutation(const std::vector<size_t>& perm, std::vector<T>& v, std::vector<Ts>&... vs) {

    std::vector<bool> done(v.size());
    for(size_t i = 0; i < v.size(); ++i) {
        if(done[i]) continue;
        done[i] = true;
        size_t prev = i;
        size_t curr = perm[i];
        while(i != curr) {
            std::swap(v[prev], v[curr]);
            (std::swap(vs[prev], vs[curr]), ...);
            done[curr] = true;
            prev = curr;
            curr = perm[curr];
        }
    }
}
Lessi
  • 165
  • 7