web-dev-qa-db-fra.com

Nombres aléatoires uniques dans un tableau d'entiers dans le langage de programmation C

Duplicata possible:
Nombres aléatoires uniques dans O (1)?

Comment puis-je remplir un tableau entier avec des valeurs uniques (pas de doublons) en C?

int vektor[10];   

for (i = 0; i < 10; i++) {
    vektor[i] = Rand() % 100 + 1;
}

//No uniqueness here
28
Chris_45

Il existe plusieurs façons de résoudre votre problème, chacune ayant ses propres avantages et inconvénients.

Je voudrais d'abord noter que vous avez déjà reçu pas mal de réponses qui font ce qui suit: elles génèrent un nombre aléatoire, puis vérifient d'une manière ou d'une autre si elles ont déjà été utilisées dans le tableau, et si elles l'ont déjà été, elles en génèrent juste une autre numéro jusqu'à ce qu'ils en trouvent un inutilisé. Il s'agit d'une approche naïve et, à vrai dire, sérieusement erronée. Le problème vient de la nature cyclique des essais et erreurs de la génération de nombres ("si déjà utilisé, essayez à nouveau"). Si la plage numérique (par exemple, [1..N]) est proche de la longueur du tableau souhaité (par exemple, M), alors vers la fin, l'algorithme peut passer énormément de temps à essayer de trouver le nombre suivant. Si le générateur de nombres aléatoires est même un peu cassé (disons, ne génère jamais de nombre, ou le fait très rarement), alors avec N == M l'algorithme est garanti pour boucler pour toujours (ou pendant très longtemps). Généralement, cette approche par essais et erreurs est inutile ou, au mieux, imparfaite.

Une autre approche déjà présentée ici consiste à générer une permutation aléatoire dans un tableau de taille N. L'idée de permutation aléatoire est prometteuse, mais le faire sur un tableau de taille N (lorsque M << N) générera certainement plus de chaleur que de lumière , parlant au figuré.

De bonnes solutions à ce problème peuvent être trouvées, par exemple, dans "Programming Pearls" de Bentley (et certaines sont tirées de Knuth).


  • L'algorithme de Knuth. Il s'agit d'un algorithme très simple avec une complexité de O(N) (c'est-à-dire la plage numérique ), ce qui signifie qu'il est plus utilisable lorsque M est proche de N. Cependant, cet algorithme ne nécessite aucune mémoire supplémentaire en plus de votre tableau vektor, contrairement à la variante déjà proposée avec permutations (ce qui signifie qu'il prend O(M) mémoire, pas O(N) comme d'autres algorithmes basés sur la permutation suggérés ici). Ce dernier en fait un algorithme viable même pour M << N cas.

L'algorithme fonctionne comme suit: parcourez tous les nombres de 1 à N et sélectionnez le nombre actuel avec probabilité rm / rn, où rm est le nombre de nombres qu'il nous reste à trouver, et rn est le nombre de nombres qu'il nous faut encore parcourir. Voici une implémentation possible pour votre cas

#define M 10
#define N 100

int in, im;

im = 0;

for (in = 0; in < N && im < M; ++in) {
  int rn = N - in;
  int rm = M - im;
  if (Rand() % rn < rm)    
    /* Take it */
    vektor[im++] = in + 1; /* +1 since your range begins from 1 */
}

assert(im == M);

Après ce cycle, nous obtenons un tableau vektor rempli de nombres choisis au hasard dans l'ordre croissant. Le bit "ordre croissant" est ce dont nous n'avons pas besoin ici. Donc, afin de "corriger" que nous faisons juste une permutation aléatoire des éléments de vektor et nous avons terminé. Notez que ceci est une permutation O(M) ne nécessitant pas de mémoire supplémentaire. (Je laisse de côté l'implémentation de l'algorithme de permutation. Beaucoup de liens ont déjà été donnés ici.).

