9.6. $RANDOM: генерация псевдослучайных целых чисел

$RANDOM -- внутренняя функция Bash (не константа), которая возвращает псевдослучайные целые числа в диапазоне 0 - 32767. Функция $RANDOM не должна использоваться для генераци ключей шифрования.

Пример 9-23. Генерация случайных чисел

#!/bin/bash

# $RANDOM возвращает различные случайные числа при каждом обращении к ней.
# Диапазон изменения: 0 - 32767 (16-битовое целое со знаком).

MAXCOUNT=10
count=1

echo
echo "$MAXCOUNT случайных чисел:"
echo "-----------------"
while [ "$count" -le $MAXCOUNT ]      # Генерация 10 ($MAXCOUNT) случайных чисел.
do
  number=$RANDOM
  echo $number
  let "count += 1"  # Нарастить счетчик.
done
echo "-----------------"

# Если вам нужны случайные числа не превышающие определенного числа,
# воспользуйтесь оператором деления по модулю (остаток от деления).

RANGE=500

echo

number=$RANDOM
let "number %= $RANGE"
echo "Случайное число меньше $RANGE  ---  $number"

echo

# Если вы желаете ограничить диапазон "снизу",
# то просто производите генерацию псевдослучайных чисел в цикле до тех пор,
# пока не получите число большее нижней границы.

FLOOR=200

number=0   # инициализация
while [ "$number" -le $FLOOR ]
do
  number=$RANDOM
done
echo "Случайное число, большее $FLOOR ---  $number"
echo


# Эти два способа могут быть скомбинированы.
number=0   #initialize
while [ "$number" -le $FLOOR ]
do
  number=$RANDOM
  let "number %= $RANGE"  # Ограничение "сверху" числом $RANGE.
done
echo "Случайное число в диапазоне от $FLOOR до $RANGE ---  $number"
echo


# Генерация случайных "true" и "false" значений.
BINARY=2
number=$RANDOM
T=1

let "number %= $BINARY"
# let "number >>= 14"    дает более равномерное распределение
# (сдвиг вправо смещает старший бит на нулевую позицию, остальные биты обнуляются).
if [ "$number" -eq $T ]
then
  echo "TRUE"
else
  echo "FALSE"
fi

echo


# Можно имитировать бросание 2-х игровых кубиков.
SPOTS=7   # остаток от деления на 7 дает диапазон 0 - 6.
ZERO=0
die1=0
die2=0

# Кубики "выбрасываются" раздельно.

  while [ "$die1" -eq $ZERO ]     # Пока на "кубике" ноль.
  do
    let "die1 = $RANDOM % $SPOTS" # Имитировать бросок первого кубика.
  done

  while [ "$die2" -eq $ZERO ]
  do
    let "die2 = $RANDOM % $SPOTS" # Имитировать бросок второго кубика.
  done

let "throw = $die1 + $die2"
echo "Результат броска кубиков = $throw"
echo


exit 0

Пример 9-24. Выбор случайной карты из колоды

#!/bin/bash
# pick-card.sh

# Пример выбора случайного элемента массива.


# Выбор случайной карты из колоды.

Suites="Треф
Бубей
Червей
Пик"

Denominations="2
3
4
5
6
7
8
9
10
Валет
Дама
Король
Туз"

suite=($Suites)                # Инициализация массивов.
denomination=($Denominations)

