17

I define an unordered_map like this:

std::unordered_map<std::string, Edge> edges;

Is there a efficient way to choose a random Edge from the unordered_map edges ?

syntagma
  • 23,346
  • 16
  • 78
  • 134
Joe Cool
  • 209
  • 1
  • 3
  • 8

6 Answers6

22

This is not an O(1) solution (unless you got only one edge):

Pre-C++11 solution:

std::tr1::unordered_map<std::string, Edge> edges;
std::tr1::unordered_map<std::string, Edge>::iterator random_it = edges.begin();
std::advance(random_it, rand_between(0, edges.size()));

C++11 onward solution:

std::unordered_map<std::string, Edge> edges;
auto random_it = std::next(std::begin(edges), rand_between(0, edges.size()));

The function that selects a valid random number is up to your choice, but be sure it returns a number in range [0 ; edges.size() - 1] when edges is not empty.

The std::next function simply wraps the std::advance function in a way that permits direct assignation.

Chnossos
  • 9,971
  • 4
  • 28
  • 40
  • 1
    Keep in mind `rand_between(0, edges.size())` may return `edges.size()` and `random_it` will point on `edges.end()`. I would suggest `rand_between(0, edges.size() - 1)` but we must be certain `edges` is not empty. – Richard Dally Aug 09 '15 at 09:55
  • Yes it must be in range `[0, edge.size()[`. – Chnossos Aug 09 '15 at 16:09
  • What would be the time complexity of the second solution? Does it have to independently run through n random keys to fetch the nth random key? – onesiumus Dec 14 '17 at 12:04
  • Why when we advance a position, we are sure we do not hit an empty slot? – JobHunter69 Jan 22 '21 at 05:36
  • @JobHunter69 No check is made by the advance function. You should ensure the number of increment is not going to do past the end yourself. – Chnossos Jan 22 '21 at 18:00
  • No I mean a hash table has a lot of empty slots scattered randomly. Does this advance function skip those empty slots? – JobHunter69 Jan 22 '21 at 21:48
  • @JobHunter69 advance only increments or decrements an iterator a set amount of time, so i'd say this is implementation defined. Check your library's implementation of unordered_map::iterator ++ and -- operators. – Chnossos Jan 25 '21 at 15:36
11

Is there a efficient way to choose a random Edge from the unordered_map edges ?

If by efficient you mean O(1), then no, it is not possible.

Since the iterators returned by unordered_map::begin / end are ForwardIterators, the approaches that simply use std::advance are O(n) in the number of elements.

If your specific use allows it, you can trade some randomness for efficiency:

You can select a random bucket (that can be accessed in O(1)), and then a random element inside that bucket.

int bucket, bucket_size;
do
{ 
    bucket = rnd(edges.bucket_count());
}
while ( (bucket_size = edges.bucket_size(bucket)) == 0 );

auto element = std::next(edges.begin(bucket), rnd(bucket_size));

Where rnd(n) returns a random number in the [0,n) range.

In practice if you have a decent hash most of the buckets will contain exactly one element, otherwise this function will slightly privilege the elements that are alone in their buckets.

sbabbi
  • 11,070
  • 2
  • 29
  • 57
  • This is an elegant method! However, there's a little problem: `std::advance` doesn't return an iterator, instead, you should use `std::next`. – for_stack Jul 12 '16 at 05:35
  • With that while loop there's actually no guarantee, that your method will return anything. Probably you won't run into that, but if the map is not densely populated, it may turn into a problem. – Victor Komarov Oct 27 '16 at 14:30
  • And seems like there is a O(1) solution, please correct me if my idea is wrong. – Victor Komarov Oct 27 '16 at 14:31
8

Strict O(1) solution without buckets:

  1. Keep a vector of keys, when you need to get a random element from your map, select a random key from the vector and return corresponding value from the map - takes constant time
  2. If you insert a key-value pair into your map, check if such key is already present, and if it's not the case, add that key to your key vector - takes constant time
  3. If you want to remove an element from the map after it was selected, swap the key you selected with the back() element of your key vector and call pop_back(), after that erase the element from the map and return the value - takes constant time

However, there is a limitation: if you want to delete elements from the map aside from random picking, you need to fix your key vector, this takes O(n) with naive approach. But still there is a way to get O(1) performance: keep a map that tells you where the key is in the key vector and update it with swap :)

Victor Komarov
  • 319
  • 4
  • 6
5

This is how you can get random element from a map:

std::unordered_map<std::string, Edge> edges;
iterator item = edges.begin();
int random_index = rand() % edges.size();
std::advance(item, random_index);

Or take a look at this answer, which provides the following solution:

std::unordered_map<std::string, Edge> edges;
iterator item = edges.begin();
std::advance( item, random_0_to_n(edges.size()) );
Community
  • 1
  • 1
syntagma
  • 23,346
  • 16
  • 78
  • 134
1

The solution of

std::unordered_map<std::string, Edge> edges;
auto random_it = std::next(std::begin(edges), rand_between(0, edges.size()));

is extremely slow....

A much faster solution will be:

  • when assigning edges, simutaneously emplaces its keys to std::vector<std::string> vec
  • random an int index ranging from 0 to vec.size() - 1
  • then get edges[vec[index]]
  • Please check other answers too, as this [solution](https://stackoverflow.com/a/40287204/7598851) has been proposed already by Victor Komarov some 3 years ago – MartinBG Oct 12 '19 at 14:31
  • 1
    @MartinBG Thanks for your reminds~Yes, I forgot to check other answers..That's my fault..I just looked at the highest votes' answer and tried it in my project, but got an poor result..I just posted my final solution. – user3370560 Oct 12 '19 at 14:42
  • You're welcome. BTW it would be great if you can share performance results you've got using both solutions. – MartinBG Oct 12 '19 at 18:25
0

you can see this problem:

problem 380. Insert Delete GetRandom O(1) you can build a vector to use vector random iterators, get random values more efficiently. Like this:

    class RandomizedSet {
public:
    unordered_map<int, int> m;
    vector<int> data;
    RandomizedSet() {

    }
    
    bool insert(int val) {
        if(m.count(val)){
            return false;
        } else{
            int index = data.size();
            data.push_back(val);
            m[val] = index;
            return true;
        }
    }
    
    bool remove(int val) {
        if(m.count(val)){
            int curr_index = m[val];
            int max_index = data.size()-1;
            m[data[max_index]] = curr_index;

            swap(data[curr_index], data[max_index]);
            data.pop_back();
            m.erase(val);
            return true;
        } else{
            return false;
        }
    }
    
    int getRandom() {
        return data[rand() % data.size()];
    }
};

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet* obj = new RandomizedSet();
 * bool param_1 = obj->insert(val);
 * bool param_2 = obj->remove(val);
 * int param_3 = obj->getRandom();
 */
ximi
  • 1