Home > database >  sorting map using callback function in sort()
sorting map using callback function in sort()

Time:04-16

I have a unordered_map<int,int> type variable. I want to sort the map according to its value. I was wondering if I can use callback in sort function, so that it returns sorted map according to it's value.

unordered_map<int,int> p;
for(int i=0;i<nums.size();i  ){
            p[nums[i]]  ;
        }
sort(p.begin(),p.end(),callback);

CodePudding user response:

You can't sort a unordered_map in-place. You need to build a sortable alternate container, dump the content there (or bring appropriate material necessary to reference the pairs in the original map), then sort away. There are a multitude of ways to accomplish this. A few examples are shown below.

Value Copy and Sort

The following generates one-hundred random draws in the inclusive domain 1..10, then prints the frequency table after sorting based on frequency (value) descending (the unspoken task I suspect to be driving all of this to begin with):

#include <iostream>
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <random>

int main()
{
    std::mt19937 rng{ std::random_device{}() };
    std::uniform_int_distribution<> dist(1, 10);

    std::unordered_map<int, int> p;
    for (int i = 0; i < 100;   p[dist(rng)],   i);

    std::vector<std::pair<int,int>> v { p.begin(), p.end() };
    std::sort(v.begin(), v.end(), 
        [](auto const& pr1, auto const& pr2) 
        { return pr2.second < pr1.second; });

    for (auto const& pr : v)
        std::cout << pr.first << ':' << pr.second << '\n';
}

Output (varies)

3:14
4:13
7:13
9:11
1:11
2:10
10:7
6:7
8:7
5:7

Pointers to Pairs

An alternate mechanism using constant pointers to the original map pairs is also possible (and probably preferable if the mapped content is either too expensive or outright impossible to make copies of):

#include <iostream>
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <random>

int main()
{
    std::mt19937 rng{ std::random_device{}() };
    std::uniform_int_distribution<> dist(1, 10);

    std::unordered_map<int, int> p;
    for (int i = 0; i < 100;   p[dist(rng)],   i);

    std::vector<const decltype(p)::value_type*> v;
    for (auto const& pr : p)
        v.emplace_back(&pr);

    std::sort(v.begin(), v.end(), 
        [](auto const& pr1, auto const& pr2) 
        { return pr2->second < pr1->second; });

    for (auto const& pr : v)
        std::cout << pr->first << ':' << pr->second << '\n';
}

Output (varies)

2:16
1:14
3:11
7:11
4:9
10:9
5:8
8:8
9:8
6:6

Caveats apply to this approach. Those pointers to pairs are only as good as the day they were cashed. E.g., modify the original map by shoving new keys in, or trimming existing keys out, and the entire pointer bed is disavowed. That obviously includes outright destroying the original map. Buyer beware.

CodePudding user response:

You addressing 2 problems here:

  1. Sorting a std::map or std::unordered_map according to its value
  2. Using a kind of "compare callback" function for std::sort

Let us first tackle the point 2. If we read the description of std::sort in the CPP Reference here, then we see that can use a "comparison function object" as the 3rd parameter. This can be implemented with Lampda, a functor or with adding a comparison operator to the object that will be sorted.

Let us assume we have a struct in a std::vector that we want to sort.

struct IntVal {
    int val{};
};
std::vector<IntVal> intVals{};

If we want to sort this with a Lambda, then:

std::sort(intVals.begin(), intVals.end(), [](const IntVal& iv1, const IntVal& iv2) { return iv1.val < iv2.val; });

Or, you could add a comparison operator to your class/struct:

struct IntVal{
    int val{};
    bool operator < (const IntVal& other) const { return val < other.val; }
};

Or you can make your class a Functor, by adding a callable operator () to it.

#include <iostream>
#include <vector>
#include <algorithm>

struct IntVal{
    int val{};
    bool operator () (const IntVal& iv1, const IntVal& iv2) const { return iv1.val < iv2.val; }
};
std::vector<IntVal> intVals{ {3},{2},{1} };

int main() {
    std::sort(intVals.begin(), intVals.end(), IntVal());

    for (const IntVal& iv : intVals)
        std::cout << iv.val << '\n';
}