num_suites=${#suite[*]}        # Количество элементов массивов.
num_denominations=${#denomination[*]}

echo -n "${denomination[$((RANDOM%num_denominations))]} "
echo ${suite[$((RANDOM%num_suites))]}


# $bozo sh pick-cards.sh
# Валет Треф


# Спасибо "jipe," за пояснения по работе с $RANDOM.
exit 0
Note

Jipe подсказал еще один способ генерации случайных чисел из заданного диапазона.

#  Генерация случайных чисел в диапазоне 6 - 30.
rnumber=$((RANDOM%25+6))

#  Генерируется случайное число из диапазона 6 - 30,
#+ но при этом число должно делиться на 3 без остатка.
rnumber=$(((RANDOM%30/3+1)*3))

# Примечательно, если $RANDOM возвращает 0
# то это приводит к возникновению ошибки.

#  Упражнение: Попробуйте разобраться с выражением самостоятельно.


Bill Gradwohl предложил усовершенствованную формулу генерации положительных псевдослучайных чисел в заданном диапазоне.

rnumber=$(((RANDOM%(max-min+divisibleBy))/divisibleBy*divisibleBy+min))
  


В сценарии ниже, Bill представил универсальную функцию, которая возвращает псевдослучайное число из заданного диапазона.

Пример 9-25. Псевдослучайное число из заданного диапазона

#!/bin/bash
# random-between.sh
# Псевдослучайное число из заданного диапазона.
# Автор: Bill Gradwohl,
# незначительные изменения внесены автором документа.
# Используется с разрешения автора сценария.


randomBetween() {
   #  Генерация положительных и отрицательных псевдослучайных чисел,
   #+ в диапазоне от $min до $max,
   #+ которые кратны числу $divisibleBy.
   #
   #  Bill Gradwohl - Oct 1, 2003

   syntax() {
   # Вложенная функция.
      echo
      echo    "Порядок вызова: randomBetween [min] [max] [multiple]"
      echo
      echo    "Функция ожидает до 3-х входных аргументов, но они не являются обязательными."
      echo    "min -- нижняя граница диапазона"
      echo    "max -- верхняя граница диапазона"
      echo    "multiple -- делитель, на который должен делиться результат без остатка."
      echo
      echo    "Если какой либо из параметров отсутствует, по-умолчанию принимаются значения: 0 32767 1"
      echo    "В случае успеха функция возвращает 0, иначе -- 1"
      echo    "и это сообщение о порядке вызова."
      echo    "Результат возвращается в глобальной переменной randomBetweenAnswer"
      echo    "Входные параметры, имеющие отрицательные значения, обрабатываются корректно."
   }

   local min=${1:-0}
   local max=${2:-32767}
   local divisibleBy=${3:-1}
   # При отсутствии какого либо из входных параметров, они принимают значения по-умолчанию.

   local x
   local spread

   # Делитель должен быть положительным числом.
   [ ${divisibleBy} -lt 0 ] && divisibleBy=$((0-divisibleBy))

   # Проверка корректности входных параметров.
   if [ $# -gt 3 -o ${divisibleBy} -eq 0 -o  ${min} -eq ${max} ]; then
      syntax
      return 1
   fi

   # Если min больше чем max, то поменять их местами.
   if [ ${min} -gt ${max} ]; then
      # Поменять местами.
      x=${min}
      min=${max}
      max=${x}
   fi

   #  Если min не делится без остатка на $divisibleBy,
   #+ то привести его к ближайшему подходящему числу из заданного диапазона.
   if [ $((min/divisibleBy*divisibleBy)) -ne ${min} ]; then
      if [ ${min} -lt 0 ]; then
         min=$((min/divisibleBy*divisibleBy))
      else
         min=$((((min/divisibleBy)+1)*divisibleBy))
      fi
   fi

   #  Если max не делится без остатка на $divisibleBy,
   #+ то привести его к ближайшему подходящему числу из заданного диапазона.
   if [ $((max/divisibleBy*divisibleBy)) -ne ${max} ]; then
      if [ ${max} -lt 0 ]; then
         max=$((((max/divisibleBy)-1)*divisibleBy))
      else
         max=$((max/divisibleBy*divisibleBy))
      fi
   fi

   #  ---------------------------------------------------------------------
   #  А теперь собственно нахождение псевдослучайного числа.

   #  Обратите внимание: чтобы получить псевдослучайное число в конце диапазона
   #+ необходимо рассматривать диапазон от 0 до
   #+ abs(max-min)+divisibleBy, а не abs(max-min)+1.

   #  Этим превышением верхней границы диапазона можно пренебречь
   #+ поскольку эта новая граница никогда не будет достигнута.

   #  Если использовать при вычислении формулу abs(max-min)+1,
   #+ то будут получаться вполне корректные значения, но при этом,
   #+ возвращаемые значения будут значительно ниже
   #+ верхней границы диапазона.
   #  ---------------------------------------------------------------------

   spread=$((max-min))
   [ ${spread} -lt 0 ] && spread=$((0-spread))
   let spread+=divisibleBy
   randomBetweenAnswer=$(((RANDOM%spread)/divisibleBy*divisibleBy+min))

   return 0
}

# Проверка функции.
min=-14
max=20
divisibleBy=3


#  Создадим массив, который будет содержать счетчики встречаемости каждого из чисел
#+ в заданном диапазоне.

declare -a answer
minimum=${min}
maximum=${max}
   if [ $((minimum/divisibleBy*divisibleBy)) -ne ${minimum} ]; then
      if [ ${minimum} -lt 0 ]; then
         minimum=$((minimum/divisibleBy*divisibleBy))
      else
         minimum=$((((minimum/divisibleBy)+1)*divisibleBy))
      fi
   fi


   #  Если max не делится без остатка на $divisibleBy,
   #+ то привести его к ближайшему подходящему числу из заданного диапазона.

   if [ $((maximum/divisibleBy*divisibleBy)) -ne ${maximum} ]; then
      if [ ${maximum} -lt 0 ]; then
         maximum=$((((maximum/divisibleBy)-1)*divisibleBy))
      else
         maximum=$((maximum/divisibleBy*divisibleBy))
      fi
   fi


#  Необходимое условие при работе с массивами --
#+ индекс массива должен быть положительным числом,
#+ поэтому введена дополнительная переменная displacement, которая
#+ гарантирует положительность индексов.

displacement=$((0-minimum))
for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
   answer[i+displacement]=0
done


# Цикл с большим количеством итераций, чтобы посмотреть -- что мы получаем.
loopIt=1000   #  Автор сценария предложил 100000 итераций,
              #+ но в этом случае цикл работает чересчур долго.

for ((i=0; i<${loopIt}; ++i)); do

   #  Обратите внимание: числа min и max передаются функции в обратном порядке,
   #+ чтобы продемонстрировать, что функция обрабатывает их корректно.

   randomBetween ${max} ${min} ${divisibleBy}

   # Вывод сообщения об ошибке, если функция вернула некорректное значение.
   [ ${randomBetweenAnswer} -lt ${min} -o ${randomBetweenAnswer} -gt ${max} ] && echo Выход за границы диапазона MIN .. MAX  - ${randomBetweenAnswer}!
   [ $((randomBetweenAnswer%${divisibleBy})) -ne 0 ] && echo Число не делится на заданный делитель без остатка - ${randomBetweenAnswer}!

   # Записать полученное число в массив.
   answer[randomBetweenAnswer+displacement]=$((answer[randomBetweenAnswer+displacement]+1))
done



# Проверим полученные результаты

for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
   [ ${answer[i+displacement]} -eq 0 ] && echo "Число $i не было получено ни разу." || echo "Число ${i} встречено ${answer[i+displacement]} раз."
done


exit 0

Насколько случайны числа, возвращаемые функцией $RANDOM? Лучший способ оценить "случайность" генерируемых чисел -- это написать сценарий, который будет имитировать бросание игрального кубика достаточно большое число раз, а затем выведет количество выпадений каждой из граней...

Пример 9-26. Имитация бросания кубика с помощью RANDOM

#!/bin/bash
# Случайные ли числа возвращает RANDOM?

RANDOM=$$       # Инициализация генератора случайных чисел числом PID процесса-сценария.

PIPS=6          # Кубик имеет 6 граней.
MAXTHROWS=600   # Можете увеличить, если не знаете куда девать свое время.
throw=0         # Счетчик бросков.

zeroes=0        # Обнулить счетчики выпадения отдельных граней.
ones=0          # т.к. неинициализированные переменные - "пустые", и не равны нулю!.
twos=0
threes=0
fours=0
fives=0
sixes=0

print_result ()
{
echo
echo "единиц   =   $ones"
echo "двоек    =   $twos"
echo "троек    =   $threes"
echo "четверок =   $fours"
echo "пятерок  =   $fives"
echo "шестерок =   $sixes"
echo
}

update_count()
{
case "$1" in
  0) let "ones += 1";;   # 0 соответствует грани "1".
  1) let "twos += 1";;   # 1 соответствует грани "2", и так далее
  2) let "threes += 1";;
  3) let "fours += 1";;
  4) let "fives += 1";;
  5) let "sixes += 1";;