Si vous regardez attentivement les algorithmes basés sur la permutation proposés ici qui fonctionnent sur un tableau de longueur N, vous verrez que la plupart d'entre eux sont à peu près ce même algorithme de Knuth, mais reformulés pour M == N. Dans ce cas, le cycle de sélection ci-dessus choisira chaque nombre dans la plage [1..N] avec la probabilité 1, se transformant effectivement en initialisation d'un N-tableau avec les nombres 1 à N. En tenant compte de cela, je pense qu'il devient plutôt évident que l'exécution de cet algorithme pour M == N, puis tronquer le résultat (en supprimant peut-être la majeure partie) a beaucoup moins de sens que d'exécuter cet algorithme dans sa forme d'origine pour la valeur d'origine de M et d'obtenir le résultat immédiatement, sans aucune troncature.


  • L'algorithme Floyd (voir ici ). Cette approche a la complexité d'environ O(M) (dépend de la structure de recherche utilisée), elle est donc mieux adaptée lorsque M << N. Cette approche garde une trace des nombres aléatoires déjà générés, il nécessite donc de la mémoire supplémentaire. Cependant, la beauté de cela est qu'il ne le fait pas effectuer l'une de ces abominables itérations d'essais et d'erreurs, en essayant de trouver un nombre aléatoire inutilisé. Cet algorithme est garanti pour générer un nombre aléatoire unique après chaque appel au générateur de nombres aléatoires.

Voici une implémentation possible pour votre cas. (Il existe différentes façons de garder une trace des nombres déjà utilisés. Je vais simplement utiliser un tableau d'indicateurs, en supposant que N n'est pas prohibitif)

#define M 10
#define N 100    

unsigned char is_used[N] = { 0 }; /* flags */
int in, im;

im = 0;

for (in = N - M; in < N && im < M; ++in) {
  int r = Rand() % (in + 1); /* generate a random number 'r' */

  if (is_used[r])
    /* we already have 'r' */
    r = in; /* use 'in' instead of the generated number */

  assert(!is_used[r]);
  vektor[im++] = r + 1; /* +1 since your range begins from 1 */
  is_used[r] = 1;
}

assert(im == M);

Pourquoi les travaux ci-dessus ne sont pas immédiatement évidents. Mais ça marche. Des nombres exactement M de l'intervalle [1..N] seront choisis avec une distribution uniforme.

Notez que pour les grands N, vous pouvez utiliser une structure basée sur la recherche pour stocker les nombres "déjà utilisés", obtenant ainsi un algorithme Nice O (M log M) avec une exigence de mémoire O(M)).

(Il y a cependant une chose à propos de cet algorithme: bien que le tableau résultant ne soit pas ordonné, une certaine "influence" de l'ordre 1..N d'origine sera toujours présente dans le résultat. Par exemple, il est évident que le nombre N, si sélectionné, ne peut être que le dernier membre du tableau résultant. Si cette "contamination" du résultat par l'ordre non souhaité n'est pas acceptable, le tableau vektor résultant peut être mélangé de manière aléatoire, tout comme dans le Khuth algorithme).


Notez le point très critique observé dans la conception de ces deux algorithmes: ils ne font jamais boucle, essayant de trouver un nouveau nombre aléatoire inutilisé. Tout algorithme qui effectue des itérations par essais et erreurs avec des nombres aléatoires est défectueux du point de vue pratique. De plus, la consommation de mémoire de ces algorithmes est liée à M, pas à N

Je recommanderais à l'OP l'algorithme de Floyd, car dans son application, M semble être considérablement inférieur à N et qu'il ne nécessite pas (ou peut-être pas) un passage supplémentaire pour la permutation. Cependant, pour de si petites valeurs de N, la différence peut être négligeable.

75
AnT

Dans votre exemple (choisissez 10 nombres aléatoires uniques entre 1 et 100), vous pouvez créer une liste avec les nombres de 1 à 100, utiliser le générateur de nombres aléatoires pour mélanger la liste, puis prendre les 10 premières valeurs de la liste.

int list[100], vektor[10];
for (i = 0; i < 100; i++) {
    list[i] = i;
}
for (i = 0; i < 100; i++) {
    int j = i + Rand() % (100 - i);
    int temp = list[i];
    list[i] = list[j];
    list[j] = temp;
}
for (i = 0; i < 10; i++) {
    vektor[i] = list[i];
}

Sur la base du commentaire de cobbal ci-dessous, il est encore mieux de simplement dire:

