Obrigado a todos pelas ótimas respostas. Acabei com a seguinte solução, que gostaria de compartilhar.
Antes de entrar em mais detalhes sobre os porquês e comos, aqui está o tl; dr : meu brilhante novo 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 ))
Salve isso ~/bin/rand
e você terá à sua disposição uma função aleatória doce no bash que pode amostrar um número inteiro em um determinado intervalo arbitrário. O intervalo pode conter números inteiros negativos e positivos e pode ter até 2 60 -1 de comprimento:
$ 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 as idéias dos outros respondentes foram ótimas. As respostas de terdon , JF Sebastian e jimmij usaram ferramentas externas para executar a tarefa de maneira simples e eficiente. No entanto, eu preferi uma solução verdadeira do bash para máxima portabilidade e, talvez um pouco, simplesmente por amor ao bash;)
As respostas de Ramesh e l0b0 usadas /dev/urandom
ou /dev/random
em combinação com od
. Isso é bom, no entanto, suas abordagens tiveram a desvantagem de poder apenas amostrar números inteiros aleatórios no intervalo de 0 a 2 8n -1 para alguns n, já que esse método faz uma amostra de bytes, ou seja, bitstrings de comprimento 8. Esses são saltos bastante grandes com crescente n.
Finalmente, a resposta de Falco descreve a idéia geral de como isso pode ser feito para intervalos arbitrários (não apenas para poderes de dois). Basicamente, para um determinado intervalo {0..max}
, podemos determinar qual é a próxima potência de dois, ou seja, exatamente quantos bits são necessários para representar max
como uma cadeia de bits . Em seguida, podemos amostrar apenas esses bits e ver se esse bistring, como um inteiro, é maior que max
. Se sim, repita. Como amostramos o número de bits necessário para representar max
, cada iteração tem uma probabilidade maior ou igual a 50% de êxito (50% no pior caso, 100% no melhor caso). Então, isso é muito eficiente.
Meu script é basicamente uma implementação concreta da resposta de Falco, escrita em bash puro e altamente eficiente, pois usa as operações bit a bit internas do bash para obter amostras de strings de bit do comprimento desejado. Além disso, ele homenageia uma idéia de Eliah Kagan que sugere o uso da $RANDOM
variável incorporada concatenando as cadeias de bits resultantes de repetidas invocações de $RANDOM
. Na verdade, eu implementei as possibilidades de usar /dev/urandom
e $RANDOM
. Por padrão, o script acima usa $RANDOM
. (E ok, se estiver usando /dev/urandom
, precisamos de od e tr , mas estes são suportados pelo POSIX.)
Então, como isso funciona?
Antes de entrar nisso, duas observações:
Acontece que o bash não pode manipular números inteiros maiores que 2 63 -1. Veja por si mesmo:
$ echo $((2**63-1))
9223372036854775807
$ echo $((2**63))
-9223372036854775808
Parece que o bash usa internamente números inteiros assinados de 64 bits para armazenar números inteiros. Então, em 2 63, ele "se envolve" e obtemos um número inteiro negativo. Portanto, não podemos esperar obter um intervalo maior que 2 63 -1 com qualquer função aleatória que usamos. O Bash simplesmente não consegue lidar com isso.
Sempre que quisermos amostrar um valor em um intervalo arbitrário entre min
e max
com min != 0
, possivelmente , podemos simplesmente amostrar um valor entre 0
e ao max-min
invés disso e depois adicionar min
ao resultado final. Isso funciona mesmo que min
e possivelmente também max
seja negativo , mas precisamos ter cuidado para amostrar um valor entre 0
e o valor absoluto de max-min
. Portanto, podemos nos concentrar em como amostrar um valor aleatório entre 0
e um número inteiro positivo arbitrário max
. O resto é fácil.
Etapa 1: determinar quantos bits são necessários para representar um número inteiro (o logaritmo)
Portanto, para um determinado valor max
, queremos saber quantos bits são necessários para representá-lo como uma cadeia de bits. Isso é para que mais tarde possamos amostrar aleatoriamente apenas quantos bits forem necessários, o que torna o script tão eficiente.
Vamos ver. Como com n
bits, podemos representar até o valor 2 n -1, então o número n
de bits necessários para representar um valor arbitrário x
é teto (log 2 (x + 1)). Portanto, precisamos de uma função para calcular o teto de um logaritmo para a base 2. É bastante auto-explicativo:
log2() {
local x=$1 n=1 l=0
while (( x>n && n>0 ))
do
let n*=2 l++
done
echo $l
}
Precisamos da condição n>0
para que, se ela crescer muito, contornar e se tornar negativa, o loop seja garantido para terminar.
Etapa 2: experimente um bitstring aleatório de comprimento n
As idéias mais portáteis são usar /dev/urandom
(ou mesmo /dev/random
se houver um motivo forte) ou a $RANDOM
variável interna do bash . Vamos ver como fazer isso $RANDOM
primeiro.
Opção A: Usando $RANDOM
Isso usa a idéia mencionada por Eliah Kagan. Basicamente, uma vez que $RANDOM
cria um número inteiro de 15 bits, podemos usar $((RANDOM<<15|RANDOM))
para amostrar um número inteiro de 30 bits. Isso significa que, desloque uma primeira invocação de $RANDOM
15 bits para a esquerda e aplique uma invocação bit a bit ou com uma segunda invocação $RANDOM
, concaturando efetivamente duas seqüências de bits com amostragem independente (ou pelo menos tão independente quanto o built-in do bash $RANDOM
).
Podemos repetir isso para obter um número inteiro de 45 ou 60 bits. Depois que o bash não aguenta mais, mas isso significa que podemos facilmente amostrar um valor aleatório entre 0 e 2 60 -1. Portanto, para amostrar um número inteiro de n bits, repetimos o procedimento até que nossa cadeia de bits aleatória, cujo comprimento cresça em etapas de 15 bits, tenha um comprimento maior ou igual a n. Por fim, cortamos os bits que são demais, deslocando-se bit a bit apropriadamente para a direita e terminamos com um número inteiro aleatório 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) ))
}
Opção B: Usando /dev/urandom
Como alternativa, podemos usar od
e /dev/urandom
amostrar um número inteiro de n bits. od
lerá bytes, isto é, cadeias de bits de comprimento 8. Da mesma forma que no método anterior, coletamos tantos bytes que o número equivalente de bits amostrados é maior ou igual a n e eliminamos os bits que são demais.
O menor número de bytes necessários para obter pelo menos n bits é o múltiplo mais baixo de 8 que é maior ou igual a n, ou seja, floor ((n + 7) / 8).
Isso funciona apenas com números inteiros de 56 bits. A amostragem de mais um byte nos daria um número inteiro de 64 bits, ou seja, um valor de até 2 64 -1, que o bash não pode manipular.
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) ))
}
Juntando as peças: obtenha números aleatórios em intervalos arbitrários
Podemos provar n
bits bitstrings agora, mas queremos inteiros de amostra em um intervalo de 0
para max
, uniformemente ao acaso , onde max
pode ser arbitrária, não necessariamente uma potência de dois. (Não podemos usar o módulo, pois isso cria um viés.)
O ponto principal por que tentamos tanto amostrar quantos bits são necessários para representar o valor max
é que agora podemos usar com segurança (e eficientemente) um loop para amostrar repetidamente uma n
cadeia de bits de bits até obtermos um valor menor ou igual a max
. No pior caso ( max
é um poder de dois), cada iteração termina com uma probabilidade de 50% e, no melhor dos casos ( max
é um poder de dois menos um), a primeira iteração termina com 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
}
Embrulhando as coisas
Finalmente, queremos amostrar números inteiros entre min
e max
, onde min
e max
podem ser arbitrários e até negativos. Como mencionado anteriormente, isso agora é trivial.
Vamos colocar tudo em um script bash. Faça algum argumento para analisar coisas ... Queremos dois argumentos min
e max
, ou apenas um argumento max
, onde o min
padrão é 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, finalmente, para amostrar uniformemente aleatoriamente um valor entre min
e max
, amostramos um número inteiro aleatório entre 0
e o valor absoluto de max-min
e adicionamos min
ao resultado final. :-)
diff=$((max-min)) && diff=${diff#-}
echo $(( $(rand $diff) + min ))
Inspirado por isso , posso tentar usar o dieharder para testar e comparar esse PRNG e colocar minhas conclusões aqui. :-)