Выберите N различных элементов случайным образом из последовательности неизвестной длины, всего за одну итерацию

Я пытаюсь написать алгоритм, который бы выбирал N отдельных элементов из последовательности случайным образом, не зная заранее размера последовательности, и где было бы дорого обходить последовательность более одного раза. Например, элементы последовательности могут быть строками огромного файла.

Я нашел решение, когда N = 1 (то есть "выбрать ровно один элемент случайным образом из огромной последовательности"):

import random
items = range(1, 10) # Imagine this is a huge sequence of unknown length
count = 1
selected = None
for item in items:
    if random.random() * count < 1:
        selected = item
    count += 1

Но как я могу добиться того же для других значений N (скажем, N = 3)?

Ответ 1

Используйте выборку коллектора. Это очень простой алгоритм, который работает для любого N.

Здесь является одной реализацией Python, а здесь является другой.

Ответ 2

Если ваша последовательность достаточно коротка, чтобы читать ее в памяти и произвольно сортировать, это приемлемо, тогда простой подход состоял бы в том, чтобы просто использовать random.shuffle:

import random
arr=[1,2,3,4]

# In-place shuffle
random.shuffle(arr)

# Take the first 2 elements of the now randomized array
print arr[0:2]
[1, 3]

В зависимости от типа вашей последовательности вам может потребоваться преобразовать его в список, вызвав на нем list(your_sequence), но это будет работать независимо от типов объектов в вашей последовательности.

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

Ответ 3

Самый простой, который я нашел, это этот ответ в SO:

import random

my_list = [1, 2, 3, 4, 5]
num_selections = 2

new_list = random.sample(my_list, num_selections)

# To preserve the order of the list, you could do:
randIndex = random.sample(range(len(my_list)), n_selections)
randIndex.sort()
new_list = [my_list[i] for i in randIndex]

Ответ 4

Если у вас версия python 3. 6+, вы можете использовать выбор

from random import choices

items = range(1, 10)
new_items = choices(items, k = 3)

print(new_items) 
[6, 3, 1]

Ответ 5

@NPE верен, но связанные с ним реализации являются субоптимальными и не очень "питоновскими". Здесь лучшая реализация:

def sample(iterator, k):
    """
    Samples k elements from an iterable object.

    :param iterator: an object that is iterable
    :param k: the number of items to sample
    """
    # fill the reservoir to start
    result = [next(iterator) for _ in range(k)]

    n = k - 1
    for item in iterator:
        n += 1
        s = random.randint(0, n)
        if s < k:
            result[s] = item

    return result

Изменить. Как показано в @ panda -34, оригинальная версия была ошибочной, но не потому, что я использовал randint vs randrange. Проблема в том, что мое начальное значение для n не учитывало того факта, что randint включен на обоих концах диапазона. Учитывая это, проблема устранена. (Примечание: вы также можете использовать randrange, поскольку оно включено в минимальное значение и исключает максимальное значение.)

Ответ 6

Далее вы получите N случайных элементов из массива X

import random
list(map(lambda _: random.choice(X), range(N)))

Ответ 7

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

Предположим, что вы выбрали N элементов K случайным образом, и вы видите (K + 1)-й элемент. Примите его с вероятностью N/(K + 1) и его вероятности в порядке. Текущие предметы попали с вероятностью N/K и выбрасывались с вероятностью (N/(K + 1)) (1/N) = 1/(K + 1), поэтому выживать с вероятностью (N/K) ( K/(K + 1)) = N/(K + 1), поэтому их вероятности тоже ОК.

И да, я вижу, кто-то указал вам на выборку коллектора - это одно объяснение того, как это работает.

Ответ 8

Как упомянуто в упомянутых работах по отбору проб коллектора. Другой вариант - генерировать случайное число для каждого числа, которое вы видите, и выбирать верхние k-числа.

Чтобы сделать это итеративно, сохраняйте кучу пар k (случайное число, число) и всякий раз, когда вы видите новую цифру в кучу, если она больше, чем наименьшее значение в куче.

Ответ 9

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

from random import randint

random_nums = []
N = # whatever number of random numbers you want
r = # lower bound of number range
R = # upper bound of number range

x = 0

while x < N:
    random_num = randint(r, R) # inclusive range
    if random_num in random_nums:
        continue
    else:
        random_nums.append(random_num)
        x += 1

Причина цикла while в цикле for заключается в том, что он позволяет упростить реализацию непропускания в случайной генерации (т.е. если вы получите 3 дубликата, вы не получите номера N-3).