for (i = 0; i < 10; i++) {
    int j = i + Rand() % (100 - i);
    int temp = list[i];
    list[i] = list[j];
    list[j] = temp;

    vektor[i] = list[i];
}

Maintenant c'est O(N) pour mettre en place la liste mais O(M) pour choisir les éléments aléatoires.

5
mob

Je pense que cela le fera (je n'ai pas essayé de le construire, donc les erreurs de syntaxe sont laissées à corriger comme exercice pour le lecteur). Il pourrait y avoir des moyens plus élégants, mais c'est la solution de force brute:

int vektor[10];    
int random;
int uniqueflag;
int i, j

for(i = 0; i < 10; i++) {
     do {
        /* Assume things are unique... we'll reset this flag if not. */
        uniqueflag = 1;
        random = Rand() % 100+ 1;
        /* This loop checks for uniqueness */
        for (j = 0; j < i && uniqueflag == 1; j++) {
           if (vektor[j] == random) {
              uniqueflag = 0;
           }
        }
     } while (uniqueflag != 1);
     vektor[i] = random;
}
3
Chris J

Générer simplement des nombres aléatoires et voir s'ils sont OK est une mauvaise façon de résoudre ce problème en général. Cette approche prend toutes les valeurs possibles, les mélange et prend ensuite les dix premiers. Ceci est directement analogue à mélanger un jeu de cartes et à distribuer le dessus.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define randrange(N) Rand() / (Rand_MAX/(N) + 1)

#define MAX 100        /* Values will be in the range (1 .. MAX) */
static int vektor[10];
int candidates[MAX];

int main (void) {
  int i;

  srand(time(NULL));   /* Seed the random number generator. */

  for (i=0; i<MAX; i++)
    candidates[i] = i;

  for (i = 0; i < MAX-1; i++) {
    int c = randrange(MAX-i);
    int t = candidates[i];
    candidates[i] = candidates[i+c];
    candidates[i+c] = t;
  }

  for (i=0; i<10; i++)
    vektor[i] = candidates[i] + 1;

  for (i=0; i<10; i++)
    printf("%i\n", vektor[i]);

  return 0;
}

Pour plus d'informations, consultez comp.lang.c FAQ listez la question 13.19 pour le mélange et question 13.16 sur la génération de nombres aléatoires.

3
Tim

Une façon serait de vérifier si le tableau contient déjà le nouveau nombre aléatoire, et si c'est le cas, d'en créer un nouveau et de réessayer.

Cela ouvre la possibilité (aléatoire;)) que vous n'obteniez jamais un nombre qui n'est pas dans le tableau. Par conséquent, vous devez compter le nombre de fois que vous vérifiez si le nombre est déjà dans le tableau et si le nombre dépasse MAX_DUPLICATE_COUNT, lancez une exception ou plus :) (EDIT, vu que vous êtes en C. Oubliez l'exceptionpart :) Retournez une erreur code à la place: P)

0
cwap

Voici une méthode O(M) temps moyen.

Méthode: si M <= N/2, utilisez la procédure S (M, N) (ci-dessous) pour générer le tableau de résultats R, et renvoyez R. Si M> N/2, utilisez la procédure S (NM, N) pour générer R, puis calculez X = {1..M}\R [le complément de R dans {1..M}], mélangez X avec mélange Fisher-Yates [dans le temps O (M)] et retournez X.

Dans le cas M> N/2, où O(M) == O (N), il existe plusieurs façons rapides de calculer le complément. Dans le code ci-dessous, par souci de concision, j'ai inclus uniquement un exemple de procédure S (M, N) codé en ligne dans main (). Le mélange Fisher-Yates est O(M) et est illustré dans la réponse principale à la question connexe # 196017 . Autres questions connexes précédentes: # 158716 et # 54059 .

La raison pour laquelle S (M, N) prend O(M) fois au lieu de O(N) fois où M <N/2 est cela, comme décrit dans Problème du collecteur de coupons l'attente E (t_k) est k H_k, à partir de laquelle E (t_ {k/2}) = k (H_k - H_ {k/2}) ou environ k * (ln (k) -ln (k/2) + O (1)) = k * (ln (k/(k/2))) + O (1)) = k * (ln (2) + O (1)) = O (k).

Procédure S (k, N): [Le corps de cette procédure est la douzaine de lignes après le commentaire "Gen M nombres aléatoires distincts" dans le code ci-dessous.] Allouez et initialisez trois tableaux d'entiers M + 1 éléments H, L et V à toutes les valeurs -1. Pour i = 0 à M-1: Mettez une valeur aléatoire v dans V [i] et dans le nœud sentinelle V [-1]. Obtenez une des têtes de liste M de H [v% M] et suivez cette liste jusqu'à trouver une correspondance avec v. Si la correspondance est à V [-1], alors v est une nouvelle valeur; mettez donc à jour la tête de liste H [v% M] et le lien de liste L [i]. Si la correspondance n'est pas à V [-1], obtenez et testez un autre v, etc.

Chaque étape "suivre la liste" a un coût prévu O(1) car à chaque étape sauf la dernière, la longueur moyenne de la liste est inférieure à 1. (à la fin du traitement, les M listes contiennent M éléments, donc la longueur moyenne augmente progressivement jusqu'à exactement 1.)

 // randomMofN - jiw 8 Nov 2011     
 // Re: https://stackoverflow.com/questions/1608181/
 #include <stdlib.h>
 #include <stdio.h>
 int main(int argc, char *argv[]) {
   int h, i, j, tM, M, N, par=0, *H, *L, *V, cxc=0;
   // Get M and N values
   ++par; M = 42;  if (argc > par) M = atoi(argv[par]);
   ++par; N = 137; if (argc > par) N = atoi(argv[par]);
   tM = 3*M+3;
   H = malloc(tM*sizeof(int));
   printf ("M = %d,  N = %d  %s\n", M, N, H?"":"\nmem error");
   if (!H) exit(13);
   for (i=0; i<tM; ++i)           // Init arrays to -1's
     H[i] = -1;
   L = H+M;  V = L+M;

   // Gen M distinct random numbers
   for (i=0; i<M; ++i) {
     do {
       ++cxc;                     // complexity counter
       V[-1] = V[i] = random()%N;
       h = V[i]%M;                // h = list-head index
       j = H[h];
       while (V[j] != V[i])
         j = L[j];
     } while (j>=0);
     L[i] = H[h];
     H[h] = i;
   }

   // Print results
   for (j=i=0; i<M; ++i) {
     j += printf ("%4d ", V[i]);
     if (j>66) j = printf ("\n");
   }
   printf ("\ncxc %d\n", cxc);
   return 0;
 }
0

Une solution rapide consiste à créer un tableau de masques de tous les nombres possibles initialisés à zéro et à définir une entrée si ce nombre est généré

int Rand_array[100] = {0};
int vektor[10];   
int i=0, rnd;
while(i<10) {
    rnd = Rand() % 100+ 1;
    if ( Rand_array[rnd-1] == 0 ) {
        vektor[i++] = rnd;
        Rand_array[rnd-1] = 1;
    }
}
0
Amro

Générez les premier et deuxième chiffres séparément. Mélangez-les plus tard si nécessaire. (syntaxe de mémoire)

int vektor[10];
int i = 0;

while(i < 10) {
  int j = Rand() % 10;
  if (vektor[j] == 0) { vektor[j] = Rand() % 10 + j * 10; i ++;}
}

Cependant, les nombres seront presque séparés de n, 0 <n <10.

Ou bien, vous devez conserver les numéros triés (O(n log n)), afin que la nouvelle génération puisse être vérifiée rapidement pour la présence (O(log n)).

0
Milind C

j'aime l'algorithme Floyd.

mais nous pouvons prendre tout le nombre aléatoire de 0 à M (et non à in):

#define M 10
#define N 100    

unsigned char is_used[N] = { 0 }; /* flags */
int in, im;

im = 0;

for (in = N - M; in < N && im < M; ++in) {
  int r = Rand() % (N + 1); /* generate a random number 'r' */

  while (is_used[r])
  {
     /* we already have 'r' */
     r = Rand() % (N + 1);
  }
  vektor[im++] = r + 1; /* +1 since your range begins from 1 */
  is_used[r] = 1;
}

assert(im == M);