Grazie a tutti per tutte le vostre grandi risposte. Ho finito con la seguente soluzione, che vorrei condividere.
Prima di entrare in ulteriori dettagli sui perché e come, ecco il tl; dr : il mio nuovo brillante script :-)
#!/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 ))
Salvalo in ~/bin/rand
e avrai a tua disposizione una dolce funzione casuale in bash che può campionare un numero intero in un dato intervallo arbitrario. L'intervallo può contenere numeri interi negativi e positivi e può avere una lunghezza massima di 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
Tutte le idee degli altri rispondenti erano fantastiche. Le risposte di terdon , JF Sebastian e jimmij hanno utilizzato strumenti esterni per svolgere il compito in modo semplice ed efficiente. Tuttavia, ho preferito una vera soluzione bash per la massima portabilità, e forse un po ', semplicemente per amore di bash;)
Le risposte di Ramesh e l0b0 utilizzate /dev/urandom
o /dev/random
in combinazione con od
. Va bene, tuttavia, i loro approcci hanno lo svantaggio di poter campionare interi casuali nell'intervallo da 0 a 2 8n -1 per alcuni n, poiché questo metodo campiona byte, ovvero stringhe di bit di lunghezza 8. Questi sono salti piuttosto grandi con crescente n.
Infine, la risposta di Falco descrive l'idea generale di come ciò possa essere fatto per intervalli arbitrari (non solo poteri di due). Fondamentalmente, per un dato intervallo {0..max}
, possiamo determinare quale sia la potenza successiva di due, cioè esattamente quanti bit sono richiesti per rappresentare max
come stringa di bit . Quindi possiamo campionare solo tanti bit e vedere se questo bistring, come numero intero, è maggiore di max
. In tal caso, ripetere. Poiché campioniamo tutti i bit necessari per rappresentare max
, ogni iterazione ha una probabilità maggiore o uguale al 50% del successo (50% nel peggiore dei casi, 100% nel migliore dei casi). Quindi questo è molto efficiente.
La mia sceneggiatura è fondamentalmente un'implementazione concreta della risposta di Falco, scritta in puro bash e altamente efficiente poiché utilizza le operazioni bit per bit incorporate di bash per campionare bittring della lunghezza desiderata. Onora inoltre un'idea di Eliah Kagan che suggerisce di usare la $RANDOM
variabile incorporata concatenando stringhe di bit risultanti da ripetute invocazioni di $RANDOM
. In realtà ho implementato entrambe le possibilità di utilizzo /dev/urandom
e $RANDOM
. Per impostazione predefinita, utilizza lo script sopra $RANDOM
. (E ok, se si utilizza /dev/urandom
abbiamo bisogno di od e tr , ma questi sono supportati da POSIX.)
Quindi, come funziona?
Prima di entrare in questo, due osservazioni:
Risulta che bash non può gestire numeri interi maggiori di 2 63 -1. Vedi tu stesso:
$ echo $((2**63-1))
9223372036854775807
$ echo $((2**63))
-9223372036854775808
Sembrerebbe che bash usi internamente numeri interi a 64 bit con segno per memorizzare numeri interi. Quindi, a 2 63 "si avvolge" e otteniamo un numero intero negativo. Quindi non possiamo sperare di ottenere un intervallo maggiore di 2 63 -1 con qualsiasi funzione casuale che utilizziamo. Bash semplicemente non può gestirlo.
Ogni volta che vogliamo campionare un valore in un intervallo arbitrario tra min
e max
con possibilmente min != 0
, possiamo semplicemente campionare un valore tra 0
e max-min
invece e quindi aggiungere min
al risultato finale. Funziona anche se min
e forse anche max
è negativo , ma dobbiamo fare attenzione a campionare un valore compreso tra 0
e il valore assoluto di max-min
. Quindi, possiamo concentrarci su come campionare un valore casuale tra 0
e un intero positivo arbitrario max
. Il resto è facile.
Passaggio 1: determinare quanti bit sono necessari per rappresentare un numero intero (il logaritmo)
Quindi, per un dato valore max
, vogliamo sapere solo quanti bit sono necessari per rappresentarlo come una stringa di bit. Questo è così che in seguito possiamo campionare casualmente solo tutti i bit necessari, il che rende lo script così efficiente.
Vediamo. Dato che con i n
bit, possiamo rappresentare fino al valore 2 n -1, quindi il numero n
di bit necessari per rappresentare un valore arbitrario x
è soffitto (log 2 (x + 1)). Quindi, abbiamo bisogno di una funzione per calcolare il massimale di un logaritmo alla base 2. È piuttosto autoesplicativo:
log2() {
local x=$1 n=1 l=0
while (( x>n && n>0 ))
do
let n*=2 l++
done
echo $l
}
Abbiamo bisogno della condizione, n>0
quindi se diventa troppo grande, si avvolge e diventa negativo, il loop è garantito per terminare.
Passaggio 2: campionare una stringa di bit casuale di lunghezza n
Le idee più portatili sono o usare /dev/urandom
(o anche /dev/random
se c'è una ragione forte) o la $RANDOM
variabile integrata di bash . Diamo un'occhiata a come farlo $RANDOM
prima.
Opzione A: utilizzo $RANDOM
Questo utilizza l' idea menzionata da Eliah Kagan. Fondamentalmente, poiché $RANDOM
campiona un numero intero a 15 bit, possiamo usare $((RANDOM<<15|RANDOM))
per campionare un numero intero a 30 bit. Ciò significa che sposta una prima invocazione di $RANDOM
15 bit a sinistra e applica una bit a bit o con una seconda invocazione di $RANDOM
, concatenando efficacemente due stringhe di bit campionate in modo indipendente (o almeno indipendenti come va incorporato in bash $RANDOM
).
Possiamo ripetere questo per ottenere un numero intero a 45 o 60 bit. Dopo che bash non può più gestirlo, ma ciò significa che possiamo facilmente campionare un valore casuale tra 0 e 2 60 -1. Quindi, per campionare un numero intero n-bit, ripetiamo la procedura fino a quando la nostra stringa di bit casuale, la cui lunghezza cresce con incrementi di 15 bit, ha una lunghezza maggiore o uguale a n. Infine, tagliamo i bit che sono troppo spostando opportunamente bit a bit verso destra e finiamo con un intero casuale n-bit.
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) ))
}
Opzione B: utilizzo /dev/urandom
In alternativa, possiamo usare od
e /dev/urandom
campionare un numero intero n-bit. od
leggerà byte, ovvero stringhe di bit di lunghezza 8. Analogamente al metodo precedente, campioniamo così tanti byte che il numero equivalente di bit campionati è maggiore o uguale a n e tagliamo i bit che sono troppo.
Il numero più basso di byte necessario per ottenere almeno n bit è il multiplo più basso di 8 che è maggiore o uguale a n, ovvero floor ((n + 7) / 8).
Funziona solo con numeri interi a 56 bit. Il campionamento di un altro byte ci darebbe un numero intero a 64 bit, ovvero un valore fino a 2 64 -1, che bash non può gestire.
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) ))
}
Mettere insieme i pezzi: ottenere interi casuali in intervalli arbitrari
Ora possiamo campionare n
bit-string a bit, ma vogliamo campionare numeri interi in un intervallo da 0
a max
, uniformemente a caso , dove max
può essere arbitrario, non necessariamente una potenza di due. (Non possiamo usare il modulo in quanto crea un pregiudizio.)
Il motivo per cui abbiamo provato così tanto a campionare tutti i bit necessari per rappresentare il valore max
, è che ora possiamo usare in modo sicuro (ed efficiente) un loop per campionare ripetutamente una n
stringa di bit a bit fino a quando campioniamo un valore più basso o uguale a max
. Nel peggiore dei casi ( max
è una potenza di due), ogni iterazione termina con una probabilità del 50% e, nel migliore dei casi ( max
è una potenza di due meno uno), la prima iterazione termina con certezza.
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
}
Avvolgere le cose
Infine, vogliamo campionare numeri interi tra min
e max
, dove min
e max
può essere arbitrario, anche negativo. Come accennato in precedenza, questo è ora banale.
Mettiamo tutto in uno script bash. Esegui un po 'di argomenti per analizzare le cose ... Vogliamo due argomenti min
e max
, o solo un argomento max
, min
per impostazione predefinita 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
... e, infine, per campionare uniformemente a caso un valore compreso tra min
e max
, campioniamo un numero intero casuale compreso tra 0
il valore assoluto di max-min
e aggiungiamo min
al risultato finale. :-)
diff=$((max-min)) && diff=${diff#-}
echo $(( $(rand $diff) + min ))
Ispirato da questo , potrei provare a usare dieharder per testare e confrontare questo PRNG, e mettere qui le mie scoperte. :-)