【问题标题】:Perl for loop for multiple rangesPerl for 多个范围的循环
【发布时间】:2014-06-24 10:06:54
【问题描述】:

在 for 循环中设置范围计数器的最佳方法是什么?我有一个制表符分隔输入文件,其中前 2 列很重要。我想找到在一系列 Pos 值内出现的分数的最小值和最大值。所以对于示例输入文件:

Pos     Score
1       5
2       17
9       80
38      22
40      11
7       0
302     19
85      33
12      51
293     1
5       19
61      8
71      15

如果存在,我需要计算每个范围的最小和最大分数。

1-29 (min=?, max=?)
30-59 (min=?, max=?)
60-89 (min=?, max=?)

预期结果:

1-29 (min=0, max=80)
30-59 (min=11, max=22)
60-89 (min=8, max=33)
290-219 (min=1, max=19)

还有另一个与此相关的线程,但他们只计算设定范围内的出现次数。我的尝试是设置一个 for 循环:

use List::MoreUtils qw( minmax );
my %inputhash;
my %storehash;

open (FF,$inputfile) || die "Cannot open file $inputfile";
while(<FF>) {
    next if $. < 2; #use to trim off first line if there is a header
    my ($Pos, $Score)  = split;
    $inputhash{$Pos} = $Score;
}


for (my $x=1; $x<1600; $x+29) #set to 1600 for now
{
    my $low = $x;
    my $high = $x+29;
    foreach my $i ($low...$high)
    {
        if (exists $inputhash{$i})
        {
            my $score = $inputhash{$i};
            push (@{$storehash{$high}}, $score);
        }
    }
} 

foreach my $range (sort {$a <=> $b} keys %storehash)
{
    my ($minrange, $maxrange) = minmax @{$storehash{$range}};
    print "$range: $minrange, $maxrange\n";
}

有没有更好的方法来处理这个问题?这个当前的实现给了我一个错误:在 void context 中无用地使用加法 (+)。

【问题讨论】:

  • 也许您想将$x+29 更改为$x+=29
  • @perreal :给定范围可能是$x+=30,并从my $x = 0;开始
  • 更改为 $x+=30 似乎可以解决错误消息。谢谢你。这似乎有效。然而,我想知道是否有人会以不同的、也许更优雅的方式实现多个范围的计数器。

标签: perl for-loop


【解决方案1】:

如果您将数据推送到数组中,而不是哈希:

$inputarray[$Pos] = $Score;

您可以在数组切片上使用minmax(在去除任何未定义的值之后):

my ($min, $max) = minmax grep {defined} @inputarray[0..3];

例如

#!/usr/bin/perl
use strict;
use warnings;

use List::MoreUtils qw(minmax);
use List::Util qw(min);

my @inputarray;
<DATA>;
while (<DATA>) {
    my ($pos, $score) = split;
    $inputarray[$pos] = $score;
}

for (my $i = 1; $i < @inputarray; $i += 29) {
    my $end = min($i + 29, $#inputarray); # Don't overrun the end of the array.
    my ($min, $max) = minmax grep {defined} @inputarray[$i..$end];
    print "$i-$end (min=$min,max=$max)\n" if defined $min;
}

__DATA__
Pos     Score
1       5
2       17
9       80
38      22
40      11
7       0
302     19
85      33
12      51
293     1
5       19
61      8
71      15

输出:

1-30 (min=0,max=80)
30-59 (min=11,max=22)
59-88 (min=8,max=33)
291-302 (min=1,max=19)

【讨论】:

  • 好的,谢谢。我以前使用过 minmax 和数组。但是,我在使用哈希进行其他数据操作时有点卡住了。
【解决方案2】:
use strict;
use warnings;

use List::Util qw(max min);

my $step = 30;  # group into 30 item ...
my @bins;       # ... bins

<DATA>;         # skip line
while (<DATA>) {
  my ($p, $s) = split;
  push @{$bins[$p / $step]}, $s; 
}

for (my $i = 0; $i < @bins; $i++) {
    next if not $bins[$i];
    printf("%d, %d  (min %d, max %d)\n", 
        $i * $step, ($i + 1) * $step, 
        min(@{$bins[$i]}), max(@{$bins[$i]}));
}

__DATA__
Pos     Score
1       5
2       17
9       80
38      22
40      11
7       0
302     19
85      33
12      51
293     1
5       19
61      8
71      15

输出

0, 30  (min 0, max 80)
30, 60  (min 11, max 22)
60, 90  (min 8, max 33)
270, 300  (min 1, max 1)
300, 330  (min 19, max 19)

【讨论】:

    【解决方案3】:

    错误信息

    Useless use of addition (+) in void context
    

    应该提醒您注意 for 循环的最后一个子句是 $x+29 而不是 $x += 29。除此之外,您在范围上有简单的边界错误

    如果您的范围宽度都相同大小,那么最简单的方法是通过简单的除法计算每个位置的范围,并为每个范围建立一个分数列表。每个范围内的最小值和最大值可以事后确定

    这个解决方案使用一个常量WIDTH来确定每个范围的大小;在这种情况下是 30

    use strict;
    use warnings;
    use autodie;
    
    use List::MoreUtils 'minmax';
    use constant WIDTH => 30;
    
    <>; # lose the header
    
    my @buckets;
    while (<>) {
      my ($pos, $score) = split;
      push @{ $buckets[$pos / WIDTH] }, $score;
    }
    
    for my $i (0 .. $#buckets) {
      next unless my $contents = $buckets[$i];
      my $start = $i * WIDTH;
      printf "%d-%d (min=%d, max=%d)\n",
          $start, $start + WIDTH - 1,
          minmax @$contents;
    }
    

    输出

    0-29 (min=0, max=80)
    30-59 (min=11, max=22)
    60-89 (min=8, max=33)
    270-299 (min=1, max=1)
    300-329 (min=19, max=19)
    

    【讨论】:

      【解决方案4】:

      使用命令行,

      perl -ane'
        /\d/ or next;
        $i = int($F[0] /30);
        (!defined or $_ >$F[1]) and $_ = $F[1] for $r[$i]{m};
        (!defined or $_ <$F[1]) and $_ = $F[1] for $r[$i]{M};
        }{
        printf("%d-%d (min=%d, max=%d)\n", $_*30, $_*30+29, $r[$_]{m}, $r[$_]{M})
          for grep $r[$_], 0 .. $#r;
      ' file
      

      输出

      0-29 (min=0, max=80)
      30-59 (min=11, max=22)
      60-89 (min=8, max=33)
      270-299 (min=1, max=1)
      300-329 (min=19, max=19)
      

      脚本等效于命令行版本,

      my @r;
      while (<>) {
        /\d/ or next;
        my @F = split;
        my $i = int($F[0] /30);
        # min topicalizer, refer to $r[$i]{m} as $_
        for ($r[$i]{m}) {
          $_ = $F[1] if !defined or $_ >$F[1];
        }
        # max topicalizer
        for ($r[$i]{M}) {
          $_ = $F[1] if !defined or $_ <$F[1];
        }
      }
      
      for (grep $r[$_], 0 .. $#r) {
        printf("%d-%d (min=%d, max=%d)\n", $_*30, $_*30+29, $r[$_]{m}, $r[$_]{M});
      }
      

      【讨论】:

      • 没有投反对票,但我没有投赞成票,因为这里的任何人都很难学习。一些解释会有很长的路要走。
      猜你喜欢
      • 2020-01-19
      • 2017-02-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多