web-dev-qa-db-fra.com

Comment obtenir un élément aléatoire à partir d'un conteneur C ++?

Quelle est la bonne façon d'obtenir un élément [pseudo-] aléatoire à partir d'une plage STL?

Le mieux que je puisse trouver est de faire std::random_shuffle(c.begin(), c.end()) puis de prendre mon élément aléatoire dans c.begin().

Cependant, je peux vouloir un élément aléatoire d'un conteneur const, ou je ne veux pas le coût d'un shuffle complet.

Y a-t-il une meilleure façon?

44
paperjam

Toutes les réponses utilisant % Ici sont incorrectes, car Rand() % n produira des résultats biaisés: imaginez Rand_MAX == 5 Et le nombre d'éléments est 4. Ensuite, vous obtiendrez deux fois plus le numéro 0 et 1 que les numéros 2 ou 3.

Une bonne façon de procéder est:

template <typename I>
I random_element(I begin, I end)
{
    const unsigned long n = std::distance(begin, end);
    const unsigned long divisor = (Rand_MAX + 1) / n;

    unsigned long k;
    do { k = std::Rand() / divisor; } while (k >= n);

    std::advance(begin, k);
    return begin;
}

Un autre problème est que std::Rand Est supposé avoir seulement 15 bits aléatoires, mais nous l'oublierons ici.

31
Alexandre C.

J'ai publié cette solution sur un article Google+ où quelqu'un d'autre l'a référencé. Le poster ici, car celui-ci est légèrement meilleur que les autres car il évite les biais en utilisant std :: uniform_int_distribution:

#include  <random>
#include  <iterator>

template<typename Iter, typename RandomGenerator>
Iter select_randomly(Iter start, Iter end, RandomGenerator& g) {
    std::uniform_int_distribution<> dis(0, std::distance(start, end) - 1);
    std::advance(start, dis(g));
    return start;
}

template<typename Iter>
Iter select_randomly(Iter start, Iter end) {
    static std::random_device rd;
    static std::mt19937 gen(rd());
    return select_randomly(start, end, gen);
}

Exemple d'utilisation:

#include <vector>
using namespace std;

vector<int> foo;
/* .... */
int r = *select_randomly(foo.begin(), foo.end());

J'ai fini par créer un Gist avec un meilleur design suivant une approche similaire .

45

C++ 17 std::sample

Il s'agit d'une méthode pratique pour obtenir plusieurs éléments aléatoires sans répétition.

main.cpp

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

int main() {
    const std::vector<int> in{1, 2, 3, 5, 7};
    std::vector<int> out;
    size_t nelems = 3;
    std::sample(
        in.begin(),
        in.end(),
        std::back_inserter(out),
        nelems,
        std::mt19937{std::random_device{}()}
    );
    for (auto i : out)
        std::cout << i << std::endl;
}

Compiler et exécuter:

g++-7 -o main -std=c++17 -Wall -Wextra -pedantic main.cpp
./main

Sortie: 3 nombres aléatoires sont choisis parmi 1, 2, 3, 5, 7 Sans répétition.

Pour plus d'efficacité, seule O(n) est garantie puisque ForwardIterator est l'API utilisée, mais je pense que les implémentations stdlib se spécialiseront dans O(1) si possible (par exemple vector ).

Testé dans GCC 7.2, Ubuntu 17.10. Comment obtenir GCC 7 en 16.04 .

Cela fonctionne très bien tant que Rand_MAX est beaucoup plus grand que la taille du conteneur, sinon il souffre du problème de biais cité par Alexandre :

vector<int>::iterator randIt = myvector.begin();
std::advance(randIt, std::Rand() % myvector.size());
9
cprogrammer

Si vous ne pouvez pas accéder à la taille, je pense que vous voudriez faire ce qui suit. Il renvoie l'itérateur à l'élément aléatoire.

#include <algorithm>
#include <iterator>

template <class InputIterator> InputIterator 
random_n(InputIterator first, InputIterator last) {
   typename std::iterator_traits<InputIterator>::difference_type distance = 
        std::distance(first, last);
   InputIterator result = first;
   if (distance > 1) {
      // Uses std::Rand() naively.  Should replace with more uniform solution. 
      std::advance( result, std::Rand() % distance );
   }
   return result;
}
// Added in case you want to specify the RNG.  RNG uses same 
// definition as std::random_shuffle
template <class InputIterator, class RandomGenerator> InputIterator 
random_n(InputIterator first, InputIterator last, RandomGenerator& Rand) {
   typename std::iterator_traits<InputIterator>::difference_type distance = 
       std::distance(first, last);
   InputIterator result = first;
   if (distance > 1) {
      std::advance( result, Rand(distance) );
   }
   return result;
}
3
Dave S

Prenez le nombre d'éléments, c.size(), puis obtenez un random_number Entre 0 et c.size(), et utilisez:

auto it = c.begin();
std::advance(it, random_number)

Jetez un œil à http://www.cplusplus.com/reference/clibrary/cstdlib/Rand/

2
ypnos

Vous pouvez essayer d'obtenir un nombre aléatoire entre 0 et le nombre d'éléments du conteneur. Vous pouvez alors accéder à l'élément correspondant du conteneur. Par exemple, vous pouvez faire ceci:

#include <cstdlib>
#include <ctime>

// ...
std::srand(std::time(0)); // must be called once at the start of the program
int r = std::Rand() % c.size() + 1; 
container_type::iterator it = c.begin();
std::advance(it, r);
1
Cedekasme