Salut,
Pour comprendre pourquoi la version modulo est biaisée, le mieux est encore de prendre un exemple.
ON va admettre pour cet exemple que RAND_MAX vaut 16, et que tu veux un nombre aléatoire entre 1 et 10.
rand()
peut retourner une valeur comprise entre 0 et 15, et donc un appel à randrange(1, 11)
peut retourner 16 résultats différents :
- 0 => 1
- 1 => 2
- 2 => 3
- 3 => 4
- 4 => 5
- 5 => 6
- 6 => 7
- 7 => 8
- 8 => 9
- 9 => 10
- 10 => 1
- 11 => 2
- 12 => 3
- 13 => 4
- 14 => 5
- 15 => 6
Qu'est-ce qu'on remarque ? Le cycle est coupé au milieu, les valeurs 1-6 ont deux fois plus de chances de sortir que les valeurs 7-10.
Si on admet que rand()
retourne effectivement un nombre aléatoire équiprobable compris entre 0 et RAND_MAX, ta fonction ne fait un tirage correct que si RAND_MAX est un multiple de l'intervalle, i.e. si (max-min)%RAND_MAX==0
.
Maintenant faisons la même chose avec ta deuxième fonction :
- 0 =>
1+(0/17)*10
= 1
- 1 =>
1+(1/17)*10
= 27/17 = 1.59
- 2 =>
1+(2/17)*10
= 37/17 = 2.18
- 3 => 47/17 = 2.76
- 4 => 57/17 = 3.35
- 5 => 67/17 = 3.91
- 6 => 4.53
- 7 => 5.12
- 8 => 5.71
- 9 => 6.29
- 10 => 6.88
- 11 => 7.47
- 12 => 8.06
- 13 => 8.65
- 14 => 9.24
- 15 => 9.82
On voit que la répartition dans l'intervalle continu 1-10 n'est pas trop mauvaise.
Si on retire le +1
, on peut obtenir comme tirage maximum 15 => 1+(15/16)*10
= 166/16 = 10.38.
Si en appelant randmax(1, 11)
tu souhaitais un nombre entre 1 et 10, tu peux donc tomber hors bornes.
En fait, ça dépend de la définition mathématique que tu souhaites pour ta fonction. Si tu veux obtenir des nombres aléatoires réels, le +1
n'a pas vraiment de sens d'après moi, car quand on écrit randrange(1, 11)
on souhaite habituellement tomber sur un nombre dans l'intervalle continu semi-ouvert 1-11, i.e. on peut tomber sur 1 et sur 10.9999, mais jamais sur 11.
Quand dans la plupart des langages de programmation on a un Math.random
qui retourne une valeur entre 0 et 1, c'est la définition qui prévaut habituellement: on ne peut jamais tomber sur 1.
Par contre si ton but est d'obtenir des nombres aléatoires entiers, le +1
peut changer la donne. Quand on écrit randrange(1, 11)
, veut-on que 11 puisse faire partie des solutions possibles ou non ?
Les deux sont intéressants suivant le cas: quand on jette un dé, c'est très naturel d'écrire randrange(1, 6)
et que 6 fasse partie des réponses possibles; quand on veut tirer une case aléatoire dans un tableau, c'est plus simple d'écrire randrange(0, length)
tout en étant certain que length
ne tombera jamais; on est quitte d'oublier l'éternel -1
.
Tu remarqueras que, même pour ta deuxième fonction, si on ramète ces sorties à des nombres entiers, on a le même problème, certaines valeurs ont deux fois plus de chance de tomber que d'autres. Dans la FAQ, on rappelle que l'intervalle (max-min)
doit être largement inférieur à RAND_MAX pour que ce biais devienne suffisament négligeable. J'ai fait exprès pour l'exemple de prendre des petites valeurs pour que ce problème paraisse évident.
Au-delà des possibles écueils évoqués jusqu'ici, on accuse souvent rand()
de ne pas être un très bon générateur de nombres aléatoires dans la plupart des implémentations de la bibliothèque C standard, pour plusieurs raisons :
- RAND_MAX vaut souvent 65536 ou 32768 seulement, ce qui pose le problème de la condition
(max-min) << RAND_MAX
qui ne peut pas être terriblement remplie, et ça peut déjà se ressentir pour des tirages entre 1 et 1000.
- rand est très souvent implémenté comme un générateur aléatoire à congruances linéaires, ce qui n'est pas un problème en soi, mais avec des paramètres particulièrement mal choisis; et comme RAND_MAX est petit, la période est forcément petite et d'autant plus avec des mauvais paramètres; donc le générateur est extrêmement prévisible.
- Non seulement la qualité des nombres générés entre 0 et RAND_MAX n'est souvent pas terrible, mais la réalité est encore pire si on analyse les sorties bit à bit.
- Même l'initialisation du générateur avec srand n'est souvent pas rapportée comme très bonne, car se basant uniquement sur l'heure du PC à la seconde près et avec un algorithme aussi naïf que
valeur = time(NULL)%RAND_MAX
. Donc même sur plusieurs exécutions successives, ça peut rester extrêmement prévisible.
Par voie de conséquence, si on veut un générateur aléatoire un minimum décent en C, on essaiera généralement de prendre une implémentation différente de celle de la bibliothèque standard. Souvent on prend un générateur Mercene Twister a.k.a. MT19937, car il est assez rapide, ne nécéssite pas trop de mémoire, et est suffisament imprévisible pour les utilisations courantes (sauf pour la cryptographie).
Pour info, un générateur MT19937 est inclus dans la bibliothèque standard du C++11.
Rajout: J'ai été multi-grillé; mais c'est pas grave.