esac
}

echo


while [ "$throw" -lt "$MAXTHROWS" ]
do
  let "die1 = RANDOM % $PIPS"
  update_count $die1
  let "throw += 1"
done

print_result

# Количество выпадений каждой из граней должно быть примерно одинаковым, если считать RANDOM достаточно случайным.
# Для $MAXTHROWS = 600, каждая грань должна выпасть примерно 100 раз (плюс-минус 20).
#
# Имейте ввиду, что RANDOM - это генератор ПСЕВДОСЛУЧАЙНЫХ чисел,

# Упражнение:
# ---------------
# Перепишите этот сценарий так, чтобы он имитировал 1000 бросков монеты.
# На каждом броске возможен один из двух вариантов выпадения - "ОРЕЛ" или "РЕШКА".

exit 0

Как видно из последнего примера, неплохо было бы производить переустановку начального числа генератора случайных чисел RANDOM перед тем, как начать работу с ним. Если используется одно и то же начальное число, то генератор RANDOM будет выдавать одну и ту же последовательность чисел. (Это совпадает с поведением функции random() в языке C.)

Пример 9-27. Переустановка RANDOM

#!/bin/bash
# seeding-random.sh: Переустановка переменной RANDOM.

MAXCOUNT=25       # Длина генерируемой последовательности чисел.

