Gracias a todos por todas sus excelentes respuestas. Terminé con la siguiente solución, que me gustaría compartir.
Antes de entrar en más detalles sobre por qué y cómo, aquí está el tl; dr : mi brillante nuevo 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 ))
Guarde eso ~/bin/rand
y tendrá a su disposición una dulce función aleatoria en bash que puede muestrear un número entero en un rango arbitrario dado. El rango puede contener enteros negativos y positivos y puede tener hasta 2 60 -1 de longitud:
$ 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
Todas las ideas de los otros que respondieron fueron geniales. Las respuestas de terdon , JF Sebastian y jimmij utilizaron herramientas externas para realizar la tarea de manera simple y eficiente. Sin embargo, preferí una verdadera solución bash para la máxima portabilidad, y tal vez un poco, simplemente por amor a bash;)
Las respuestas de Ramesh y l0b0 utilizadas /dev/urandom
o /dev/random
en combinación con od
. Eso es bueno, sin embargo, sus enfoques tenían la desventaja de que solo podían muestrear enteros aleatorios en el rango de 0 a 2 8n -1 para algunos n, ya que este método muestrea bytes, es decir, cadenas de bits de longitud 8. Estos son saltos bastante grandes con creciente n.
Finalmente, la respuesta de Falco describe la idea general de cómo esto podría hacerse para rangos arbitrarios (no solo potencias de dos). Básicamente, para un rango dado {0..max}
, podemos determinar cuál es la siguiente potencia de dos, es decir, exactamente cuántos bits se requieren para representar max
como una cadena de bits. Luego podemos muestrear tantos bits y ver si este bistring, como entero, es mayor que max
. Si es así, repita. Dado que muestreamos tantos bits como sea necesario para representar max
, cada iteración tiene una probabilidad mayor o igual al 50% de tener éxito (50% en el peor de los casos, 100% en el mejor de los casos). Entonces esto es muy eficiente.
Mi script es básicamente una implementación concreta de la respuesta de Falco, escrita en puro bash y altamente eficiente, ya que utiliza las operaciones bit a bit integradas de bash para muestrear cadenas de bits de la longitud deseada. También honra una idea de Eliah Kagan que sugiere utilizar la $RANDOM
variable incorporada al concatenar cadenas de bits resultantes de invocaciones repetidas de $RANDOM
. De hecho, implementé las posibilidades de uso /dev/urandom
y $RANDOM
. Por defecto, el script anterior usa $RANDOM
. (Y bueno, si usamos /dev/urandom
, necesitamos od y tr , pero estos están respaldados por POSIX).
¿Entonces, cómo funciona?
Antes de entrar en esto, dos observaciones:
Resulta que bash no puede manejar enteros mayores de 2 63 -1. Ver por ti mismo:
$ echo $((2**63-1))
9223372036854775807
$ echo $((2**63))
-9223372036854775808
Parece que bash usa internamente enteros de 64 bits con signo para almacenar enteros. Entonces, en 2 63 "se envuelve" y obtenemos un número entero negativo. Por lo tanto, no podemos esperar obtener un rango mayor que 2 63 -1 con cualquier función aleatoria que usemos. Bash simplemente no puede manejarlo.
Siempre que queramos muestrear un valor en un rango arbitrario entre min
y max
posiblemente min != 0
, simplemente podemos muestrear un valor entre 0
y en su max-min
lugar y luego agregarlo min
al resultado final. Esto funciona incluso si es min
posible que max
sea negativo , pero debemos tener cuidado de muestrear un valor entre 0
y el valor absoluto de max-min
. Entonces, podemos centrarnos en cómo muestrear un valor aleatorio entre 0
y un entero positivo arbitrario max
. El resto es fácil.
Paso 1: Determine cuántos bits se necesitan para representar un número entero (el logaritmo)
Entonces, para un valor dado max
, queremos saber cuántos bits se necesitan para representarlo como una cadena de bits. Esto es para que luego podamos muestrear aleatoriamente solo tantos bits como sean necesarios, lo que hace que el script sea tan eficiente.
Veamos. Como con n
bits, podemos representar hasta el valor 2 n -1, entonces el número n
de bits necesarios para representar un valor arbitrario x
es el techo (log 2 (x + 1)). Por lo tanto, necesitamos una función para calcular el techo de un logaritmo a la base 2. Es bastante explicativo:
log2() {
local x=$1 n=1 l=0
while (( x>n && n>0 ))
do
let n*=2 l++
done
echo $l
}
Necesitamos la condición, n>0
por lo que si crece demasiado, se envuelve y se vuelve negativa, se garantiza que el ciclo terminará.
Paso 2: muestrear una cadena de bits aleatoria de longitud n
Las ideas más portátiles son usar /dev/urandom
(o incluso /dev/random
si hay una razón sólida) o la $RANDOM
variable incorporada de bash . Veamos $RANDOM
primero cómo hacerlo .
Opción A: usar $RANDOM
Esto utiliza la idea mencionada por Eliah Kagan. Básicamente, dado que $RANDOM
muestrea un entero de 15 bits, podemos usarlo $((RANDOM<<15|RANDOM))
para muestrear un entero de 30 bits. Eso significa, desplazar una primera invocación de $RANDOM
15 bits hacia la izquierda, y aplicar una invocación a nivel de bit o con una segunda invocación de $RANDOM
, concatenando efectivamente dos cadenas de bits muestreadas independientemente (o al menos tan independientes como va el incorporado de bash $RANDOM
).
Podemos repetir esto para obtener un entero de 45 bits o 60 bits. Después de que bash ya no puede manejarlo, pero esto significa que podemos muestrear fácilmente un valor aleatorio entre 0 y 2 60 -1. Entonces, para muestrear un número entero de n bits, repetimos el procedimiento hasta que nuestra cadena de bits aleatoria, cuya longitud crece en pasos de 15 bits, tenga una longitud mayor o igual que n. Finalmente, cortamos los bits que son demasiado desplazándonos adecuadamente a la derecha, y terminamos con un entero aleatorio 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) ))
}
Opción B: uso /dev/urandom
Alternativamente, podemos usar od
y /dev/urandom
para muestrear un número entero de n bits. od
leerá bytes, es decir, cadenas de bits de longitud 8. Del mismo modo que en el método anterior, muestreamos tantos bytes que el número equivalente de bits muestreados es mayor o igual que n, y cortamos los bits que son demasiado.
El número más bajo de bytes necesarios para obtener al menos n bits es el múltiplo más bajo de 8 que es mayor o igual que n, es decir, piso ((n + 7) / 8).
Esto solo funciona con enteros de hasta 56 bits. El muestreo de un byte más nos daría un número entero de 64 bits, es decir, un valor de hasta 2 64 -1, que bash no puede manejar.
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) ))
}
Poner las piezas juntas: obtener enteros aleatorios en rangos arbitrarios
Nos puede muestrear n
bits bitstrings ahora, pero queremos enteros de la muestra en un rango de 0
a max
, de manera uniforme al azar , donde max
puede ser arbitrario, no necesariamente una potencia de dos. (No podemos usar el módulo ya que eso crea un sesgo).
El punto principal por el que tratamos de muestrear tantos bits como sea necesario para representar el valor max
, es que ahora podemos usar de forma segura (y eficiente) un bucle para muestrear repetidamente una n
cadena de bits de un bit hasta que muestreemos un valor que es más bajo o igual a max
. En el peor de los casos ( max
es una potencia de dos), cada iteración termina con una probabilidad del 50%, y en el mejor de los casos ( max
es una potencia de dos menos uno), la primera iteración termina con certeza.
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
}
Terminando las cosas
Finalmente, queremos muestrear enteros entre min
y max
, donde min
y max
pueden ser arbitrarios, incluso negativos. Como se mencionó anteriormente, esto ahora es trivial.
Pongámoslo todo en un script bash. Haga algunos análisis de argumentos ... Queremos dos argumentos min
y max
, o solo un argumento max
, donde los min
valores predeterminados sean 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
... y, finalmente, para muestrear uniformemente al azar un valor entre min
y max
, muestreamos un entero aleatorio entre 0
y el valor absoluto de max-min
, y lo sumamos min
al resultado final. :-)
diff=$((max-min)) && diff=${diff#-}
echo $(( $(rand $diff) + min ))
Inspirado por esto , podría intentar usar dieharder para probar y comparar este PRNG, y poner mis hallazgos aquí. :-)