【问题标题】:Faster way to check for element in array?更快的方法来检查数组中的元素?
【发布时间】:2011-10-20 00:03:46
【问题描述】:

此函数的作用与 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;
}

【问题讨论】:

标签: arrays perl


【解决方案1】:

如果您必须在固定数组上执行很多操作,请改用哈希:

 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

由于智能匹配是递归的,if 会一直深入到数据结构中。我在Rethinking smart matching 中写过其中的一些内容。

【讨论】:

  • 那是婴儿和洗澡水。我认为保持 smartmatch 和添加叠加应该允许在简单情况下安全简单地使用,否则显式 $item ~~ any(@array)
  • 如果你要使用any(),你不需要智能匹配。它对你没有任何帮助。
  • @brian d foy:为什么要从 Perl 中删除功能?您是否知道出于此原因不应使用的功能列表?
  • 我们删除功能是因为它们已损坏。这已经发生了好几次了。我还没有列出完整的清单。
  • @brian d foy : Perl 如何处理在以前的版本中删除了某个特性的情况?我的脚本是否仍会运行,但会使用 use warnings 提供弃用消息?
【解决方案2】:

您可以使用 smart matching,在 Perl 5.10 及更高版本中可用:

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

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

【讨论】:

  • 查看我的答案,了解其中的陷阱。我现在的建议是永远不要使用智能匹配运算符。 blogs.perl.org/users/brian_d_foy/2011/07/…
  • @brian:除了简单标量的一维数组,我从不使用~~,无论如何这似乎是安全的。
  • 您认为您永远不会以任何其他方式使用它,直到某些错误插入您不期望的数据。即便如此,您现在每次使用 ~~ 时都必须解释所有这些边缘情况。不使用它会更容易,然后解释所有问题,这样您就不会烧毁下一个程序员,或者检查您的数组以确保它没有引用(这意味着遍历整个数组)。
  • @brian:我会说这同样适用于each()while(<>)m//o 等。你用过这些吗?
  • 你可能会说同样适用于那些,但我挑战你证明这一点。
【解决方案3】:

我会使用List::MoreUtils::any

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

【讨论】:

  • 在检查 List::MoreUtils::any 的源代码时,我认为该解决方案与 OP 已有的解决方案几乎相同。
  • 谢谢@brian,我删除了那部分答案。
  • @TLP,不同之处在于 any 方法看起来更干净,而且他并没有重新发明稳定且经过测试的东西。此外,List::MoreUtils 有一个可以安装的 XS 版本,应该更快。
  • @gpojd 仍然是一个子程序调用,比简单的操作符慢
  • @gpo 可能是这样,但性能是问题所在。我认为最好不要只采取一揽子解决方案而不进行检查。
【解决方案4】:

由于 StackOverflow 上有很多类似的问题,不同的“正确答案”会返回不同的结果,因此我尝试对它们进行比较。这个问题似乎是分享我的小基准的好地方。

对于我的测试,我使用了一个包含 1000 个长度为 10 的元素(字符串)的测试集 (@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 = { map { $_ => 1 } @test_set }$mapping(包含在最终测量中)

_正则表达式

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)我得到了以下结果。显示的值为秒,由Time::HiResgettimeofdaytv_interval(版本1.9726)返回。

元素$search_value位于数组@test_set中的位置0

_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位于数组@test_set中的位置500

_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位于数组@test_set中的第999位

_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 绝对是最快的。

【讨论】:

    【解决方案5】:

    brian d foy 建议使用散列,它提供 O(1) 查找,但代价是创建散列的成本略高。 Marc Jason Dominus 在他的《Higher Order Perl》一书中描述了一种技术,其中使用哈希来记忆(或缓存)给定参数的子结果。例如,如果findit(1000) 总是为给定参数返回相同的结果,则无需每次都重新计算结果。该技术在 Memoize 模块(Perl 核心的一部分)中实现。

    记忆并不总是一种胜利。有时,memoized wrapper 的开销大于计算结果的成本。有时,一个给定的参数不可能被检查超过一次或相对几次。有时不能保证给定参数的函数结果总是相同的(即缓存可能变得陈旧)。但是,如果你有一个昂贵的函数,每个参数的返回值稳定,那么记忆化可能是一个巨大的胜利。

    就像 brian d foy 的答案使用哈希一样,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 确实如此。如果记忆化对您没有帮助,也许布赖恩的哈希方法是最好的。但就不必重写太多代码而言,memoization 结合传递数组 ref 可能是一个很好的选择。

    【讨论】:

    • 是的,我正要谈论 Memoize,但我很懒。在某些情况下,情况可能要好得多,但这些事情取决于具体情况。 +1 :)
    • 顺便说一句,使用 any{...} memoized 比 opt_memo 好,并且在“或缓存”(兽人机动)功能的 9% 范围内(不会从 memoizing 中受益)。我很难测试一种纯哈希方法,因为一步构建哈希似乎并没有很好地影响我的测试。
    【解决方案6】:

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

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

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

    【讨论】:

    • 如果需要重复测试是否存在,我会将数组的元素放在哈希中,而不是对它们进行排序并使用二进制搜索。初始哈希将花费大约。与排序相同,但哈希查找会快很多,因为它是由 perl 直接实现的,而二进制搜索不是。
    【解决方案7】:

    你可以使用grep:

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

    令人惊讶的是,它与 List::MoreUtils' any() 的速度相差不大。如果您的项目位于列表末尾约 25% 会更快,如果您的项目位于列表开头则慢约 50%。

    如果需要,您也可以将其内联——无需将其放入子例程中。即

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

    【讨论】:

    • 为了不被错误值绊倒,只要确保你处于标量上下文中 - 也许return scalar(grep { $val eq $_ } @_) 更好/更安全
    • @user5402 : 这也适用于内联解决方案吗?
    • @Sandra - 不,因为if 总是在标量上下文中评估其参数。
    • @user5402:它可以在列表上下文中没有问题。如果没有匹配项,它将返回一个空列表。例如:perl -E 'say "got something: $_" for grep { $_ == 20 } ( 1 .. 10 )'
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-23
    • 2019-12-14
    • 1970-01-01
    • 1970-01-01
    • 2016-12-10
    • 2021-05-26
    • 2013-07-08
    相关资源
    最近更新 更多