Comment générer efficacement une liste de K entiers non répétitifs entre 0 et une limite supérieure N [dupliquer]
cette question a déjà une réponse ici:
la question donne toutes les données nécessaires: qu'est-ce qu'un algorithme efficace pour générer une séquence de K entiers non répétitifs dans un donné intervalle [0,N-1] . L'algorithme trivial (générer des nombres aléatoires et, avant de les ajouter à la séquence, les rechercher pour voir s'ils étaient déjà là) est très coûteux si K est assez grand et assez proche de N .
l'algorithme fourni dans sélectionner efficacement un ensemble d'éléments aléatoires à partir d'une liste liée semble plus compliqué que nécessaire, et nécessite certaines de mise en œuvre. Je viens de trouver un autre algorithme qui semble faire l'affaire, tant que vous connaissez tous les paramètres pertinents, en un seul passage.
13 réponses
le module aléatoire de la bibliothèque Python le rend extrêmement facile et efficace:
from random import sample
print sample(xrange(N), K)
sample
renvoie une liste de K éléments uniques choisis dans la séquence donnée.
xrange
est un "émulateur de liste", c'est-à-dire qu'il se comporte comme une liste de nombres consécutifs sans la créer en mémoire, ce qui le rend super-rapide pour des tâches comme celle-ci.
Dans The Art of Computer Programming, Volume 2: Seminumerical Algorithmes, Troisième Édition , Knuth décrit la sélection suivante algorithme d'échantillonnage:
algorithms (Selection sampling technique). Pour sélectionner n enregistrements au hasard à partir d'un ensemble de N, où 0 < N ≤ N.
S1. [Initialiser.] Set t ← 0, m ← 0. (Au cours de cet algorithme, m représente le nombre d'enregistrements sélectionnés jusqu'à présent, et t est le nombre total d'enregistrements d'entrée que nous avons abordés.)
S2. [Générer U.] générer un nombre aléatoire U, uniformément réparti entre zéro et un.
S3. [Test.] Si (N – t)U ≥ n – m, passer à l'étape S5.
S4. [Sélectionner.] Sélectionnez l'enregistrement suivant pour l'échantillon et augmentez m et t de 1. Si m < n, passez à l'étape S2; sinon, l'échantillon est complet et l'algorithme se termine.
S5. [Sauter.] Sauter le prochain enregistrement (ne pas l'inclure dans l'échantillon), augmenter t de 1, et revenir à l'étape S2.
une implémentation peut être plus facile à suivre que la description. Voici une implémentation Lisp commune qui sélectionne n membres au hasard dans une liste:
(defun sample-list (n list &optional (length (length list)) result)
(cond ((= length 0) result)
((< (* length (random 1.0)) n)
(sample-list (1- n) (cdr list) (1- length)
(cons (car list) result)))
(t (sample-list n (cdr list) (1- length) result))))
et voici une implémentation qui n'utilise pas la récursion, et qui fonctionne avec toutes sortes de séquences:
(defun sample (n sequence)
(let ((length (length sequence))
(result (subseq sequence 0 n)))
(loop
with m = 0
for i from 0 and u = (random 1.0)
do (when (< (* (- length i) u)
(- n m))
(setf (elt result m) (elt sequence i))
(incf m))
until (= m n))
result))
Il est effectivement possible de le faire dans l'espace proportionnel au nombre d'éléments sélectionnés, plutôt que la taille de l'ensemble que vous achetez à partir, quelle que soit la proportion de l'ensemble total que vous sélectionnez. Vous faites cela en générant une permutation aléatoire, puis en sélectionnant comme ceci:
choisir un chiffre de bloc, tel que thé ou XTEA. Utilisez XOR folding pour réduire la taille du bloc à la plus petite puissance de deux plus grands que le set que vous choisissez. Utilisez la graine aléatoire comme la clé du cipher. Pour générer un élément n dans la permutation, chiffrez n avec le chiffre. Si le numéro de sortie n'est pas dans votre jeu, cryptez cela. Répétez jusqu'à ce que le nombre soit à l'intérieur du jeu. En moyenne, vous aurez à faire à moins de deux chiffrements par nombre généré. Cela a l'avantage que si votre graine est cryptographiquement sûr, est à votre entière permutation.
j'ai écrit à ce sujet dans beaucoup plus de détails ici .
le code suivant (en C, origine inconnue) semble résoudre le problème extrêmement bien:
/* generate N sorted, non-duplicate integers in [0, max[ */
int *generate(int n, int max) {
int i, m, a;
int *g = (int *)calloc(n, sizeof(int));
if ( ! g) return 0;
m = 0;
for (i=0; i<max; i++) {
a = random_in_between(0, max - i);
if (a < n - m) {
g[m] = i;
m ++;
}
}
return g;
}
est-ce que quelqu'un sait où je peux trouver d'autres gemmes comme celle-ci?
génère un tableau 0...N-1
remplit a[i] = i
.
puis mélangez les premiers K
articles.
Traînante:
- Démarrer
J = N-1
- choisir un nombre aléatoire
0...J
(dire,R
) - swap
a[R]
aveca[J]
- depuis
R
peut être égal àJ
, l'élément peut être échangé avec lui-même
- depuis
- soustrayez
1
deJ
et répétez.
enfin, prenez K
derniers éléments.
il s'agit essentiellement de choisir un élément aléatoire dans la liste, de le déplacer, puis de choisir un élément aléatoire dans la liste restante, et ainsi de suite.
Œuvres O(K) et O(N) temps, exige O(N) entreposage.
la partie de mélange est appelée Fisher-Yates shuffle ou knut's shuffle , décrit dans le deuxième volume de L'Art de la programmation informatique.
accélère l'algorithme trivial en stockant les nombres K dans une mémoire de hachage. Connaître K avant de commencer enlève toute l'inefficacité de l'insertion dans une carte hash, et vous obtenez toujours l'avantage de la recherche rapide.
ma solution est orientée C++, mais je suis sûr qu'elle pourrait être traduite dans d'autres langues car c'est assez simple.
- tout d'abord, générer une liste liée avec des éléments K, allant de 0 à K
- Alors tant que la liste n'est pas vide, générer un nombre aléatoire entre 0 et la taille du vecteur
- prendre cet élément, le pousser dans un autre vecteur, et le supprimer de la liste originale
cette solution implique seulement deux itérations de boucle, et pas de recherche de table de hachage ou quoi que ce soit de la sorte. Ainsi en code réel:
// Assume K is the highest number in the list
std::vector<int> sorted_list;
std::vector<int> random_list;
for(int i = 0; i < K; ++i) {
sorted_list.push_back(i);
}
// Loop to K - 1 elements, as this will cause problems when trying to erase
// the first element
while(!sorted_list.size() > 1) {
int rand_index = rand() % sorted_list.size();
random_list.push_back(sorted_list.at(rand_index));
sorted_list.erase(sorted_list.begin() + rand_index);
}
// Finally push back the last remaining element to the random list
// The if() statement here is just a sanity check, in case K == 0
if(!sorted_list.empty()) {
random_list.push_back(sorted_list.at(0));
}
Étape 1: générez votre liste d'entiers.
Étape 2: Effectuer Knuth Shuffle .
notez que vous n'avez pas besoin de mélanger la liste entière, puisque L'algorithme Knuth Shuffle vous permet d'appliquer seulement n shuffles, où n est le nombre d'éléments à retourner. La création de la liste prendra tout de même un temps proportionnel à la taille de la liste, mais vous pouvez réutiliser votre liste existante pour les besoins futurs de mélange (en supposant que la taille reste la même) sans qu'il soit nécessaire de présélectionner la liste partiellement mélangée avant de redémarrer l'algorithme de mélange.
l'algorithme de base pour Knuth Shuffle est que vous commencez avec une liste d'entiers. Ensuite, vous changez le premier entier avec n'importe quel nombre dans la liste et retournez le (nouveau) Premier entier courant. Ensuite, vous changez le deuxième entier avec n'importe quel nombre dans la liste (sauf le premier) et retournez le (nouveau) deuxième entier courant. Puis...etc...
c'est un algorithme ridiculement simple, mais attention que vous incluiez l'élément courant dans la liste lors de l'exécution du swap ou vous briseriez l'algorithme.
la version D'échantillonnage du réservoir est assez simple:
my $N = 20;
my $k;
my @r;
while(<>) {
if(++$k <= $N) {
push @r, $_;
} elsif(rand(1) <= ($N/$k)) {
$r[rand(@r)] = $_;
}
}
print @r;
C'est $N lignes choisies au hasard de STDIN. Remplacez le truc <>/$_ par quelque chose d'autre si vous n'utilisez pas les lignes d'un fichier, mais c'est un algorithme assez simple.
si la liste est triée, par exemple, si vous voulez extraire des éléments K de N, Mais que vous ne vous souciez pas de leur ordre relatif, un algorithme efficace est proposé dans l'article un algorithme efficace pour L'échantillonnage aléatoire séquentiel (Jeffrey Scott Vitter, ACM Transactions on Mathematical Software , Vol. 13, No 1, Mars 1987, Pages 56-67.).
révisé à ajoutez le code en c++ en utilisant boost. Je viens de le taper et il pourrait y avoir beaucoup d'erreurs. Les nombres aléatoires viennent de la bibliothèque boost, avec une graine stupide, donc ne faites rien de sérieux avec ça.
/* Sampling according to [Vitter87].
*
* Bibliography
* [Vitter 87]
* Jeffrey Scott Vitter,
* An Efficient Algorithm for Sequential Random Sampling
* ACM Transactions on MAthematical Software, 13 (1), 58 (1987).
*/
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <string>
#include <iostream>
#include <iomanip>
#include <boost/random/linear_congruential.hpp>
#include <boost/random/variate_generator.hpp>
#include <boost/random/uniform_real.hpp>
using namespace std;
// This is a typedef for a random number generator.
// Try boost::mt19937 or boost::ecuyer1988 instead of boost::minstd_rand
typedef boost::minstd_rand base_generator_type;
// Define a random number generator and initialize it with a reproducible
// seed.
// (The seed is unsigned, otherwise the wrong overload may be selected
// when using mt19937 as the base_generator_type.)
base_generator_type generator(0xBB84u);
//TODO : change the seed above !
// Defines the suitable uniform ditribution.
boost::uniform_real<> uni_dist(0,1);
boost::variate_generator<base_generator_type&, boost::uniform_real<> > uni(generator, uni_dist);
void SequentialSamplesMethodA(int K, int N)
// Outputs K sorted random integers out of 0..N, taken according to
// [Vitter87], method A.
{
int top=N-K, S, curr=0, currsample=-1;
double Nreal=N, quot=1., V;
while (K>=2)
{
V=uni();
S=0;
quot=top/Nreal;
while (quot > V)
{
S++; top--; Nreal--;
quot *= top/Nreal;
}
currsample+=1+S;
cout << curr << " : " << currsample << "\n";
Nreal--; K--;curr++;
}
// special case K=1 to avoid overflow
S=floor(round(Nreal)*uni());
currsample+=1+S;
cout << curr << " : " << currsample << "\n";
}
void SequentialSamplesMethodD(int K, int N)
// Outputs K sorted random integers out of 0..N, taken according to
// [Vitter87], method D.
{
const int negalphainv=-13; //between -20 and -7 according to [Vitter87]
//optimized for an implementation in 1987 !!!
int curr=0, currsample=0;
int threshold=-negalphainv*K;
double Kreal=K, Kinv=1./Kreal, Nreal=N;
double Vprime=exp(log(uni())*Kinv);
int qu1=N+1-K; double qu1real=qu1;
double Kmin1inv, X, U, negSreal, y1, y2, top, bottom;
int S, limit;
while ((K>1)&&(threshold<N))
{
Kmin1inv=1./(Kreal-1.);
while(1)
{//Step D2: generate X and U
while(1)
{
X=Nreal*(1-Vprime);
S=floor(X);
if (S<qu1) {break;}
Vprime=exp(log(uni())*Kinv);
}
U=uni();
negSreal=-S;
//step D3: Accept ?
y1=exp(log(U*Nreal/qu1real)*Kmin1inv);
Vprime=y1*(1. - X/Nreal)*(qu1real/(negSreal+qu1real));
if (Vprime <=1.) {break;} //Accept ! Test [Vitter87](2.8) is true
//step D4 Accept ?
y2=0; top=Nreal-1.;
if (K-1 > S)
{bottom=Nreal-Kreal; limit=N-S;}
else {bottom=Nreal+negSreal-1.; limit=qu1;}
for(int t=N-1;t>=limit;t--)
{y2*=top/bottom;top--; bottom--;}
if (Nreal/(Nreal-X)>=y1*exp(log(y2)*Kmin1inv))
{//Accept !
Vprime=exp(log(uni())*Kmin1inv);
break;
}
Vprime=exp(log(uni())*Kmin1inv);
}
// Step D5: Select the (S+1)th record
currsample+=1+S;
cout << curr << " : " << currsample << "\n";
curr++;
N-=S+1; Nreal+=negSreal-1.;
K-=1; Kreal-=1; Kinv=Kmin1inv;
qu1-=S; qu1real+=negSreal;
threshold+=negalphainv;
}
if (K>1) {SequentialSamplesMethodA(K, N);}
else {
S=floor(N*Vprime);
currsample+=1+S;
cout << curr << " : " << currsample << "\n";
}
}
int main(void)
{
int Ntest=10000000, Ktest=Ntest/100;
SequentialSamplesMethodD(Ktest,Ntest);
return 0;
}
$ time ./sampling|tail
donne l'ouptut suivant sur mon ordinateur portable
99990 : 9998882
99991 : 9998885
99992 : 9999021
99993 : 9999058
99994 : 9999339
99995 : 9999359
99996 : 9999411
99997 : 9999427
99998 : 9999584
99999 : 9999745
real 0m0.075s
user 0m0.060s
sys 0m0.000s
ce code Ruby présente la méthode D'échantillonnage du réservoir , algorithme R . Dans chaque cycle, je sélectionne n=5
entiers aléatoires uniques de [0,N=10)
gamme:
t=0
m=0
N=10
n=5
s=0
distrib=Array.new(N,0)
for i in 1..500000 do
t=0
m=0
s=0
while m<n do
u=rand()
if (N-t)*u>=n-m then
t=t+1
else
distrib[s]+=1
m=m+1
t=t+1
end #if
s=s+1
end #while
if (i % 100000)==0 then puts i.to_s + ". cycle..." end
end #for
puts "--------------"
puts distrib
sortie:
100000. cycle...
200000. cycle...
300000. cycle...
400000. cycle...
500000. cycle...
--------------
250272
249924
249628
249894
250193
250202
249647
249606
250600
250034
tous les entiers entre 0-9 ont été choisis avec presque la même probabilité.
c'est essentiellement algorithme de Knuth appliqué à des séquences arbitraires (en effet, cette réponse a une version LISP de cela). L'algorithme est O(N) dans le temps et peut être O (1) dans la mémoire si la séquence y est striée comme indiqué dans @Michaelcramer'S réponse .
Voici un moyen de le faire en O(N) sans stockage supplémentaire. Je suis presque sûr que ce n'est pas une distribution purement aléatoire, mais c'est probablement assez proche pour de nombreux usages.
/* generate N sorted, non-duplicate integers in [0, max[ in O(N))*/
int *generate(int n, int max) {
float step,a,v=0;
int i;
int *g = (int *)calloc(n, sizeof(int));
if ( ! g) return 0;
for (i=0; i<n; i++) {
step = (max-v)/(float)(n-i);
v+ = floating_pt_random_in_between(0.0, step*2.0);
if ((int)v == g[i-1]){
v=(int)v+1; //avoid collisions
}
g[i]=v;
}
while (g[i]>max) {
g[i]=max; //fix up overflow
max=g[i--]-1;
}
return g;
}
C'est le code Perl. Grep est un filtre, et comme toujours, je n'ai pas testé ce code.
@list = grep ($_ % I) == 0, (0..N);
- I = intervalle
- N = Limite Supérieure
n'obtenez que des nombres qui correspondent à votre intervalle via l'opérateur de module.
@list = grep ($_ % 3) == 0, (0..30);
renvoie 0, 3, 6,... 30
c'est un pseudo code Perl. Vous devrez peut-être ajuster pour la compiler.