Дякую всім за всі ваші чудові відповіді. Я закінчив таке рішення, яким я хотів би поділитися.
Перш ніж розібратися в деталях про те, про що і як, ось ось 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;)
Відповіді Рамеша та l0b0 використовуються /dev/urandom
або /dev/random
в поєднанні з od
. Це добре, однак, їхні підходи мали недолік лише в тому, що вони могли вибирати випадкові цілі числа в діапазоні від 0 до 2 8n -1 для деякого n, оскільки цей метод вибірки байтів, тобто біт-рядки довжиною 8. Це досить великі стрибки з збільшуючи n.
Нарешті, відповідь Фалько описує загальну думку про те, як це можна зробити для довільних діапазонів (не тільки потужностей двох). В основному, для заданого діапазону {0..max}
ми можемо визначити, яка наступна потужність двох, тобто, скільки саме бітів потрібно представити max
як бітструмент. Тоді ми можемо відібрати лише стільки бітів і побачити, чи більша ця бістринга як ціле число max
. Якщо так, повторіть. Оскільки ми відбираємо стільки бітів, скільки потрібно для представлення max
, кожна ітерація має ймовірність, що більша або дорівнює 50% успіху (50% у гіршому випадку, 100% у кращому випадку). Тож це дуже ефективно.
Мій сценарій - це в основному конкретна реалізація відповіді Фалько, написаного в чистому стилі та високоефективний, оскільки він використовує вбудовані побітові операції 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 -1 із будь-якою випадковою функцією. Bash просто не впорається з цим.
Щоразу, коли ми хочемо відібрати значення в довільному діапазоні між 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
спочатку.
Варіант A: Використання $RANDOM
Для цього використовується ідея, згадана Елією Каган. В основному, оскільки $RANDOM
вибірки є 15-бітним цілим числом, ми можемо використовувати $((RANDOM<<15|RANDOM))
для вибірки 30-бітного цілого числа. Це означає, що змініть перший виклик $RANDOM
на 15 біт ліворуч і застосуйте побіжно або з другим викликом $RANDOM
, ефективно змикаючи два незалежно відібраних бітстринга (або, принаймні, настільки ж незалежні, як йде вбудована версія bash $RANDOM
).
Ми можемо повторити це, щоб отримати 45-бітне або 60-бітове ціле число. Після цього баш вже не може впоратися з цим, але це означає, що ми можемо легко відібрати випадкове значення між 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 біт, - найменше кратне значення 8, яке більше або рівне n, тобто підлогу ((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
можуть бути довільними, навіть від’ємними. Як було сказано раніше, це зараз банально.
Давайте покладемо все це на баш сценарій. Зробіть деякі матеріали для аналізу аргументів ... Ми хочемо два аргументи 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, і розміщую свої висновки тут. :-)