Как я могу случайно взять n элементов из массива Perl?

У меня есть массив A = [a1,a2,a3,...aP] с размером P. Мне нужно отбирать элементы q из массива A.

Я планирую использовать цикл с итерациями q и произвольно выбирать элит из A на каждой итерации. Но как я могу убедиться, что выбранный номер будет отличаться на каждой итерации?

Ответ 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

Не совсем доказательство, но полезно убедить себя, что это работает.

Ответ 2

От perldoc perlfaq4:

Как произвольно перемешать массив?

Если у вас установлен Perl 5.8.0 или более поздней версии, или если у вас есть Scalar-List-Utils 1.03 или новее, вы можете сказать:

use List::Util 'shuffle';
@shuffled = shuffle(@list);

Если нет, вы можете использовать Shuffle Fisher-Yates.

sub fisher_yates_shuffle {

    my $deck = shift;  # $deck is a reference to an array
    return unless @$deck; # must not be empty!

    my $i = @$deck;
    while (--$i) {
        my $j = int rand ($i+1);
        @$deck[$i,$j] = @$deck[$j,$i];
    }
}


# shuffle my mpeg collection
# 

my @mpeg = <audio/*/*.mp3>;
fisher_yates_shuffle( \@mpeg );    # randomize @mpeg in place
print @mpeg;

Вы также можете использовать List::Gen:

my $gen = <1..10>;
print "$_\n" for $gen->pick(5);  # prints five random numbers

Ответ 3

Вы можете использовать алгоритм Shuffle Fisher-Yates, чтобы случайным образом переставить ваш массив, а затем использовать срез первых q элементов. Здесь код из PerlMonks:

# randomly permutate @array in place
sub fisher_yates_shuffle
{
    my $array = shift;
    my $i = @$array;
    while ( --$i )
    {
        my $j = int rand( $i+1 );
        @$array[$i,$j] = @$array[$j,$i];
    }
}

fisher_yates_shuffle( \@array );    # permutes @array in place

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

Ответ 4

Вы можете создать второй массив, boolean с размером P и сохранить true для выбранных чисел. И когда выбран номер, проверьте вторую таблицу; в случае "истинного" вы должны выбрать следующий.