Спасибо всем за все ваши отличные ответы. Я получил следующее решение, которым я хотел бы поделиться.
Прежде чем углубляться в подробности о том, почему и как, вот tl; dr : мой новый блестящий сценарий :-)
#!/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 ))
Сохраните это, ~/bin/rand
и в вашем распоряжении будет приятная случайная функция в bash, которая может выбирать целое число в заданном произвольном диапазоне. Диапазон может содержать отрицательные и положительные целые числа и может быть длиной до 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
Все идеи других ответчиков были великолепны. В ответах Тердона , Дж. Ф. Себастьяна и Джиммия использовались внешние инструменты для простого и эффективного выполнения задачи. Тем не менее, я предпочел настоящее решение bash для максимальной переносимости, и, возможно, немного, просто из любви к bash;)
Ответы Рамеша и l0b0 используются /dev/urandom
или /dev/random
в сочетании с od
. Это хорошо, однако, их подходы имели тот недостаток, что они могли выбирать случайные целые числа в диапазоне от 0 до 2 8n -1 для некоторого n, поскольку этот метод выбирает байты, то есть цепочки битов длины 8. Это довольно большие переходы с увеличивая п.
Наконец, ответ Фалько описывает общую идею, как это можно сделать для произвольных диапазонов (не только степеней двойки). По сути, для данного диапазона {0..max}
мы можем определить, какова следующая степень двух, т. Е. Сколько именно битов требуется представить max
в виде цепочки битов. Затем мы можем сэмплировать столько битов и посмотреть, больше ли это число в виде целого числа max
. Если так, повторите. Поскольку мы выбираем столько битов, сколько требуется для представления max
, каждая итерация имеет вероятность, превышающую или равную 50% успеха (50% в худшем случае, 100% в лучшем случае). Так что это очень эффективно.
Мой сценарий, в основном, представляет собой конкретную реализацию ответа Falco, написанную на чистом bash и очень эффективную, поскольку он использует встроенные побитовые операции bash для выборки цепочек битов желаемой длины. Кроме того, она поддерживает идею Элии Кагана, которая предлагает использовать встроенную $RANDOM
переменную путем объединения цепочек битов, возникающих в результате повторных вызовов $RANDOM
. Я фактически реализовал обе возможности /dev/urandom
и $RANDOM
. По умолчанию вышеуказанный скрипт использует $RANDOM
. (И хорошо, если /dev/urandom
мы используем od и tr , но они поддерживаются POSIX.)
Итак, как это работает?
Прежде чем я углублюсь в это, два замечания:
Оказывается, bash не может обрабатывать целые числа больше 2 63 -1. Посмотреть на себя:
$ echo $((2**63-1))
9223372036854775807
$ echo $((2**63))
-9223372036854775808
Казалось бы, bash внутренне использует 64-битные целые числа со знаком для хранения целых чисел. Таким образом, в 2 63 оно «оборачивается», и мы получаем отрицательное целое число. Поэтому мы не можем надеяться получить какой-либо диапазон больше, чем 2 63 с какой-либо случайной функцией, которую мы используем Баш просто не может с этим справиться.
Всякий раз, когда мы хотим выбрать значение в произвольном диапазоне между min
и max
с возможно min != 0
, мы можем просто выбрать значение между 0
и max-min
вместо, а затем добавить min
к конечному результату. Это работает , даже если min
и возможно , также max
являются отрицательными , но мы должны быть осторожны , чтобы попробовать значение между 0
и абсолютное значение max-min
. Итак, мы можем сосредоточиться на том, как выбрать случайное значение между 0
произвольным положительным целым числом max
. Остальное легко.
Шаг 1: Определите, сколько битов необходимо для представления целого числа (логарифм)
Таким образом, для данного значения max
мы хотим знать, сколько бит необходимо, чтобы представить его как цепочку битов. Это сделано для того, чтобы в дальнейшем мы могли произвольно выбирать столько битов, сколько необходимо, что делает скрипт таким эффективным.
Посмотрим. Поскольку с n
битами мы можем представить до значения 2 n -1, то число n
битов, необходимое для представления произвольного значения, x
является максимальным (log 2 (x + 1)). Итак, нам нужна функция для вычисления потолка логарифма к основанию 2. Это довольно очевидно:
log2() {
local x=$1 n=1 l=0
while (( x>n && n>0 ))
do
let n*=2 l++
done
echo $l
}
Нам нужно условие, n>0
поэтому, если оно слишком велико, оборачивается и становится отрицательным, цикл гарантированно завершится.
Шаг 2: выборка случайной строки длины n
Наиболее переносимые идеи - использовать /dev/urandom
(или даже /dev/random
если есть веские причины) встроенную $RANDOM
переменную bash . Давайте $RANDOM
сначала посмотрим, как это сделать .
Вариант А: Использование $RANDOM
Здесь используется идея, упомянутая Элией Каганом. По сути, поскольку выборка $RANDOM
15-разрядного целого числа, мы можем использовать $((RANDOM<<15|RANDOM))
для выборки 30-разрядного целого числа. Это означает, что сдвиньте первый вызов $RANDOM
на 15 бит влево и примените побитовый или со вторым вызовом $RANDOM
, эффективно объединяя две независимо выбранные цепочки битов (или, по крайней мере, столь же независимые, как встроенная команда bash $RANDOM
).
Мы можем повторить это, чтобы получить 45-битное или 60-битное целое число. После этого bash больше не может это обрабатывать, но это означает, что мы можем легко выбрать случайное значение между 0 и 2 60 -1. Итак, для выборки n-битного целого числа мы повторяем процедуру до тех пор, пока наша случайная цепочка битов, длина которой увеличивается с шагом 15 бит, не будет иметь длину, большую или равную n. Наконец, мы обрезаем слишком много битов путем соответствующего побитового сдвига вправо, и в итоге получаем n-битное случайное целое число.
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) ))
}
Вариант Б: Использование /dev/urandom
В качестве альтернативы мы можем использовать od
и /dev/urandom
для выборки n-битного целого числа. od
будет считывать байты, т. е. битовые строки длиной 8. Как и в предыдущем методе, мы выбираем столько байтов, что эквивалентное количество дискретных битов больше или равно n, и обрезаем слишком большие биты.
Наименьшее количество байтов, необходимое для получения по меньшей мере n битов, является наименьшим кратным 8, которое больше или равно n, т. Е. Floor ((n + 7) / 8).
Это работает только до 56-битных целых чисел. Выборка еще одного байта дала бы нам 64-битное целое число, то есть значение до 2 64 с -1, которое bash не может обработать.
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) ))
}
Соединение частей: получить случайные целые числа в произвольных диапазонах
Теперь мы можем сэмплировать n
битовые строки -бит, но мы хотим сэмплировать целые числа в диапазоне от 0
до max
, равномерно случайным образом , где max
может быть произвольным, а не обязательно степенью двойки. (Мы не можем использовать по модулю, поскольку это создает уклон.)
Весь смысл, почему мы так старались собрать столько битов, сколько необходимо для представления значения max
, заключается в том, что теперь мы можем безопасно (и эффективно) использовать цикл для многократной выборки n
-битной цепочки битов, пока мы не выберем значение, которое меньше или равно max
. В худшем случае ( max
это степень двойки) каждая итерация заканчивается с вероятностью 50%, а в лучшем случае ( max
это степень два минус один) первая итерация завершается с уверенностью.
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
}
Завершение вещей
Наконец, мы хотим выбрать целые числа между min
и max
, где min
и max
может быть произвольным, даже отрицательным. Как упоминалось ранее, это теперь тривиально.
Давайте поместим все это в сценарий bash. Делаем какие-то аргументы при разборе ... Нам нужны два аргумента min
и max
, или только один аргумент max
, где по min
умолчанию 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
... и, наконец, для выборки случайным образом значения между min
и max
мы выбираем случайное целое число между 0
и абсолютным значением max-min
и добавляем min
к конечному результату. :-)
diff=$((max-min)) && diff=${diff#-}
echo $(( $(rand $diff) + min ))
Вдохновленный этим , я мог бы попытаться использовать dieharder для тестирования и тестирования этого PRNG и выложить свои результаты здесь. :-)