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

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

List<string> items;
while (true)
{
    string item = GetNextItem();
    if (item == null)
        break;
}
int index = random.GetNext(0, items.count);

Как видите, я создаю гигантскую коллекцию, которая мне на самом деле не нужна, мне просто нужно случайное число от 0 до количества элементов. Вот что я думаю сделать, и это работает, но я хотел бы знать, может ли кто-нибудь из экспертов найти в этом ошибку:

int index = -1;
int total;
string selectedItem;
while (true)
{
    string item = GetNextItem();
    if (item == null)
        break;

    ++total;
    int rnd = random.Next(0, total);
    if (rnd == total- 1)
    {
        index = total- 1;
        selectedItem = item;
    }
}

Это дает мне мой порядковый номер и случайно выбранный элемент. Я думаю об этом так: когда всего 3 элемента, например, я выбираю случайное число от 0 до 2 (включительно), и если оно равно 2, я использую новый элемент в качестве выбранного элемента, если не просто игнорирую его. По мере увеличения общего количества предметов вероятность выбора каждого нового предмета соответственно уменьшается.

Является ли этот метод «хорошим»? Это так же «случайно», как создание массива и выбор элемента позже? Это так быстро, как это может быть? Пожалуйста, направьте меня через мое невежество в случайных числах. :)


person Jon Tackabury    schedule 07.03.2010    source источник
comment
@Sky Sanders: ночная субботняя почта...   -  person Randolpho    schedule 07.03.2010
comment
LOL - поздний ночной пост точно. :) У меня есть бесконечный цикл, который продолжает извлекать элементы из источника, пока не будет возвращен нулевой элемент. Мне нужно выбрать один из этих предметов наугад.   -  person Jon Tackabury    schedule 07.03.2010
comment
Я думаю, вы использовали «количество», когда имели в виду «всего» во втором блоке кода.   -  person Ian Mercer    schedule 07.03.2010
comment
@Hightechrider: Спасибо, я исправил это.   -  person Jon Tackabury    schedule 07.03.2010
comment
Если бы это было четко сформулировано, это был бы довольно хороший вопрос для интервью. Я должен иметь это в виду.   -  person zdav    schedule 07.03.2010
comment
Это классический вопрос для интервью на Amazon.com — я думаю, что каждый, кто брал там интервью, задавался им хотя бы раз!   -  person Ken    schedule 07.03.2010
comment
это обман, хотя я не могу его найти...   -  person P Shved    schedule 07.03.2010
comment
Спасибо всем, это метод, который я использовал, и он отлично работает.   -  person Jon Tackabury    schedule 10.03.2010


Ответы (4)


То, что вы делаете, сработает.

Вот его переформулировка, которая может сделать алгоритм немного более понятным:

  1. Выберите первый элемент, есть вероятность 100%, что это будет текущий выбор
  2. Если есть второй элемент, есть вероятность 1/2, что он заменит текущий выбор (если вы сделаете математику, то вероятность 50%, что это будет первый элемент, и вероятность 50%, что он будет вторым пункт)
  3. Если есть третий элемент, есть вероятность 1/3, что он заменит текущий выбор (опять же, математическая вероятность для каждого элемента равна 1/3)
  4. Если есть четвертый элемент, есть шанс 1/4, что он заменит текущий выбор.
  5. ... и т.д ...

Обратите внимание, что вы должны быть в состоянии вычислить шанс 1/x, сказав rand.Next(0,x) == 0 (или любое другое целое число от 0 до x - 1 включительно; вам не нужно беспокоиться об использовании total - 1.

На самом деле это довольно аккуратный подход; изначально я думал, что не будет никакого хорошего способа сделать то, о чем вы просили!

person Daniel LeCheminant    schedule 07.03.2010
comment
Я довольно плохо разбираюсь в вероятности и статистике, но это выглядит как лучший вариант, если заранее не знать верхнюю границу. - person Josh; 07.03.2010
comment
Бинго, это то, о чем я думал в своем бессвязном описании. Может ли кто-нибудь найти ошибку в этом, что касается выбора случайных предметов? - person Jon Tackabury; 07.03.2010
comment
@Jon, в этом подходе нет никаких недостатков: это очень классический, даже традиционный алгоритм, опубликованный, например. в книгах Кнута «Искусство компьютерного программирования» — см., например, geomblog. blogspot.com/2008/01/happy-birthday-don-knuth.html . - person Alex Martelli; 07.03.2010
comment
Это действительно известный алгоритм. Обобщенная форма для выбора нескольких элементов называется Reservoir Sampling (en.wikipedia.org/wiki/Reservoir_sampling< /а>). - person Nick Johnson; 07.03.2010

Ваш подход выглядит хорошо, да.

1 элемент = будет выбран

2 предмета = 50% шанс, что вы выберете 2-й предмет вместо 1-го.

3 предмета = 33% шанс, что вы выберете третий предмет, 67% шанс, что вы выберете один из первых двух предметов

4 предмета = 25% шанс, что вы выберете 4-й предмет, 75% шанс, что вы выберете...

...

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

Вы можете упростить случайную проверку:

 int rnd = random.Next(0, total);
    if (rnd == 0)

Поскольку не имеет значения, какое из значений total-1 вы проверяете, чтобы получить вероятность 1/n.

person Ian Mercer    schedule 07.03.2010

мы можем доказать это по индукции.
это верно для 1;
если это верно для n; верно для n+1;
=> вероятн. выбора для первых n элементов = 1/n
=> sice prob. вероятность выбора (n+1)-го элемента равна 1/(n+1)
=> вероятность невыбора (n+1)-го элемента равна n/(n+1)
=> вероятность выбора для первые n элементов после добавления (n+1)-го элемента = 1/n*(n/n+1)=1/n+1

person Community    schedule 10.07.2010

В первом фрагменте кода вы используете items.count, чтобы знать, сколько у вас элементов. Вам необходимо знать это число, чтобы каждый элемент имел равные шансы быть выбранным.

Как вы написали, вы генерируете случайное число i такое, что 0 ‹= i ‹ items.count, а затем пытаетесь быстро получить доступ к элементу i списка. (Связный список может быть не лучшим выбором структуры данных.)

Если у вас есть хорошая оценка N количества элементов, вы можете использовать это вместо items.count.

Во втором фрагменте кода вам, возможно, придется инициализировать «всего» нулем.

person Winston C. Yang    schedule 07.03.2010