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


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

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

  1. #!/bin/bash
  2. # $RANDOM возвращает различные случайные числа при каждом обращении к ней.
  3. # Диапазон изменения: 0 - 32767 (16-битовое целое со знаком).
  4. MAXCOUNT=10
  5. count=1
  6. echo
  7. echo "$MAXCOUNT случайных чисел:"
  8. echo "-----------------"
  9. while [ "$count" -le $MAXCOUNT ]      # Генерация 10 ($MAXCOUNT) случайных чисел.
  10. do
  11.   number=$RANDOM
  12.   echo $number
  13.   let "count += 1"  # Нарастить счетчик.
  14. done
  15. echo "-----------------"
  16. # Если вам нужны случайные числа не превышающие определенного числа,
  17. # воспользуйтесь оператором деления по модулю (остаток от деления).
  18. RANGE=500
  19. echo
  20. number=$RANDOM
  21. let "number %= $RANGE"
  22. echo "Случайное число меньше $RANGE  ---  $number"
  23. echo
  24. # Если вы желаете ограничить диапазон "снизу",
  25. # то просто производите генерацию псевдослучайных чисел в цикле до тех пор,
  26. # пока не получите число большее нижней границы.
  27. FLOOR=200
  28. number=0   # инициализация
  29. while [ "$number" -le $FLOOR ]
  30. do
  31.   number=$RANDOM
  32. done
  33. echo "Случайное число, большее $FLOOR ---  $number"
  34. echo
  35. # Эти два способа могут быть скомбинированы.
  36. number=0   #initialize
  37. while [ "$number" -le $FLOOR ]
  38. do
  39.   number=$RANDOM
  40.   let "number %= $RANGE"  # Ограничение "сверху" числом $RANGE.
  41. done
  42. echo "Случайное число в диапазоне от $FLOOR до $RANGE ---  $number"
  43. echo
  44. # Генерация случайных "true" и "false" значений.
  45. BINARY=2
  46. number=$RANDOM
  47. T=1
  48. let "number %= $BINARY"
  49. # let "number >>= 14"    дает более равномерное распределение
  50. # (сдвиг вправо смещает старший бит на нулевую позицию, остальные биты обнуляются).
  51. if [ "$number" -eq $T ]
  52. then
  53.   echo "TRUE"
  54. else
  55.   echo "FALSE"
  56. fi
  57. echo
  58. # Можно имитировать бросание 2-х игровых кубиков.
  59. SPOTS=7   # остаток от деления на 7 дает диапазон 0 - 6.
  60. ZERO=0
  61. die1=0
  62. die2=0
  63. # Кубики "выбрасываются" раздельно.
  64.   while [ "$die1" -eq $ZERO ]     # Пока на "кубике" ноль.
  65.   do
  66.     let "die1 = $RANDOM % $SPOTS" # Имитировать бросок первого кубика.
  67.   done
  68.   while [ "$die2" -eq $ZERO ]
  69.   do
  70.     let "die2 = $RANDOM % $SPOTS" # Имитировать бросок второго кубика.
  71.   done
  72. let "throw = $die1 + $die2"
  73. echo "Результат броска кубиков = $throw"
  74. echo
  75. exit 0

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

  1. #!/bin/bash
  2. # pick-card.sh
  3. # Пример выбора случайного элемента массива.
  4. # Выбор случайной карты из колоды.
  5. Suites="Треф
  6. Бубей
  7. Червей
  8. Пик"
  9. Denominations="2
  10. 3
  11. 4
  12. 5
  13. 6
  14. 7
  15. 8
  16. 9
  17. 10
  18. Валет
  19. Дама
  20. Король
  21. Туз"
  22. suite=($Suites)                # Инициализация массивов.
  23. denomination=($Denominations)
  24. num_suites=${#suite[*]}        # Количество элементов массивов.
  25. num_denominations=${#denomination[*]}
  26. echo -n "${denomination[$((RANDOM%num_denominations))]} "
  27. echo ${suite[$((RANDOM%num_suites))]}
  28. # $bozo sh pick-cards.sh
  29. # Валет Треф
  30. # Спасибо "jipe," за пояснения по работе с $RANDOM.
  31. exit 0
Note

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

  1. #  Генерация случайных чисел в диапазоне 6 - 30.
  2. rnumber=$((RANDOM%25+6))
  3. #  Генерируется случайное число из диапазона 6 - 30,
  4. #+ но при этом число должно делиться на 3 без остатка.
  5. rnumber=$(((RANDOM%30/3+1)*3))
  6. # Примечательно, если $RANDOM возвращает 0
  7. # то это приводит к возникновению ошибки.
  8. #  Упражнение: Попробуйте разобраться с выражением самостоятельно.


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

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


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

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

  1. #!/bin/bash
  2. # random-between.sh
  3. # Псевдослучайное число из заданного диапазона.
  4. # Автор: Bill Gradwohl,
  5. # незначительные изменения внесены автором документа.
  6. # Используется с разрешения автора сценария.
  7. randomBetween() {
  8.    #  Генерация положительных и отрицательных псевдослучайных чисел,
  9.    #+ в диапазоне от $min до $max,
  10.    #+ которые кратны числу $divisibleBy.
  11.    #
  12.    #  Bill Gradwohl - Oct 1, 2003
  13.    syntax() {
  14.    # Вложенная функция.
  15.       echo
  16.       echo    "Порядок вызова: randomBetween [min] [max] [multiple]"
  17.       echo
  18.       echo    "Функция ожидает до 3-х входных аргументов, но они не являются обязательными."
  19.       echo    "min — нижняя граница диапазона"
  20.       echo    "max — верхняя граница диапазона"
  21.       echo    "multiple — делитель, на который должен делиться результат без остатка."
  22.       echo
  23.       echo    "Если какой либо из параметров отсутствует, по-умолчанию принимаются значения: 0 32767 1"
  24.       echo    "В случае успеха функция возвращает 0, иначе — 1"
  25.       echo    "и это сообщение о порядке вызова."
  26.       echo    "Результат возвращается в глобальной переменной randomBetweenAnswer"
  27.       echo    "Входные параметры, имеющие отрицательные значения, обрабатываются корректно."
  28.    }
  29.    local min=${1:-0}
  30.    local max=${2:-32767}
  31.    local divisibleBy=${3:-1}
  32.    # При отсутствии какого либо из входных параметров, они принимают значения по-умолчанию.
  33.    local x
  34.    local spread
  35.    # Делитель должен быть положительным числом.
  36.    [ ${divisibleBy} -lt 0 ] && divisibleBy=$((0-divisibleBy))
  37.    # Проверка корректности входных параметров.
  38.    if [ $# -gt 3 -o ${divisibleBy} -eq 0 -o  ${min} -eq ${max} ]; then
  39.       syntax
  40.       return 1
  41.    fi
  42.    # Если min больше чем max, то поменять их местами.
  43.    if [ ${min} -gt ${max} ]; then
  44.       # Поменять местами.
  45.       x=${min}
  46.       min=${max}
  47.       max=${x}
  48.    fi
  49.    #  Если min не делится без остатка на $divisibleBy,
  50.    #+ то привести его к ближайшему подходящему числу из заданного диапазона.
  51.    if [ $((min/divisibleBy*divisibleBy)) -ne ${min} ]; then
  52.       if [ ${min} -lt 0 ]; then
  53.          min=$((min/divisibleBy*divisibleBy))
  54.       else
  55.          min=$((((min/divisibleBy)+1)*divisibleBy))
  56.       fi
  57.    fi
  58.    #  Если max не делится без остатка на $divisibleBy,
  59.    #+ то привести его к ближайшему подходящему числу из заданного диапазона.
  60.    if [ $((max/divisibleBy*divisibleBy)) -ne ${max} ]; then
  61.       if [ ${max} -lt 0 ]; then
  62.          max=$((((max/divisibleBy)-1)*divisibleBy))
  63.       else
  64.          max=$((max/divisibleBy*divisibleBy))
  65.       fi
  66.    fi
  67.    #  ---------------------------------------------------------------------
  68.    #  А теперь собственно нахождение псевдослучайного числа.
  69.    #  Обратите внимание: чтобы получить псевдослучайное число в конце диапазона
  70.    #+ необходимо рассматривать диапазон от 0 до
  71.    #+ abs(max-min)+divisibleBy, а не abs(max-min)+1.
  72.    #  Этим превышением верхней границы диапазона можно пренебречь
  73.    #+ поскольку эта новая граница никогда не будет достигнута.
  74.    #  Если использовать при вычислении формулу abs(max-min)+1,
  75.    #+ то будут получаться вполне корректные значения, но при этом,
  76.    #+ возвращаемые значения будут значительно ниже
  77.    #+ верхней границы диапазона.
  78.    #  ---------------------------------------------------------------------
  79.    spread=$((max-min))
  80.    [ ${spread} -lt 0 ] && spread=$((0-spread))
  81.    let spread+=divisibleBy
  82.    randomBetweenAnswer=$(((RANDOM%spread)/divisibleBy*divisibleBy+min))
  83.    return 0
  84. }
  85. # Проверка функции.
  86. min=-14
  87. max=20
  88. divisibleBy=3
  89. #  Создадим массив, который будет содержать счетчики встречаемости каждого из чисел
  90. #+ в заданном диапазоне.
  91. declare -a answer
  92. minimum=${min}
  93. maximum=${max}
  94.    if [ $((minimum/divisibleBy*divisibleBy)) -ne ${minimum} ]; then
  95.       if [ ${minimum} -lt 0 ]; then
  96.          minimum=$((minimum/divisibleBy*divisibleBy))
  97.       else
  98.          minimum=$((((minimum/divisibleBy)+1)*divisibleBy))
  99.       fi
  100.    fi
  101.    #  Если max не делится без остатка на $divisibleBy,
  102.    #+ то привести его к ближайшему подходящему числу из заданного диапазона.
  103.    if [ $((maximum/divisibleBy*divisibleBy)) -ne ${maximum} ]; then
  104.       if [ ${maximum} -lt 0 ]; then
  105.          maximum=$((((maximum/divisibleBy)-1)*divisibleBy))
  106.       else
  107.          maximum=$((maximum/divisibleBy*divisibleBy))
  108.       fi
  109.    fi
  110. #  Необходимое условие при работе с массивами --
  111. #+ индекс массива должен быть положительным числом,
  112. #+ поэтому введена дополнительная переменная displacement, которая
  113. #+ гарантирует положительность индексов.
  114. displacement=$((0-minimum))
  115. for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
  116.    answer[i+displacement]=0
  117. done
  118. # Цикл с большим количеством итераций, чтобы посмотреть — что мы получаем.
  119. loopIt=1000   #  Автор сценария предложил 100000 итераций,
  120.               #+ но в этом случае цикл работает чересчур долго.
  121. for ((i=0; i<${loopIt}; ++i)); do
  122.    #  Обратите внимание: числа min и max передаются функции в обратном порядке,
  123.    #+ чтобы продемонстрировать, что функция обрабатывает их корректно.
  124.    randomBetween ${max} ${min} ${divisibleBy}
  125.    # Вывод сообщения об ошибке, если функция вернула некорректное значение.
  126.    [ ${randomBetweenAnswer} -lt ${min} -o ${randomBetweenAnswer} -gt ${max} ] && echo Выход за границы диапазона MIN .. MAX  - ${randomBetweenAnswer}!
  127.    [ $((randomBetweenAnswer%${divisibleBy})) -ne 0 ] && echo Число не делится на заданный делитель без остатка - ${randomBetweenAnswer}!
  128.    # Записать полученное число в массив.
  129.    answer[randomBetweenAnswer+displacement]=$((answer[randomBetweenAnswer+displacement]+1))
  130. done
  131. # Проверим полученные результаты
  132. for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
  133.    [ ${answer[i+displacement]} -eq 0 ] && echo "Число $i не было получено ни разу." || echo "Число ${i} встречено ${answer[i+displacement]} раз."
  134. done
  135. exit 0

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

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

  1. #!/bin/bash
  2. # Случайные ли числа возвращает RANDOM?
  3. RANDOM=$$       # Инициализация генератора случайных чисел числом PID процесса-сценария.
  4. PIPS=6          # Кубик имеет 6 граней.
  5. MAXTHROWS=600   # Можете увеличить, если не знаете куда девать свое время.
  6. throw=0         # Счетчик бросков.
  7. zeroes=0        # Обнулить счетчики выпадения отдельных граней.
  8. ones=0          # т.к. неинициализированные переменные - "пустые", и не равны нулю!.
  9. twos=0
  10. threes=0
  11. fours=0
  12. fives=0
  13. sixes=0
  14. print_result ()
  15. {
  16. echo
  17. echo "единиц   =   $ones"
  18. echo "двоек    =   $twos"
  19. echo "троек    =   $threes"
  20. echo "четверок =   $fours"
  21. echo "пятерок  =   $fives"
  22. echo "шестерок =   $sixes"
  23. echo
  24. }
  25. update_count()
  26. {
  27. case "$1" in
  28.   0) let "ones += 1";;   # 0 соответствует грани "1".
  29.   1) let "twos += 1";;   # 1 соответствует грани "2", и так далее
  30.   2) let "threes += 1";;
  31.   3) let "fours += 1";;
  32.   4) let "fives += 1";;
  33.   5) let "sixes += 1";;
  34. esac
  35. }
  36. echo
  37. while [ "$throw" -lt "$MAXTHROWS" ]
  38. do
  39.   let "die1 = RANDOM % $PIPS"
  40.   update_count $die1
  41.   let "throw += 1"
  42. done
  43. print_result
  44. # Количество выпадений каждой из граней должно быть примерно одинаковым, если считать RANDOM достаточно случайным.
  45. # Для $MAXTHROWS = 600, каждая грань должна выпасть примерно 100 раз (плюс-минус 20).
  46. #
  47. # Имейте ввиду, что RANDOM - это генератор ПСЕВДОСЛУЧАЙНЫХ чисел,
  48. # Упражнение:
  49. # ---------------
  50. # Перепишите этот сценарий так, чтобы он имитировал 1000 бросков монеты.
  51. # На каждом броске возможен один из двух вариантов выпадения - "ОРЕЛ" или "РЕШКА".
  52. exit 0

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

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

  1. #!/bin/bash
  2. # seeding-random.sh: Переустановка переменной RANDOM.
  3. MAXCOUNT=25       # Длина генерируемой последовательности чисел.
  4. random_numbers ()
  5. {
  6. count=0
  7. while [ "$count" -lt "$MAXCOUNT" ]
  8. do
  9.   number=$RANDOM
  10.   echo -n "$number "
  11.   let "count += 1"
  12. done
  13. }
  14. echo; echo
  15. RANDOM=1          # Переустановка начального числа генератора случайных чисел RANDOM.
  16. random_numbers
  17. echo; echo
  18. RANDOM=1          # То же самое начальное число...
  19. random_numbers    # ...в результате получается та же последовательность чисел.
  20.                   #
  21.                   # В каких случаях может оказаться полезной генерация совпадающих серий?
  22. echo; echo
  23. RANDOM=2          # Еще одна попытка, но с другим начальным числом...
  24. random_numbers    # получим другую последовательность.
  25. echo; echo
  26. # RANDOM=$$  в качестве начального числа выбирается PID процесса-сценария.
  27. # Вполне допустимо взять в качестве начального числа результат работы команд 'time' или 'date'.
  28. # Немного воображения...
  29. SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
  30. #  Псевдослучайное число забирается
  31. #+ из системного генератора псевдослучайных чисел /dev/urandom ,
  32. #+ затем конвертируется в восьмеричное число командой "od",
  33. #+ и наконец "awk" возвращает единственное число для переменной SEED.
  34. RANDOM=$SEED
  35. random_numbers
  36. echo; echo
  37. exit 0

Note

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

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

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

  1. #!/bin/bash
  2. # random2.sh: Генерация псевдослучайных чисел в диапазоне 0 - 1.
  3. # Используется функция rand() из awk.
  4. AWKSCRIPT=' { srand(); print rand() } '
  5. # Команды/параметры, передаваемые awk
  6. # Обратите внимание, функция srand() переустанавливает начальное число генератора случайных чисел.
  7. echo -n "Случайное число в диапазоне от 0 до 1 = "
  8. echo | awk "$AWKSCRIPT"
  9. exit 0
  10. # Упражнения:
  11. # ---------
  12. # 1) С помощью оператора цикла выведите 10 различных случайных чисел.
  13. #      (Подсказка: вам потребуется вызвать функцию "srand()"
  14. #      в каждом цикле с разными начальными числами.
  15. #      Что произойдет, если этого не сделать?)
  16. # 2) Заставьте сценарий генерировать случайные числа в диапазоне 10 - 100
  17. #      используя целочисленный множитель, как коэффициент масштабирования
  18. # 3) То же самое, что и во втором упражнении,
  19. #      но на этот раз случайные числа должны быть целыми.

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