Ответ 1
Другие ответы на все включают перетасовку массива, которая O(n)
.
Это означает изменение исходного массива (деструктивный) или копирование исходного массива (интенсивный объем памяти).
Первый способ повысить эффективность памяти - это не перетасовывать исходный массив, а перетасовывать массив индексов.
# Shuffled list of indexes into @deck
my @shuffled_indexes = shuffle(0..$#deck);
# Get just N of them.
my @pick_indexes = @shuffled_indexes[ 0 .. $num_picks - 1 ];
# Pick cards from @deck
my @picks = @deck[ @pick_indexes ];
Он по крайней мере не зависит от содержимого @deck, но его все еще O (nlogn) и O (n) памяти.
Более эффективный алгоритм (не обязательно быстрее, зависит теперь от вашего большого массива) заключается в том, чтобы посмотреть на каждый элемент массива и решить, собирается ли он сделать его в массиве. Это похоже на то, как вы выбираете случайную строку из файла, не читая весь файл в памяти, каждая строка имеет вероятность 1/N, которую нужно выбрать, где N - номер строки. Таким образом, первая строка имеет шанс 1/1 (она всегда выбирается). Следующий имеет 1/2. Затем 1/3 и так далее. Каждый выбор будет перезаписывать предыдущий выбор. Это приводит к тому, что каждая строка имеет вероятность 1/total_lines.
Вы можете решить это самостоятельно. Однострочный файл имеет шанс 1/1, поэтому первый выбирается всегда. Файл с двумя строками... первая строка имеет 1/1, а затем 1/2 шанса на выживание, которая равна 1/2, а вторая строка имеет шанс 1/2. Для трехстрочного файла... первая строка имеет 1/1 шанс быть выбранным, а затем 1/2 * 2/3 шанс выжить, который равен 2/6 или 1/3. И так далее.
Алгоритм O (n) для скорости, он итерации через неупорядоченный массив один раз и не потребляет больше памяти, чем требуется для хранения выбора.
С небольшим изменением, это работает для нескольких выборов. Вместо шанса 1/$position
, он $picks_left / $position
. Каждый раз, когда выбор успешный, вы уменьшаете $picks_left. Вы работаете от высокого положения к низкому. В отличие от предыдущего, вы не перезаписываете.
my $picks_left = $picks;
my $num_left = @$deck;
my @picks;
my $idx = 0;
while($picks_left > 0 ) { # when we have all our picks, stop
# random number from 0..$num_left-1
my $rand = int(rand($num_left));
# pick successful
if( $rand < $picks_left ) {
push @result, $deck->[$idx];
$picks_left--;
}
$num_left--;
$idx++;
}
Это как perl5i реализует свой метод выбора (следующий выпуск).
Чтобы понять, почему это работает, возьмите пример выбора 2 из списка из 4 элементов. Каждый должен иметь шанс 1/2 быть выбранным.
1. (2 picks, 4 items): 2/4 = 1/2
Прост достаточно. Следующий элемент имеет шанс 1/2, чтобы элемент уже был выбран, и в этом случае шансы равны 1/3. В противном случае его шансы равны 2/3. Выполнение математики...
2. (1 or 2 picks, 3 items): (1/3 * 1/2) + (2/3 * 1/2) = 3/6 = 1/2
Далее есть шанс 1/4, что оба элемента уже будут выбраны (1/2 * 1/2), тогда у него нет шансов; 1/2 шанс, что только один будет выбран, тогда он будет 1/2; и оставшаяся 1/4, что никакие предметы не будут выбраны, в этом случае это 2/2.
3. (0, 1 or 2 picks, 2 items): (0/2 * 1/4) + (1/2 * 2/4) + (2/2 * 1/4) = 2/8 + 1/4 = 1/2
Наконец, для последнего элемента есть 1/2 предыдущего предыдущего выбора.
4. (0 or 1 pick, 1 items): (0/1 * 2/4) + (1/1 * 2/4) = 1/2
Не совсем доказательство, но полезно убедить себя, что это работает.