Более быстрый способ проверить элемент в массиве? - PullRequest
9 голосов
/ 04 августа 2011

Эта функция работает так же, как exists с хешами.

Я планирую много ее использовать.

Можно ли ее каким-то образом оптимизировать?

my @a = qw/a b c d/;

my $ret = array_exists("b", @a);

sub array_exists {
    my ($var, @a) = @_;

    foreach my $e (@a) {
        if ($var eq $e) {
            return 1;
        }
    }
    return 0;
}

Ответы [ 7 ]

12 голосов
/ 04 августа 2011

Если вам приходится много делать с фиксированным массивом, используйте вместо этого хеш:

 my %hash = map { $_, 1 } @array;

 if( exists $hash{$key} ) { ... }

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

use 5.010;

my @a = (
    qw(x y z),
    [ { 'b' => 1 } ],
    );

say 'Matches' if "b" ~~ @a; # This matches

Поскольку интеллектуальное сопоставление является рекурсивным, если оно продолжает углубляться в данныеструктур.Я пишу об этом в Переосмысление умного соответствия .

8 голосов
/ 04 августа 2011

Вы можете использовать интеллектуальное сопоставление , доступное в Perl 5.10 и более поздних версиях:

if ("b" ~~ @a) {
    # "b" exists in @a
}

Это должно быть намного быстрее, чем вызов функции.

7 голосов
/ 04 августа 2011

Я бы использовал List :: MoreUtils :: any .

my $ret = any { $_ eq 'b' } @a;
4 голосов
/ 03 апреля 2014

Поскольку в StackOverflow много похожих вопросов, в которых разные «правильные ответы» дают разные результаты, я попытался сравнить их. Этот вопрос, кажется, является хорошим местом для того, чтобы поделиться моим маленьким тестом.

Для своих тестов я использовал набор тестов (@test_set) из 1000 элементов (строк) длиной 10, где только один элемент ($search_value) соответствует данной строке.

Я принял следующие утверждения, чтобы проверить существование этого элемента в цикле 100 000 витков.

_grep

grep( $_ eq $search_value, @test_set )

_hash

{ map { $_ => 1 } @test_set }->{ $search_value }

_hash_premapped

$mapping->{ $search_value }
  • использует $mapping, который предварительно рассчитывается как $mapping = { map { $_ => 1 } @test_set } (который включен в окончательное измерение)

_ регулярное выражение

sub{ my $rx = join "|", map quotemeta, @test_set; $search_value =~ /^(?:$rx)$/ }

_ regex_prejoined

$search_value =~ /^(?:$rx)$/
  • использует регулярное выражение $rx, которое предварительно рассчитывается как $rx = join "|", map quotemeta, @test_set; (которое входит в окончательное измерение)

_manual_first

sub{ foreach ( @test_set ) { return 1 if( $_ eq $search_value ); } return 0; }

_first

first { $_ eq $search_value } @test_set
  • из List::Util (версия 1.38)

_smart

$search_value ~~ @test_set

_any

any { $_ eq $search_value } @test_set
  • из List::MoreUtils (версия 0.33)

На моей машине (Ubuntu, 3.2.0-60-generic, x86_64, Perl v5.14.2) я получил следующие результаты. Показанные значения являются секундами и возвращаются gettimeofday и tv_interval из Time::HiRes (версия 1.9726).

Элемент $search_value расположен в позиции 0 в массиве @test_set

_hash_premapped:    0.056211
_smart:             0.060267
_manual_first:      0.064195
_first:             0.258953
_any:               0.292959
_regex_prejoined:   0.350076
_grep:              5.748364
_regex:             29.27262
_hash:              45.638838

Элемент $search_value расположен в позиции 500 в массиве @test_set

_hash_premapped:    0.056316
_regex_prejoined:   0.357595
_first:             2.337911
_smart:             2.80226
_manual_first:      3.34348
_any:               3.408409
_grep:              5.772233
_regex:             28.668455
_hash:              45.076083

Элемент $search_value расположен в позиции 999 в массиве @test_set

_hash_premapped:    0.054434
_regex_prejoined:   0.362615
_first:             4.383842
_smart:             5.536873
_grep:              5.962746
_any:               6.31152
_manual_first:      6.59063
_regex:             28.695459
_hash:              45.804386

Заключение

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

Во многих случаях подготовленная среда не подходит.

Удивительно, но List::Util::first дает очень хорошие результаты, когда дело доходит до сравнения утверждений, которые не имеют подготовленной среды. Имея значение поиска в начале (которое, возможно, также можно интерпретировать как результат в меньших наборах), оно очень близко к избранным ~~ и any (и может даже находиться в диапазоне погрешности измерения). Для предметов в середине или в конце моего большего тестового набора, first определенно самый быстрый.

3 голосов
/ 04 августа 2011

Брайан Д Фой предложил использовать хеш, который дает O (1) поисков, за счет немного более дорогого создания хешей. Существует методика, которую Марк Джейсон Доминус описывает в своей книге Perl высшего порядка, где хеш используется для запоминания (или кеширования) результатов подпрограммы для данного параметра. Так, например, если findit(1000) всегда возвращает одно и то же для данного параметра, нет необходимости каждый раз пересчитывать результат. Техника реализована в модуле Memoize (входит в ядро ​​Perl).

