【问题标题】:Idiomatic way to slice a Perl array by value rather than index按值而不是索引对 Perl 数组进行切片的惯用方法
【发布时间】:2021-12-26 08:48:21
【问题描述】:

我有一段代码从两个大的排序整数数组之一中提取一个切片,代表程序工作流程中的停止点。我将在此处包括两者之一。

基本想法是,我试图从这个大数组中切出一个工作范围,作为该程序运行的起点。 $min是从任务对象中拉出来的,代表任务的当前进度。 $limit 是可选的用户覆盖,默认为 -1(被忽略)。

目前,我正在使用来自List::MoreUtils CPAN 模块的firstidx 函数来检索开始和结束的索引,然后我使用它们以通常的方式对@steps 数组进行切片。有没有更快和/或更惯用的方法来做到这一点?特别是,有没有直接使用$min$limit 值的好方法($limit == -1 案例的另一个代码路径)?

代码如下:

my @steps = (
    0, 1, 5, 10,
    20, 30, 40, 50, 60, 70, 80, 90, 100,
    200, 300, 400, 500, 600, 700, 800, 900, 1000,
    1200, 1400, 1600, 1800, 2000,
    2500, 3000, 3500, 4000, 4500, 5000,
    6000, 7000, 8000, 9000, 10000,
    11000, 12000, 13000, 14000, 15000, 17500, 20000,
    25000, 30000, 35000, 40000, 45000, 50000,
    60000, 70000, 80000, 90000, 100000
);
my $min_index = firstidx { $_ > $min } @steps;
my $max_index;
if ($limit == -1) {
    $max_index = @steps - 1;
} else {
    $max_index = firstidx { $_ >= $limit } @steps;
}
my @steps_todo = @steps[ $min_index .. $max_index ];

【问题讨论】:

  • 您确定开始/结束条件吗? $min 为 5 时从 10 开始。$limit 为 31 时从 40 结束?
  • @clamp 是的,没错。 @steps_todo 是一个素筛程序的最大 p 值列表,前提是我在每一步检查因子去除率以确定是否值得继续。由于已经达到$min 步骤(当前进度),因此它被跳过。它被认为对我的工作流程来说太混乱了,不允许自定义步骤(如 31),所以它向上取整。

标签: arrays perl slice


【解决方案1】:

不一定是惯用的或高性能的,但您可以构造一个迭代器:

#!/usr/bin/env perl

use strict;
use warnings;

use feature 'say';
use List::MoreUtils qw( firstidx );

sub work_steps {
    my ($steps, $min, $max) = @_;

    my $start = firstidx { $_ > $min } @$steps;
    return sub {} if $start == -1;

    my $end;

    if ($max != -1) {
        $end = firstidx { $_ >= $max } @$steps;
        return sub {} if $end == -1;
    }
    else {
        $end = $#$steps;
    }

    return sub {} if $start > $end;

    my $k = $start;

    return sub {
        return unless ($k >= $start) && ($k <= $end);
        return $steps->[$k++];
    }
}

sub main {
    my @steps = (
        0, 1, 5, 10,
        20, 30, 40, 50, 60, 70, 80, 90, 100,
        200, 300, 400, 500, 600, 700, 800, 900, 1000,
        1200, 1400, 1600, 1800, 2000,
        2500, 3000, 3500, 4000, 4500, 5000,
        6000, 7000, 8000, 9000, 10000,
        11000, 12000, 13000, 14000, 15000, 17500, 20000,
        25000, 30000, 35000, 40000, 45000, 50000,
        60000, 70000, 80000, 90000, 100000
    );

    my $it = work_steps(\@steps, 222, 9300);

    while (my $step = $it->()) {
        say $step;
    }
}

main();

这避免了额外数组的构建,从而减少了内存压力。如果您正在处理的列表足够大,这可能很重要。

您可以传递迭代器而不是范围。这可能会使程序更易于阅读。

另一方面,每次迭代完成的额外工作也很重要。

