Поиск случайных чисел

Лавовая лампа

Случайные числа придают жизни остроту. Ну, по крайней мере, в мире программирования. Например, Bored API использует их для выбора занятий, способных развеять вашу скуку, а игры вроде Minecraft — для создания случайной местности. Есть и другие варианты практического использования случайных чисел. Например, шифрование в CloudFlare: там в качестве генератора случайных чисел для шифрования веб-запросов используется стена из лавовых ламп.

Давайте теперь поговорим об использовании случайных чисел в Python. Например, разберем следующую задачу с собеседования.

# Напишите функцию, которая будет принимать
# целое число n и список чисел и генерировать случайное число
# в диапазоне от 0 до n-1 (включительно), 
# причем этого числа не должно быть в списке.

Если необходимо сгенерировать случайное число, нам нужны две вещи. Во-первых, библиотека Python random, а во-вторых, библиотека math (чтобы округлять случайные числа до целых).

import random
import math

Далее мы определим наш метод. Он должен принимать целое число и список целых чисел, который мы назовем nums.

def get_rand(n, nums):
    pass

Решение 1

Для начала давайте обсудим «великоразумную» идею, которая первой приходит в голову. План следующий. Сначала находим случайное число в заданном диапазоне, а затем, если это число есть в списке, просто запускаем генератор случайных чисел снова. Повторяем до тех пор, пока не найдем валидное число.

Начнем с получения рандомного числа от 0 до n. В объекте random есть метод под названием random(), который возвращает случайное десятичное число от 0,0 до 1,0. Для получения числа от 0 до n мы будем умножать полученное десятичное число на n, а затем округлять в меньшую сторону при помощи math.floor().

rand_num = math.floor(random.random() * n)

Если каждый раз после получения случайного числа мы будем перебирать в цикле весь список, проверяя, нет ли там этого числа, время работы этой программы будет просто ужасным. Мы можем сделать кое-что получше. Давайте вспомним о типе данных, который используется во многих языках программирования, включая Python, — о множествах.

Множество (set) содержит уникальные неупорядоченные значения.

Множества можно представлять в виде облаков, в которых совершенно хаотично плавает куча чисел. И что особенно важно, эти числа не повторяются, они все уникальны. Если вы попытаетесь добавить во множество число 3, а оно там уже есть, вы не сможете внести его повторно.

С уникальностью элементов во множестве связан полезный для нас факт. Проверка того, содержится ли во множестве какое-либо число, имеет постоянную временную сложность, O(1). Это не суперэффективно, но все же лучше, чем сложность O(N), которая у нас была бы, если бы мы стали каждый раз перебирать все элементы в цикле.

Как нам реализовать множество? Начнем с вызова класса set при помощи его конструктора, set().

s = set()

Далее мы добавим в наше множество числа из списка.

for num in nums:
    s.add(num)

Теперь мы будем проверять, содержится ли во множестве полученное нами случайное число. Если оно там есть, мы будем искать новое случайное число. Это можно реализовать при помощи цикла while, который будет запускаться, если случайное число содержится во множестве. Чисто для интереса можно добавить print — чтобы посмотреть, сколько раз методу придется заново найти случайное число, прежде чем будет найдено валидное.

while rand_num in s:
    print("reroll")
    rand_num = math.floor(random.random()*n)

И, наконец, мы возвращаем итоговое случайное число сразу после выхода из цикла while. Все вместе выглядит следующим образом:

def get_rand(n, nums):
    rand_num = math.floor(random.random()*n)
    s = set()
    for num in nums:
        s.add(num)
    while rand_num in s:
        print("reroll")
        rand_num = math.floor(random.random()*n)
    return rand_num

Если мы это испытаем, запустив get_rand(5, [0, 1, 2, 3]), мы получим 4 — единственное число, меньшее 5 и не входящее в список.

Как уже упоминалось, это решение не слишком эффективно. Этот алгоритм вполне может выбирать рандомные числа до бесконечности, если подходящее никак не будет попадаться. Например, это произойдет в ситуации, когда попросту не существует числа, отвечающего заданным критериям. Вот попробуйте запустить get_rand(5, [0, 1, 2, 3, 4]). Теперь в списке содержатся все целые числа до 5. Получилось как-то так?

reroll
reroll
reroll  
reroll
reroll
reroll
reroll
…

Если нет чисел, из которых можно выбрать подходящее, этот алгоритм будет работать бесконечно. Поэтому давайте подумаем о других вариантах, в которых учитывались бы крайние случаи и ликвидировалась возможность бесконечной работы.

Решение 2

Иногда мы подходим к решению задач по программированию так, будто они математические, и забываем, что мы-то разработчики. Так что всегда стоит спрашивать себя: «А не могу ли я самостоятельно построить то, что нужно для решения этой задачи?»

Представьте, что вместо того чтобы каждый раз проверять, есть ли наше случайное число в списке, мы создаем список валидных чисел, из которых потом выбираем случайное. Такой подход также решит нашу проблему с бесконечной работой программы, потому что если список возможных чисел будет пуст, мы сможем просто вернуть None.

Первая часть нашего метода остается прежней: мы берем числа из списка nums и помещаем их во множество. После этого нам нужно сделать пустой список. В него мы поместим все те валидные числа, из которых можно будет выбирать случайное.

def get_rand2(n, nums):
    s = set()
    for num in nums:
        s.add(num)
    poss_nums = []

Как нам найти валидные числа? Мы просто переберем в цикле каждое число от 0 до n и, если этого числа не будет во множестве, мы добавим его в список.

for num in range(n):
    if num not in s:
        poss_nums.append(num)

Если бы мы теперь запустили get_rand(5, [0, 1, 2, 3]), мы бы получили список poss_nums, содержащий только число 4.

Далее мы проверяем наш крайний случай. Просто смотрим, не пуст ли список, и если он действительно пуст, возвращаем None.

if not len(poss_nums):
    return None

Что нам остается? Конечно, выбрать случайное число! Сначала мы выбираем случайный индекс, получая случайное число при помощи функции random.random() и умножая его на длину списка с валидными числами. Затем мы возвращаем число под этим индексом из списка валидных чисел.

rand_i = math.floor(random.random()*len(poss_nums))
return poss_nums[rand_i]

Вот как выглядит весь код полностью:

def get_rand2(n, nums):
    s = set()
    for num in nums:
        s.add(num)
    poss_nums = []
    for num in range(n):
        if num not in s:
            poss_nums.append(num)
    if not len(poss_nums):
        return None
    rand_i = math.floor(random.random()*len(poss_nums))
    return poss_nums[rand_i]

Если мы теперь попробуем запустить get_rand(5, [0, 1, 2, 3, 4]) и вывести результат, мы получим None, потому что нет чисел, отвечающих заданным критериям. Ура! Больше никаких бесконечных циклов.