random_numbers ()
{
count=0
while [ "$count" -lt "$MAXCOUNT" ]
do
  number=$RANDOM
  echo -n "$number "
  let "count += 1"
done
}

echo; echo

RANDOM=1          # Переустановка начального числа генератора случайных чисел RANDOM.
random_numbers

echo; echo

RANDOM=1          # То же самое начальное число...
random_numbers    # ...в результате получается та же последовательность чисел.
                  #
                  # В каких случаях может оказаться полезной генерация совпадающих серий?

echo; echo

RANDOM=2          # Еще одна попытка, но с другим начальным числом...
random_numbers    # получим другую последовательность.

echo; echo

# RANDOM=$$  в качестве начального числа выбирается PID процесса-сценария.
# Вполне допустимо взять в качестве начального числа результат работы команд 'time' или 'date'.

# Немного воображения...
SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
#  Псевдослучайное число забирается
#+ из системного генератора псевдослучайных чисел /dev/urandom ,
#+ затем конвертируется в восьмеричное число командой "od",
#+ и наконец "awk" возвращает единственное число для переменной SEED.
RANDOM=$SEED
random_numbers

echo; echo

exit 0

Note

Системный генератор /dev/urandom дает последовательность псевдослучайных чисел с более равномерным распределением, чем $RANDOM. Команда dd if=/dev/urandom of=targetfile bs=1 count=XX создает файл, содержащий последовательность псевдослучайных чисел. Однако, эти числа требуют дополнительной обработки, например с помощью команды od (этот прием используется в примере выше) или dd (см. Пример 12-45).

Есть и другие способы генерации псевдослучайных последовательностей в сценариях. Awk имеет для этого достаточно удобные средства.

Пример 9-28. Получение псевдослучайных чисел с помощью awk

#!/bin/bash
# random2.sh: Генерация псевдослучайных чисел в диапазоне 0 - 1.
# Используется функция rand() из awk.

AWKSCRIPT=' { srand(); print rand() } '
# Команды/параметры, передаваемые awk
# Обратите внимание, функция srand() переустанавливает начальное число генератора случайных чисел.

echo -n "Случайное число в диапазоне от 0 до 1 = "
echo | awk "$AWKSCRIPT"

exit 0


# Упражнения:
# ---------

# 1) С помощью оператора цикла выведите 10 различных случайных чисел.
#      (Подсказка: вам потребуется вызвать функцию "srand()"
#      в каждом цикле с разными начальными числами.
#      Что произойдет, если этого не сделать?)

# 2) Заставьте сценарий генерировать случайные числа в диапазоне 10 - 100
#      используя целочисленный множитель, как коэффициент масштабирования

# 3) То же самое, что и во втором упражнении,
#      но на этот раз случайные числа должны быть целыми.

Кроме того, команда date так же может использоваться для генерации псевдослучайных целых чисел.