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/randet 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/urandomou /dev/randomcombiné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 maxcomme 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 $RANDOMvariable 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/urandomet $RANDOM. Par défaut, le script ci-dessus utilise $RANDOM. (Et ok, si vous utilisez, /dev/urandomnous 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 minet maxavec éventuellement min != 0, nous pouvons simplement échantillonner une valeur entre 0et à la max-minplace, puis ajouter minau résultat final. Cela fonctionne même si minet peut-être aussi maxsont négatifs , mais nous devons faire attention à échantillonner une valeur entre 0et la valeur absolue de max-min . Ainsi, nous pouvons nous concentrer sur la façon d'échantillonner une valeur aléatoire entre 0et 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 nbits, nous pouvons représenter jusqu'à la valeur 2 n -1, alors le nombre nde bits nécessaires pour représenter une valeur arbitraire xest 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>0donc 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/randoms'il y a une bonne raison) soit la $RANDOMvariable 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 $RANDOM15 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 $RANDOMva).
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 odet /dev/urandompour échantillonner un entier de n bits. odlira 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 nmaintenant échantillonner des chaînes de bits, mais nous voulons échantillonner des entiers dans une plage de 0à max, uniformément au hasard , où maxpeut ê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 nchaîne de bits -bit jusqu'à ce que nous échantillonnions une valeur qui est inférieure ou égal à max. Dans le pire des cas ( maxest une puissance de deux), chaque itération se termine avec une probabilité de 50%, et dans le meilleur des cas ( maxest 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 minet max, où minet maxpeut ê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 minet max, ou un seul argument max, par mindé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 minet max, nous échantillonnons un entier aléatoire entre 0et la valeur absolue de max-min, et ajoutons minau 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. :-)