Запоминание не всегда является победой. Иногда накладные расходы на запомненную обертку превышают затраты на вычисление результата. Иногда данный параметр вряд ли когда-либо будет проверен более одного раза или относительно несколько раз. И иногда нельзя гарантировать, что результат функции для данного параметра всегда будет одинаковым (т. Е. Кэш может устареть). Но если у вас дорогая функция со стабильными возвращаемыми значениями для каждого параметра, запоминание может быть большим выигрышем.

Так же, как ответ Брайана Д. Фоя использует хеш, Memoize использует хеш внутри. В реализации Memoize есть дополнительные издержки, но преимущество использования Memoize заключается в том, что он не требует рефакторинга исходной подпрограммы. Вы просто use Memoize;, а затем memoize( 'expensive_function' );, при условии, что оно соответствует критериям для извлечения выгоды из запоминания.

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

В бенчмаркинге мне приходилось принимать некоторые решения. Во-первых, сколько итераций для тестирования. Чем больше итераций мы тестируем, тем выше вероятность того, что мы получим хорошие попадания в кеш для запомненных версий. Затем я также должен был решить, сколько элементов поместить в массив выборок. Чем больше элементов, тем меньше вероятность попадания в кеш, но тем значительнее экономия при обращении к кешу. В итоге я выбрал массив для поиска, содержащий 8000 элементов, и выбрал поиск по 24000 итераций. Это означает, что в среднем должно быть два попадания в кэш на запомненный вызов. (Первый вызов с заданным параметром записывает в кеш, а второй и третий вызовы читают из кеша, поэтому в среднем два хороших попадания).

Вот код теста:

use warnings;
use strict;
use Memoize;
use Benchmark qw/cmpthese/;

my $n = 8000; # Elements in target array
my $count = 24000; # Test iterations.

my @a = ( 1 .. $n );
my @find = map { int(rand($n)) } 0 .. $count;
my ( $orx, $ormx, $opx, $opmx ) = ( 0, 0, 0, 0 );

memoize( 'orig_memo' );
memoize( 'opt_memo'  );

cmpthese( $count, {
    original  => sub{ my $ret =  original( $find[ $orx++  ],  @a ); },
    orig_memo => sub{ my $ret = orig_memo( $find[ $ormx++ ],  @a ); },
    optimized => sub{ my $ret = optimized( $find[ $opx++  ], \@a ); },
    opt_memo  => sub{ my $ret =  opt_memo( $find[ $opmx++ ], \@a ); }
} );

sub original {
    my ( $var, @a) = @_;
    foreach my $e ( @a ) {
        return 1 if $var == $e;
    }
    return 0;
}

sub orig_memo {
    my ( $var, @a ) = @_;
    foreach my $e ( @a ) {
        return 1 if $var == $e;
    }
    return 0;
}

sub optimized {
    my( $var, $aref ) = @_;
    foreach my $e ( @{$aref} ) {
        return 1 if $var == $e;
    }
    return 0;
}

sub opt_memo {
    my( $var, $aref ) = @_;
    foreach my $e ( @{$aref} ) {
        return 1 if $var == $e;
    }
    return 0;
}

А вот и результаты:

             Rate orig_memo  original optimized  opt_memo
orig_memo   876/s        --      -10%      -83%      -94%
original    972/s       11%        --      -82%      -94%
optimized  5298/s      505%      445%        --      -66%
opt_memo  15385/s     1657%     1483%      190%        --

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

Но как только мы передадим ссылку на массив вместо копии, мы уберем затраты на передачу всего массива. Ваша скорость прыгает значительно. Но явным победителем является оптимизированная (то есть передаваемая ссылка на массив) версия, которую мы запомнили (кэшировали), на 1483% быстрее, чем ваша исходная функция. С запоминанием поиск O (n) происходит только при первой проверке заданного параметра. Последующие поиски происходят за O (1) раз.

Теперь вам нужно решить (с помощью бенчмаркинга), поможет ли вам запоминание. Конечно, передача массива ref делает. И если запоминание не поможет вам, возможно, лучше использовать метод хэширования Брайана. Но с точки зрения отсутствия необходимости переписывать большой объем кода, памятная записка в сочетании с передачей ссылки на массив может быть отличной альтернативой.

2 голосов
/ 04 августа 2011

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

sub array_exists {
  my $val = shift;
  return grep { $val eq $_ } @_;
}

Удивительно, но по скорости он не слишком далеко от List :: MoreUtils 'any().Это быстрее, если ваш элемент находится в конце списка примерно на 25% и медленнее примерно на 50%, если ваш элемент находится в начале списка.

Вы также можете вставить его, если необходимо - не нужнозасунуть это в подпрограмму.т.е.

if ( grep { $needle eq $_ } @haystack ) {
  ### Do something
  ...
}
2 голосов
/ 04 августа 2011

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

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

Конечно, в первую очередь нужно учитывать штраф за сортировку массива, что является довольно медленной операцией (n log n ).Если содержимое массива, с которым вы сопоставляете, часто изменяется, вы должны сортировать после каждого изменения, и оно становится действительно медленным.Если после первоначальной сортировки содержимое остается прежним, бинарный поиск оказывается на практически быстрее.

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