Or, if you cannot modify the class, the create an external functor:

#include <iostream>
#include <vector>
#include <algorithm>

struct IntVal{
    int val{};
};

// Functor
struct Comp {
    bool operator () (const IntVal& iv1, const IntVal& iv2) const { return iv1.val < iv2.val; }
};

std::vector<IntVal> intVals{ {3},{2},{1} };

int main() {
    std::sort(intVals.begin(), intVals.end(), Comp());

    for (const IntVal& iv : intVals)
        std::cout << iv.val << '\n';
}

Coming to the next topic.

You want to sort the data, stored in the std::unordered_map by its "value"-type.

This is not possible in place, because the nature of the maps is that they are already sorted. You cannot resort them. This would violate their internal behaviour.

The second problem is often that there key is unique. And sorting could also probaly destroy this strict requirement.

The solution is to use a second container. With you above mentioned "Sort" parameter. Copy the data to a second container and sort this.

We can use a std::vector and its range constructor to copy the data.

#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_map>
#include <utility>

// Functor
struct Comp {
    bool operator () (const std::pair<int,int>& p1, const std::pair<int, int>& p2) { return (p1.second == p2.second) ? p1.first<p2.first : p1.second>p2.second; }
};

int main() {

    std::unordered_map<int, int> test{ {1,4},{2,3},{3,2},{4,1} };

    std::vector<std::pair<int, int>> data(test.begin(), test.end());
    
    std::sort(data.begin(), data.end(), Comp());

    for (const auto[i1,i2] : data)
        std::cout << i1 << ' ' << i2 << '\n';
}

Or, last but not least, and the final solution. We can use a 2nd container that will be sorted by definition, like a std::multiset

This will create a piece of code that is really easy to understand.

Please see:

#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_map>
#include <utility>
#include <set>

// ------------------------------------------------------------
// Create aliases. Save typing work and make code more readable
using Pair = std::pair<int, int>;

// The map
using Map = std::unordered_map<Pair::first_type, Pair::second_type>;

// Sorted values will be stored in a multiset
struct Comp { bool operator ()(const Pair& p1, const Pair& p2) const { return (p1.second == p2.second) ? p1.first<p2.first : p1.second>p2.second; } };
using Sorter = std::multiset<Pair, Comp>;

int main() {

    Map test{ {1,4},{2,3},{3,2},{4,1} };

    Sorter sorter(test.begin(), test.end());

    for (const auto[i1,i2] : sorter)
        std::cout << i1 << ' ' << i2 << '\n';
}




And since what you really want to do is counting, please see an example for this as well. Here we will count as an example, the letters in a string:

#include <iostream>
#include <string>
#include <utility>
#include <set>
#include <unordered_map>
#include <type_traits>
#include <cctype>

// ------------------------------------------------------------
// Create aliases. Save typing work and make code more readable
using Pair = std::pair<char, unsigned int>;

// Standard approach for counter
using Counter = std::unordered_map<Pair::first_type, Pair::second_type>;

// Sorted values will be stored in a multiset
struct Comp { bool operator ()(const Pair& p1, const Pair& p2) const { return (p1.second == p2.second) ? p1.first<p2.first : p1.second>p2.second; } };
using Rank = std::multiset<Pair, Comp>;
// ------------------------------------------------------------


// --------------------------------------------------------------------------------------
// Compact function to calculate the frequency of charcters and then get their rank
Rank getRank(std::string& text) {

    // Definition of our counter
    Counter counter{};

    // Iterate over all charcters in text and count their frequency
    for (const char c : text) if (std::isalpha(c)) counter[char(std::tolower(c))]  ;
    
    // Return ranks,sorted by frequency and then sorted by character
    return { counter.begin(), counter.end() };
}
// --------------------------------------------------------------------------------------
// Test, driver code
int main() {
    // Get a string from the user
    if (std::string text{}; std::getline(std::cin, text))

        // Calculate rank and show result
        for (const auto& [letter, count] : getRank(text))
            std::cout << letter << " = " << count << '\n';
}
  • Related