Спасибо всем за все ваши отличные ответы. Я получил следующее решение, которым я хотел бы поделиться.
Прежде чем углубляться в подробности о том, почему и как, вот 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
Здесь используется идея, упомянутая Элией Каганом. По сути, поскольку выборка $RANDOM15-разрядного целого числа, мы можем использовать $((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 и выложить свои результаты здесь. :-)