Merci à tous pour vos excellentes réponses. Je me suis retrouvé avec la solution suivante, que je voudrais partager.
Avant d'entrer dans les détails du pourquoi et du comment, voici le tl; dr : mon nouveau script brillant :-)
#!/usr/bin/env bash
#
# Generates a random integer in a given range
# computes the ceiling of log2
# i.e., for parameter x returns the lowest integer l such that 2**l >= x
log2() {
local x=$1 n=1 l=0
while (( x>n && n>0 ))
do
let n*=2 l++
done
echo $l
}
# uses $RANDOM to generate an n-bit random bitstring uniformly at random
# (if we assume $RANDOM is uniformly distributed)
# takes the length n of the bitstring as parameter, n can be up to 60 bits
get_n_rand_bits() {
local n=$1 rnd=$RANDOM rnd_bitlen=15
while (( rnd_bitlen < n ))
do
rnd=$(( rnd<<15|$RANDOM ))
let rnd_bitlen+=15
done
echo $(( rnd>>(rnd_bitlen-n) ))
}
# alternative implementation of get_n_rand_bits:
# uses /dev/urandom to generate an n-bit random bitstring uniformly at random
# (if we assume /dev/urandom is uniformly distributed)
# takes the length n of the bitstring as parameter, n can be up to 56 bits
get_n_rand_bits_alt() {
local n=$1
local nb_bytes=$(( (n+7)/8 ))
local rnd=$(od --read-bytes=$nb_bytes --address-radix=n --format=uL /dev/urandom | tr --delete " ")
echo $(( rnd>>(nb_bytes*8-n) ))
}
# for parameter max, generates an integer in the range {0..max} uniformly at random
# max can be an arbitrary integer, needs not be a power of 2
rand() {
local rnd max=$1
# get number of bits needed to represent $max
local bitlen=$(log2 $((max+1)))
while
# could use get_n_rand_bits_alt instead if /dev/urandom is preferred over $RANDOM
rnd=$(get_n_rand_bits $bitlen)
(( rnd > max ))
do :
done
echo $rnd
}
# MAIN SCRIPT
# check number of parameters
if (( $# != 1 && $# != 2 ))
then
cat <<EOF 1>&2
Usage: $(basename $0) [min] max
Returns an integer distributed uniformly at random in the range {min..max}
min defaults to 0
(max - min) can be up to 2**60-1
EOF
exit 1
fi
# If we have one parameter, set min to 0 and max to $1
# If we have two parameters, set min to $1 and max to $2
max=0
while (( $# > 0 ))
do
min=$max
max=$1
shift
done
# ensure that min <= max
if (( min > max ))
then
echo "$(basename $0): error: min is greater than max" 1>&2
exit 1
fi
# need absolute value of diff since min (and also max) may be negative
diff=$((max-min)) && diff=${diff#-}
echo $(( $(rand $diff) + min ))
Enregistrez cela dans ~/bin/rand
et vous avez à votre disposition une fonction aléatoire douce dans bash qui peut échantillonner un entier dans une plage arbitraire donnée. La plage peut contenir des nombres entiers négatifs et positifs et peut avoir une longueur maximale de 2 60 -1:
$ rand
Usage: rand [min] max
Returns an integer distributed uniformly at random in the range {min..max}
min defaults to 0
(max - min) can be up to 2**60-1
$ rand 1 10
9
$ rand -43543 -124
-15757
$ rand -3 3
1
$ for i in {0..9}; do rand $((2**60-1)); done
777148045699177620
456074454250332606
95080022501817128
993412753202315192
527158971491831964
336543936737015986
1034537273675883580
127413814010621078
758532158881427336
924637728863691573
Toutes les idées des autres répondeurs étaient excellentes. Les réponses de terdon , JF Sebastian et jimmij ont utilisé des outils externes pour effectuer la tâche de manière simple et efficace. Cependant, j'ai préféré une vraie solution bash pour une portabilité maximale, et peut-être un peu, simplement par amour pour bash;)
Réponses de Ramesh et l0b0 utilisées /dev/urandom
ou /dev/random
combinées avec od
. C'est bien, cependant, leurs approches avaient l'inconvénient de ne pouvoir échantillonner que des nombres entiers aléatoires compris entre 0 et 2 8n -1 pour certains n, car cette méthode échantillonne les octets, c'est-à-dire les chaînes de bits de longueur 8. Ce sont de très gros sauts avec croissant n.
Enfin, la réponse de Falco décrit l'idée générale de la façon dont cela pourrait être fait pour des plages arbitraires (pas seulement des puissances de deux). Fondamentalement, pour une plage donnée {0..max}
, nous pouvons déterminer quelle est la puissance suivante de deux, c'est-à-dire exactement combien de bits sont nécessaires pour représenter max
comme une chaîne de bits. Ensuite, nous pouvons échantillonner juste autant de bits et voir si cet enregistrement, en tant qu'entier, est supérieur à max
. Si oui, répétez. Puisque nous échantillonnons autant de bits que nécessaire pour représenter max
, chaque itération a une probabilité supérieure ou égale à 50% de réussite (50% dans le pire des cas, 100% dans le meilleur des cas). C'est donc très efficace.
Mon script est essentiellement une implémentation concrète de la réponse de Falco, écrite en bash pur et très efficace car elle utilise les opérations bit à bit intégrées de bash pour échantillonner des chaînes de bits de la longueur souhaitée. Il honore en outre une idée d' Eliah Kagan qui suggère d'utiliser la $RANDOM
variable intégrée en concaténant les chaînes de bits résultant des invocations répétées de $RANDOM
. J'ai en fait implémenté à la fois les possibilités d'utilisation /dev/urandom
et $RANDOM
. Par défaut, le script ci-dessus utilise $RANDOM
. (Et ok, si vous utilisez, /dev/urandom
nous avons besoin de od et tr , mais ceux-ci sont soutenus par POSIX.)
Alors, comment ça marche?
Avant d'entrer dans le détail, deux observations:
Il s'avère que bash ne peut pas gérer des entiers supérieurs à 2 63 -1. Voir par vous-même:
$ echo $((2**63-1))
9223372036854775807
$ echo $((2**63))
-9223372036854775808
Il semblerait que bash utilise en interne des entiers signés 64 bits pour stocker des entiers. Donc, à 2 63, il «s'enroule» et nous obtenons un entier négatif. Nous ne pouvons donc pas espérer obtenir une plage supérieure à 2 63 -1 avec la fonction aléatoire que nous utilisons. Bash ne peut tout simplement pas le gérer.
Chaque fois que nous voulons échantillonner une valeur dans une plage arbitraire entre min
et max
avec éventuellement min != 0
, nous pouvons simplement échantillonner une valeur entre 0
et à la max-min
place, puis ajouter min
au résultat final. Cela fonctionne même si min
et peut-être aussi max
sont négatifs , mais nous devons faire attention à échantillonner une valeur entre 0
et la valeur absolue de max-min
. Ainsi, nous pouvons nous concentrer sur la façon d'échantillonner une valeur aléatoire entre 0
et un entier positif arbitraire max
. Le reste est facile.
Étape 1: déterminer le nombre de bits nécessaires pour représenter un entier (le logarithme)
Donc, pour une valeur donnée max
, nous voulons savoir exactement combien de bits sont nécessaires pour la représenter comme une chaîne de bits. C'est ainsi que plus tard, nous pouvons échantillonner au hasard seulement autant de bits que nécessaire, ce qui rend le script si efficace.
Voyons voir. Comme avec les n
bits, nous pouvons représenter jusqu'à la valeur 2 n -1, alors le nombre n
de bits nécessaires pour représenter une valeur arbitraire x
est plafond (log 2 (x + 1)). Donc, nous avons besoin d'une fonction pour calculer le plafond d'un logarithme à la base 2. Elle est plutôt explicite:
log2() {
local x=$1 n=1 l=0
while (( x>n && n>0 ))
do
let n*=2 l++
done
echo $l
}
Nous avons besoin de la condition, n>0
donc si elle devient trop grande, s'enroule et devient négative, la boucle est garantie de se terminer.
Étape 2: échantillonner une chaîne binaire aléatoire de longueur n
Les idées les plus portables sont soit d'utiliser /dev/urandom
(ou même /dev/random
s'il y a une bonne raison) soit la $RANDOM
variable intégrée de bash . Voyons d'abord comment le faire $RANDOM
.
Option A: utilisation $RANDOM
Cela utilise l' idée mentionnée par Eliah Kagan. Fondamentalement, puisque $RANDOM
échantillonne un entier de 15 bits, nous pouvons utiliser $((RANDOM<<15|RANDOM))
pour échantillonner un entier de 30 bits. Cela signifie, décaler une première invocation de $RANDOM
15 bits vers la gauche et appliquer au niveau du bit ou avec une seconde invocation de $RANDOM
, concaténant efficacement deux chaînes de bits échantillonnées indépendamment (ou au moins aussi indépendantes que le bash intégré de $RANDOM
va).
Nous pouvons répéter ceci pour obtenir un entier de 45 bits ou 60 bits. Après cela, bash ne peut plus le gérer, mais cela signifie que nous pouvons facilement échantillonner une valeur aléatoire entre 0 et 2 60 -1. Donc, pour échantillonner un entier de n bits, nous répétons la procédure jusqu'à ce que notre chaîne de bits aléatoire, dont la longueur augmente par pas de 15 bits, ait une longueur supérieure ou égale à n. Enfin, nous coupons les bits qui sont trop en décalant de façon appropriée au niveau du bit vers la droite, et nous nous retrouvons avec un entier aléatoire de n bits.
get_n_rand_bits() {
local n=$1 rnd=$RANDOM rnd_bitlen=15
while (( rnd_bitlen < n ))
do
rnd=$(( rnd<<15|$RANDOM ))
let rnd_bitlen+=15
done
echo $(( rnd>>(rnd_bitlen-n) ))
}
Option B: utilisation /dev/urandom
Alternativement, nous pouvons utiliser od
et /dev/urandom
pour échantillonner un entier de n bits. od
lira des octets, c'est-à-dire des chaînes de bits de longueur 8. De la même manière que dans la méthode précédente, nous échantillonnons juste autant d'octets que le nombre équivalent de bits échantillonnés est supérieur ou égal à n, et coupons les bits qui sont trop.
Le plus petit nombre d'octets nécessaires pour obtenir au moins n bits est le plus petit multiple de 8 supérieur ou égal à n, c'est-à-dire étage ((n + 7) / 8).
Cela ne fonctionne que jusqu'à 56 bits. L'échantillonnage d'un octet supplémentaire nous donnerait un entier 64 bits, c'est-à-dire une valeur jusqu'à 2 64 -1, que bash ne peut pas gérer.
get_n_rand_bits_alt() {
local n=$1
local nb_bytes=$(( (n+7)/8 ))
local rnd=$(od --read-bytes=$nb_bytes --address-radix=n --format=uL /dev/urandom | tr --delete " ")
echo $(( rnd>>(nb_bytes*8-n) ))
}
Assembler les morceaux: Obtenez des nombres entiers aléatoires dans des plages arbitraires
Nous pouvons n
maintenant échantillonner des chaînes de bits, mais nous voulons échantillonner des entiers dans une plage de 0
à max
, uniformément au hasard , où max
peut être arbitraire, pas nécessairement une puissance de deux. (Nous ne pouvons pas utiliser modulo car cela crée un biais.)
Tout ce pourquoi nous avons essayé si dur d'échantillonner autant de bits que nécessaire pour représenter la valeur max
, c'est que nous pouvons maintenant utiliser en toute sécurité (et efficacement) une boucle pour échantillonner de manière répétée une n
chaîne de bits -bit jusqu'à ce que nous échantillonnions une valeur qui est inférieure ou égal à max
. Dans le pire des cas ( max
est une puissance de deux), chaque itération se termine avec une probabilité de 50%, et dans le meilleur des cas ( max
est une puissance de deux moins un), la première itération se termine avec certitude.
rand() {
local rnd max=$1
# get number of bits needed to represent $max
local bitlen=$(log2 $((max+1)))
while
# could use get_n_rand_bits_alt instead if /dev/urandom is preferred over $RANDOM
rnd=$(get_n_rand_bits $bitlen)
(( rnd > max ))
do :
done
echo $rnd
}
Envelopper les choses
Enfin, nous voulons échantillonner des entiers entre min
et max
, où min
et max
peut être arbitraire, voire négatif. Comme mentionné précédemment, cela est désormais trivial.
Mettons tout cela dans un script bash. Faites des analyses d'arguments ... Nous voulons deux arguments min
et max
, ou un seul argument max
, par min
défaut 0
.
# check number of parameters
if (( $# != 1 && $# != 2 ))
then
cat <<EOF 1>&2
Usage: $(basename $0) [min] max
Returns an integer distributed uniformly at random in the range {min..max}
min defaults to 0
(max - min) can be up to 2**60-1
EOF
exit 1
fi
# If we have one parameter, set min to 0 and max to $1
# If we have two parameters, set min to $1 and max to $2
max=0
while (( $# > 0 ))
do
min=$max
max=$1
shift
done
# ensure that min <= max
if (( min > max ))
then
echo "$(basename $0): error: min is greater than max" 1>&2
exit 1
fi
... et, enfin, pour échantillonner uniformément au hasard une valeur entre min
et max
, nous échantillonnons un entier aléatoire entre 0
et la valeur absolue de max-min
, et ajoutons min
au résultat final. :-)
diff=$((max-min)) && diff=${diff#-}
echo $(( $(rand $diff) + min ))
Inspiré par cela , je pourrais essayer d'utiliser dieharder pour tester et comparer ce PRNG, et mettre mes résultats ici. :-)