Générer tous les indices d'une séquence est généralement une mauvaise idée, car cela peut prendre beaucoup de temps, surtout si le rapport des nombres à choisir MAX
est faible (la complexité devient dominée par O(MAX)
). Cela s'aggrave si le rapport des nombres à choisir à se MAX
rapproche de un, car alors supprimer les indices choisis de la séquence de tous devient également coûteux (nous approchons O(MAX^2/2)
). Mais pour les petits nombres, cela fonctionne généralement bien et n'est pas particulièrement sujet aux erreurs.
Filtrer les indices générés à l'aide d'une collection est également une mauvaise idée, car un certain temps est passé à insérer les indices dans la séquence, et la progression n'est pas garantie car le même nombre aléatoire peut être tiré plusieurs fois (mais pour suffisamment grand, MAX
il est peu probable ). Cela pourrait être proche de la complexité
O(k n log^2(n)/2)
, en ignorant les doublons et en supposant que la collection utilise un arbre pour une recherche efficace (mais avec un coût constant important k
d'allocation des nœuds de l'arbre et éventuellement un rééquilibrage ).
Une autre option consiste à générer les valeurs aléatoires uniquement depuis le début, garantissant que des progrès sont réalisés. Cela signifie qu'au premier tour, un index aléatoire [0, MAX]
est généré:
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
Au deuxième tour, seul [0, MAX - 1]
est généré (car un élément a déjà été sélectionné):
items i0 i1 i3 i4 i5 i6 (total 6 items)
idx 1 ^^ (index 2 out of these 6, but 3 out of the original 7)
Les valeurs des indices doivent alors être ajustées: si le deuxième indice tombe dans la seconde moitié de la séquence (après le premier index), il doit être incrémenté pour tenir compte de l'écart. Nous pouvons l'implémenter en boucle, nous permettant de sélectionner un nombre arbitraire d'éléments uniques.
Pour les séquences courtes, il s'agit d'un O(n^2/2)
algorithme assez rapide :
void RandomUniqueSequence(std::vector<int> &rand_num,
const size_t n_select_num, const size_t n_item_num)
{
assert(n_select_num <= n_item_num);
rand_num.clear();
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
size_t n_where = i;
for(size_t j = 0; j < i; ++ j) {
if(n + j < rand_num[j]) {
n_where = j;
break;
}
}
rand_num.insert(rand_num.begin() + n_where, 1, n + n_where);
}
}
Où n_select_num
est votre 5 et n_number_num
est votre MAX
. Le n_Rand(x)
retourne des entiers aléatoires dans [0, x]
(inclus). Cela peut être un peu plus rapide si vous sélectionnez un grand nombre d'éléments (par exemple pas 5 mais 500) en utilisant la recherche binaire pour trouver le point d'insertion. Pour ce faire, nous devons nous assurer de répondre aux exigences.
Nous ferons une recherche binaire avec la comparaison n + j < rand_num[j]
qui est la même que
n < rand_num[j] - j
. Nous devons montrer qu'il rand_num[j] - j
s'agit toujours d'une séquence triée pour une séquence triée rand_num[j]
. Ceci est heureusement facile à montrer, car la distance la plus basse entre deux éléments de l'original rand_num
est un (les nombres générés sont uniques, il y a donc toujours une différence d'au moins 1). En même temps, si nous soustrayons les indices j
de tous les éléments
rand_num[j]
, les différences d'index sont exactement de 1. Donc, dans le «pire» cas, nous obtenons une séquence constante - mais jamais décroissante. La recherche binaire peut donc être utilisée, donnant l' O(n log(n))
algorithme:
struct TNeedle {
int n;
TNeedle(int _n)
:n(_n)
{}
};
class CCompareWithOffset {
protected:
std::vector<int>::iterator m_p_begin_it;
public:
CCompareWithOffset(std::vector<int>::iterator p_begin_it)
:m_p_begin_it(p_begin_it)
{}
bool operator ()(const int &r_value, TNeedle n) const
{
size_t n_index = &r_value - &*m_p_begin_it;
return r_value < n.n + n_index;
}
bool operator ()(TNeedle n, const int &r_value) const
{
size_t n_index = &r_value - &*m_p_begin_it;
return n.n + n_index < r_value;
}
};
Et enfin:
void RandomUniqueSequence(std::vector<int> &rand_num,
const size_t n_select_num, const size_t n_item_num)
{
assert(n_select_num <= n_item_num);
rand_num.clear();
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
std::vector<int>::iterator p_where_it = std::upper_bound(rand_num.begin(), rand_num.end(),
TNeedle(n), CCompareWithOffset(rand_num.begin()));
rand_num.insert(p_where_it, 1, n + p_where_it - rand_num.begin());
}
}
J'ai testé cela sur trois benchmarks. Tout d'abord, 3 numéros ont été choisis sur 7 éléments, et un histogramme des éléments choisis a été accumulé sur 10000 passages:
4265 4229 4351 4267 4267 4364 4257
Cela montre que chacun des 7 items a été choisi approximativement le même nombre de fois, et il n'y a pas de biais apparent causé par l'algorithme. L'exactitude de toutes les séquences a également été vérifiée (unicité du contenu).
Le deuxième critère consistait à choisir 7 numéros sur 5000 articles. Le temps de plusieurs versions de l'algorithme s'est accumulé sur 10 000 000 d'exécutions. Les résultats sont indiqués dans les commentaires dans le code comme b1
. La version simple de l'algorithme est légèrement plus rapide.
Le troisième critère consistait à choisir 700 numéros sur 5000 éléments. Le temps de plusieurs versions de l'algorithme s'est à nouveau accumulé, cette fois sur 10 000 exécutions. Les résultats sont indiqués dans les commentaires dans le code comme b2
. La version de recherche binaire de l'algorithme est maintenant plus de deux fois plus rapide que la version simple.
La deuxième méthode commence à être plus rapide pour choisir plus de 75 éléments environ sur ma machine (notez que la complexité de l'un ou l'autre algorithme ne dépend pas du nombre d'éléments, MAX
).
Il est à noter que les algorithmes ci-dessus génèrent les nombres aléatoires dans l'ordre croissant. Mais il serait simple d'ajouter un autre tableau dans lequel les numéros seraient enregistrés dans l'ordre dans lequel ils ont été générés, et de le renvoyer à la place (à un coût supplémentaire négligeable O(n)
). Il n'est pas nécessaire de mélanger la sortie: ce serait beaucoup plus lent.
Notez que les sources sont en C ++, je n'ai pas Java sur ma machine, mais le concept doit être clair.
MODIFIER :
Pour m'amuser, j'ai également implémenté l'approche qui génère une liste avec tous les indices
0 .. MAX
, les choisit au hasard et les supprime de la liste pour garantir l'unicité. Depuis que j'ai choisi assez haut MAX
(5000), les performances sont catastrophiques:
std::vector<int> all_numbers(n_item_num);
std::iota(all_numbers.begin(), all_numbers.end(), 0);
for(size_t i = 0; i < n_number_num; ++ i) {
assert(all_numbers.size() == n_item_num - i);
int n = n_Rand(n_item_num - i - 1);
rand_num.push_back(all_numbers[n]);
all_numbers.erase(all_numbers.begin() + n);
}
J'ai également implémenté l'approche avec une set
(une collection C ++), qui arrive en deuxième position sur le benchmark b2
, étant seulement environ 50% plus lente que l'approche avec la recherche binaire. Cela est compréhensible, car le set
utilise un arbre binaire, où le coût d'insertion est similaire à la recherche binaire. La seule différence est la possibilité d'obtenir des éléments en double, ce qui ralentit la progression.
std::set<int> numbers;
while(numbers.size() < n_number_num)
numbers.insert(n_Rand(n_item_num - 1));
rand_num.resize(numbers.size());
std::copy(numbers.begin(), numbers.end(), rand_num.begin());
Le code source complet est ici .