检查数组中元素的更快方法?

时间:2011-08-04 13:15:12

标签: arrays perl

此功能与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 个答案:

答案 0 :(得分:12)

如果必须在固定数组上执行此操作,请改为使用哈希:

 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

由于智能匹配是递归的,因此如果继续进入数据结构。我在Rethinking smart matching中写了一些内容。

答案 1 :(得分:8)

您可以使用Perl 5.10及更高版本中提供的smart matching

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

这应该比函数调用快得多。

答案 2 :(得分:7)

我会使用List::MoreUtils::any

my $ret = any { $_ eq 'b' } @a;

答案 3 :(得分:4)

由于StackOverflow上有很多类似的问题,其中有不同的正确答案"返回不同的结果,我试着比较它们。这个问题似乎是分享我的小基准的好地方。

对于我的测试,我使用了长度为10的1,000个元素(字符串)的测试集(@test_set),其中只有一个元素($search_value)与给定的字符串匹配。

我采用以下语句来验证这个元素在100,000转的循环中是否存在。

<强> _grep

grep( $_ eq $search_value, @test_set )

<强> _hash

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

<强> _hash_premapped

$mapping->{ $search_value }
  • 使用预先计算为$mapping的{​​{1}}(包含在最终测量中)

_ <强>正则表达式

$mapping = { map { $_ => 1 } @test_set }

_ <强> regex_prejoined

sub{ my $rx = join "|", map quotemeta, @test_set; $search_value =~ /^(?:$rx)$/ }
  • 使用预先计算为$search_value =~ /^(?:$rx)$/ 的正则表达式$rx(包含在最终测量中)

<强> _manual_first

$rx = join "|", map quotemeta, @test_set;

<强> _First

sub{ foreach ( @test_set ) { return 1 if( $_ eq $search_value ); } return 0; }
  • 来自first { $_ eq $search_value } @test_set (版本1.38)

<强> _smart

List::Util

<强> _any

$search_value ~~ @test_set
  • 来自any { $_ eq $search_value } @test_set (版本0.33)

在我的机器上(Ubuntu,3.2.0-60-generic,x86_64,Perl v5.14.2)我得到了以下结果。显示的值为秒,由List::MoreUtilsgettimeofday tv_interval(版本1.9726)返回。

元素Time::HiRes位于数组$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~~(甚至可能在测量不准确)。对于较大测试集中间或末尾的项目,第一肯定是最快的。

答案 4 :(得分:3)

brian d foy建议使用散列,这会产生O(1)查找,代价是稍微更昂贵的散列创建。 Marc Jason Dominus在他的书“高阶Perl”中描述了一种技术,其中使用散列来记忆(或缓存)给定参数的子结果。因此,例如,如果findit(1000)始终为给定参数返回相同的内容,则无需每次都重新计算结果。该技术在Memoize模块(Perl核心的一部分)中实现。

记忆并不总是一场胜利。有时,memoized包装器的开销大于计算结果的开销。有时,给定参数不可能被多次检查或相对较少次检查。有时,无法保证给定参数的函数结果总是相同(即缓存可能变得陈旧)。但是如果你有一个昂贵的函数,每个参数的返回值都很稳定,那么memoization就是一个很大的胜利。

就像brian d foy的回答使用哈希一样,Memoize在内部使用哈希。 Memoize实现中还有额外的开销,但使用Memoize的好处是它不需要重构原始子例程。您只需use Memoize;然后memoize( 'expensive_function' );,前提是它符合从备忘录中受益的条件。

我使用了原始子程序并将其转换为整数(仅为了简化测试)。然后我添加了第二个版本,它传递了对原始数组的引用,而不是复制数组。有了这两个版本,我创建了两个我记忆的潜艇。然后我对四个潜艇进行了基准测试。

在基准测试中,我不得不做出一些决定。首先,要测试多少次迭代。我们测试的迭代次数越多,我们就越有可能为memoized版本提供良好的缓存命中率。然后我还必须决定将多少项放入样本数组中。项目越多,缓存命中的可能性就越小,但缓存命中发生时的节省越多。我最终决定要搜索包含8000个元素的数组,并选择搜索24000次迭代。这意味着平均每个memoized调用应该有两个缓存命中。 (使用给定参数的第一次调用将写入缓存,而第二次和第三次调用将从缓存中读取,因此平均有两次良好的命中。)

这是测试代码:

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%        --

正如您所看到的,原始函数的memoized版本实际上更慢。这是因为原始子程序的大部分成本花费在制作8000元素数组的副本上,再加上memoized版本还有额外的调用堆栈和簿记开销。

但是一旦我们传递一个数组引用而不是一个副本,我们就消除了传递整个数组的费用。你的速度大幅提升。但明显的赢家是我们记忆(缓存)的优化(即传递数组引用)版本,比原始函数快1483%。通过memoization,O(n)查找仅在第一次检查给定参数时发生。后续查找在O(1)时间内进行。

现在你必须决定(通过Benchmarking)memoization是否对你有所帮助。当然传递一个数组引用。如果memoization对你没有帮助,也许brian的哈希方法是最好的。但是,在不必重写大量代码方面,memoization与传递数组ref相结合可能是一个很好的选择。

答案 5 :(得分:2)

您当前的解决方案会在找到要查找的元素之前遍历数组。因此,它是线性算法。

如果您首先使用关系运算符(>表示数字元素,gt表示字符串)对数组进行排序,则可以使用binary search来查找元素。它是对数算法,比线性快得多。

当然,首先必须考虑对数组进行排序的惩罚,这是一个相当慢的操作( n log n )。如果您要匹配的数组的内容经常更改,则必须在每次更改后进行排序,并且它变得非常慢。如果在您最初对它们进行排序后内容保持不变,则二进制搜索最终会实际上更快。

答案 6 :(得分:2)

您可以使用grep:

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

令人惊讶的是,它与List :: MoreUtils'any()的速度并没有太大差距。如果您的商品位于列表的最后,如果您的商品位于列表的开头,则该商品在列表末尾的速度提高约25%,速度提高约50%。

如果需要,您也可以内联它 - 无需将其推送到子程序中。即。

if ( grep { $needle eq $_ } @haystack ) {
  ### Do something
  ...
}