【讨论】:

    【解决方案2】:

    您可以通过一个简单的 foreach 循环来做到这一点。流控制关键字控制何时开始或停止使用 @steps 数组。

    我将无限制情况更改为定义检查,因为这样更有效。

    这不是最有效的算法,但它是一种简单的解决方法。

    use diagnostics; # Very verbose warnings
    ($min, $max, @out) = (1000, 9999);
    
    @steps = (
        0, 1, 5, 10,
        20, 30, 40, 50, 60, 70, 80, 90, 100,
        200, 300, 400, 500, 600, 700, 800, 900, 1000,
        1200, 1400, 1600, 1800, 2000,
        2500, 3000, 3500, 4000, 4500, 5000,
        6000, 7000, 8000, 9000, 10000,
        11000, 12000, 13000, 14000, 15000, 17500, 20000,
        25000, 30000, 35000, 40000, 45000, 50000,
        60000, 70000, 80000, 90000, 100000
    );
    
    
    foreach (@steps) {
        next if $_ < $min;
        last if defined $max and $_ > $max;
        push @out, $_;
    }
    
    
    print "@out";
    
    ## output
    ## 1000 1200 1400 1600 1800 2000 2500 3000 3500 4000 4500 5000 6000 7000 8000 9000
    
    
    

    这也是触发器运算符.. 的一个可能用例。它看起来像列表范围运算符,但在标量上下文中它是触发器。可以把它想象成cmp 三态运算符,但用于任意表达式。它返回一个值,该值定义了从“this”到“that”的范围。称它为挡板。看看这个样本。有关更多信息,请参阅perlop Range Operator 部分。操作数可以是任何返回布尔值的东西。

    use Data::Dump qw/pp/;
    ($min, $max) = (1000, 9999);
    foreach (@steps) {
        my $x = ($_ >= $min .. $_ < $max);
        printf "%-9s %s\n", $_, pp $x;
    }
    
    ## Output
    0         ""
    1         ""
    5         ""
    10        ""
    20        ""
    30        ""
    40        ""
    50        ""
    60        ""
    70        ""
    80        ""
    90        ""
    100       ""
    200       ""
    300       ""
    400       ""
    500       ""
    600       ""
    700       ""
    800       ""
    900       ""
    1000      "1E0"
    1200      "1E0"
    1400      "1E0"
    1600      "1E0"
    1800      "1E0"
    2000      "1E0"
    2500      "1E0"
    3000      "1E0"
    3500      "1E0"
    4000      "1E0"
    4500      "1E0"
    5000      "1E0"
    6000      "1E0"
    7000      "1E0"
    8000      "1E0"
    9000      "1E0"
    10000     1
    11000     2
    12000     3
    13000     4
    14000     5
    15000     6
    17500     7
    20000     8
    25000     9
    30000     10
    35000     11
    40000     12
    45000     13
    50000     14
    60000     15
    70000     16
    80000     17
    90000     18
    100000    19
    
    
    

    请注意返回“1E0”的值,因为这些值会触发触发器中的复位。在一个真正的循环中,过渡点是它终止的地方。您可以使用参数来选择获得“”、“1E0”或序列号的一组值并采取相应的行动。

    此版本将序列号赋予想要的值,然后开始“E0”重置值超出。

    ($_ >= $min .. $_ > $max);
    
    ...
    800       ""
    900       ""
    1000      1
    1200      2
    ...
    8000      15
    9000      16
    10000     "17E0"
    11000     "1E0"
    12000     "1E0"
    ...
    
    

    以下是使用触发器解决问题的方法。这种方式的优点是一旦达到下限条件,就不再评估它。主要的缺点是它非常神秘,非 Perl 程序员可能无法理解。但这是尽可能紧凑和高效的。

    foreach (@steps) {
        ($_ >= $min .. ($_ > $max and last)) or next;
        push @out, $_;
    }
    
    print "@out\n";
    ## 1000 1200 1400 1600 1800 2000 2500 3000 3500 4000 4500 5000 6000 7000 8000 9000
    
    
    

    当左挡板为假时,整个表达式为假,我们进入下一项。当左挡板变为真时,整个表达式现在为真,我们继续并开始检查右挡板。虽然右挡板为假(整个表达式仍然为真),但我们继续将项目保存在@out 中。当右挡板变为真时,我们终止循环。 (这也是它重置为 false 的地方。)

    再修改一次。 :)

    如果没有最大值,这是一个很好的方法。

    
    for (my $i = 0; $i <= $#steps; $i++) {
        next unless $steps[$i] >= $min;
        @out = @steps[$i .. $#steps];
        # Or, if you don't need the original array anymore you can consume it 
        # and be a bit more efficient and save memory
        # @out = splice @steps, $i;
        last;
    
    }
    
    

    好问题!

    【讨论】:

    • 您的输出与预期不符。根据1000, 9999 输入的问题,输出应以1200 开头并以10000 结尾。在您的第一个示例中,将 push 向上移动一行。
    • @clamp 啊,我没有意识到这一点。此更改将获得您的值。 foreach (@steps) { (($_ &gt; $min ) .. ($_ &gt;= $max and (push(@out, $_), last))) or next; push @out, $_;} print "@out\n"; --> 1200 1400 1600 1800 2000 2500 3000 3500 4000 4500 5000 6000 7000 8000 9000 10000
    【解决方案3】:

    一种惯用的方法是使用 grep 来选择范围。这样做的缺点是它会扫描整个数组,如果数组很大并且经常执行 grep,这可能是一个性能点。

    由于列表已排序,性能的一种可能性(但肯定不会更短或更简单)是使用 List::MoreUtils 中的二进制搜索函数来查找范围的边界。

    【讨论】:

      猜你喜欢
      • 2015-04-12
      • 1970-01-01
      • 1970-01-01
      • 2020-02-27
      • 2015-05-21
      • 1970-01-01
      • 1970-01-01
      • 2018-11-07
      • 1970-01-01
      相关资源
      最近更新 更多