【问题标题】:How can I partition a Perl array into equal sized chunks?如何将 Perl 数组划分为大小相等的块?
【发布时间】:2020-10-01 23:00:32
【问题描述】:

我有一个固定大小的数组,其中数组的大小始终是 3 倍。

my @array = ('foo', 'bar', 'qux', 'foo1', 'bar', 'qux2', 3, 4, 5);

如何对数组的成员进行聚类,以便我们可以得到 数组按 3 分组的数组:

$VAR = [ ['foo','bar','qux'],
         ['foo1','bar','qux2'],
         [3, 4, 5] ];

【问题讨论】:

  • 当心,下面所有基于拼接的选项都会破坏您的阵列。如果您想保留原始数组,则需要处理副本。
  • 这是一个非常重要的说明:拼接。补充:natatime也是使用splice实现的,所以以上面的说明为准。

标签: perl


【解决方案1】:
my @VAR;
push @VAR, [ splice @array, 0, 3 ] while @array;

或者您可以使用来自List::MoreUtilsnatatime

use List::MoreUtils qw(natatime);

my @VAR;
{
  my $iter = natatime 3, @array;
  while( my @tmp = $iter->() ){
    push @VAR, \@tmp;
  }
}

【讨论】:

  • @Brad - List::MoreUtils +1 - 即使在这个答案之外,它也是一个很棒的宝石。
  • 另外,请注意 - 至少在 2/2009 年 - 在 XS 版本的 natatime 中存在内存泄漏(在 PP 版本中没有泄漏)。见perlmonks.org/?node_id=742364
  • 在 Perl 6 中你可以写成@array.rotor(3,0)
  • Perl 6 中.rotor 的设计已更改,您只需将其写为@array.rotor(3)。如果您需要在列表未均匀拆分时获取最后几个元素,您可以在方法调用中添加 :partial
【解决方案2】:

我真的很喜欢 List::MoreUtils 并且经常使用它。但是,我从不喜欢natatime 函数。它不会产生可用于 for 循环或 mapgrep 的输出。

我喜欢在我的代码中链接 map/grep/apply 操作。一旦您了解了这些功能的工作原理,它们就会非常富有表现力并且非常强大。

但很容易让函数像 natatime 一样返回数组引用列表。

sub group_by ($@) {
    my $n     = shift;
    my @array = @_;

    croak "group_by count argument must be a non-zero positive integer"
        unless $n > 0 and int($n) == $n;

    my @groups;
    push @groups, [ splice @array, 0, $n ] while @array;

    return @groups;
}

现在你可以这样做了:

my @grouped = map [ reverse @$_ ],
              group_by 3, @array;

** 更新 Chris Lutz 的建议 **

克里斯,我可以看到您建议在界面中添加代码参考的优点。这样就内置了类似地图的行为。

# equivalent to my map/group_by above
group_by { [ reverse @_ ] } 3, @array;

这很好,简洁。但是为了保持良好的 {} 代码引用语义,我们将 count 参数 3 放在一个难以看到的位置。

我认为我更喜欢我最初写的东西。

链式地图并不比我们使用扩展 API 获得的详细得多。 使用原始方法,无需重新实现即可使用 grep 或其他类似函数。

例如,如果将代码引用添加到 API,那么您必须这样做:

my @result = group_by { $_[0] =~ /foo/ ? [@_] : () } 3, @array;

得到等价于:

my @result = grep $_->[0] =~ /foo/,
             group_by 3, @array;

因为我建议这个是为了方便链接,所以我更喜欢原来的。

当然,允许任何一种形式都很容易:

sub _copy_to_ref { [ @_ ] }

sub group_by ($@) {
    my $code = \&_copy_to_ref;
    my $n = shift;

    if( reftype $n eq 'CODE' ) {
        $code = $n;
        $n = shift;
    }

    my @array = @_;

    croak "group_by count argument must be a non-zero positive integer"
        unless $n > 0 and int($n) == $n;

    my @groups;
    push @groups, $code->(splice @array, 0, $n) while @array;

    return @groups;
}

现在任何一种形式都应该可以工作(未经测试)。我不确定我更喜欢原始 API,还是更喜欢具有内置地图功能的 API。

有人想吗?

** 再次更新**

Chris 指出可选的代码参考版本会迫使用户这样做是正确的:

group_by sub { foo }, 3, @array;

这不太好,违反了预期。由于没有办法拥有灵活的原型(据我所知),因此将 kibosh 放在扩展 API 上,我会坚持使用原始原型。

顺便说一句,我从备用 API 中的匿名 sub 开始,但我将其更改为命名 sub,因为我对代码的外观感到微妙的困扰。没有真正的充分理由,只是直觉反应。我不知道这是否重要。

【讨论】:

  • 为什么不让group_by 将代码引用作为第一个参数,以便我们确定如何处理我们的组?用法:group_by { [ @_ ] } 3, @array;
  • 理想的语法是group_by 3 { [ @_ ] } @array;,但当然我们需要显式声明匿名sub,这样Perl才不会发牢骚。
  • 使用可选代码引用的第二个版本的唯一问题是map { code } @list 语法只有在子例程原型化为第一个参数是代码引用时才有效。如所写,您需要明确指定代码块是sub(或在其他地方声明子并传递对它的引用)。此外,我不会费心为_copy_to_ref() 编写命名子例程,只是说my $code = sub { [ @_ ] };,但这只是我。按照自己的方式做可能会更有效。
  • 我同意natatime 提供非常有限的(并且绝对不是ModernPerl)API。没有链接,没有简单的计数迭代等。
【解决方案3】:

或者这个:

my $VAR;
while( my @list = splice( @array, 0, 3 ) ) {
    push @$VAR, \@list;
}

【讨论】:

    【解决方案4】:

    另一个答案(Tore 的一个变体,使用拼接但避免使用 while 循环以支持更多 Perl-y 映射)

    my $result = [ map { [splice(@array, 0, 3)] } (1 .. (scalar(@array) + 2) % 3) ];
    

    【讨论】:

    • 我不会仅仅因为它使用map() 而称它为 Perl-y - 它在很大程度上更加混乱且难以理解。最“Perl-y”的解决方案是natatime(),因为它来自 CPAN。
    • 嗯...我不能说我非常不同意你的观点:可能更难理解。但是多年来一直是一名专业的 Perl 开发人员,我在 CPAN 上遇到了足够多的糟糕到可怕的垃圾,我不一定认为“使用 CPAN 中的东西”是 perl 解决方案的良好家庭批准印章。请注意,List::MoreUtils,从我今天的粗略检查来看,它似乎是一个非常整洁和有用的模块,所以它绝对不包括在上面的抱怨中:)
    • @DVK - 当我说“因为它来自 CPAN”时,我是在深情地取笑我最喜欢的语言的趋势,而不是把它作为最终的解决方案。我们真的需要找到一种在互联网上表达讽刺的方式。
    • 对不起。经过 2 个不眠之夜,我的讽刺模块没有加载。
    【解决方案5】:

    试试这个:

    $VAR = [map $_ % 3 == 0 ? ([ $array[$_], $array[$_ + 1], $array[$_ + 2] ]) 
                            : (),
                0..$#array];
    

    【讨论】:

    • 我不确定是 +1 是为了可爱还是 -1 是为了纯粹的 hackiness :) 未投票它会保留。
    • -1,因为我会 100% 在某处犯错 :)
    【解决方案6】:

    另一种通用解决方案,对原始数组无损:

    use Data::Dumper;
    
    sub partition {
        my ($arr, $N) = @_; 
    
        my @res;
        my $i = 0;
    
        while ($i + $N-1 <= $#$arr) {
            push @res, [@$arr[$i .. $i+$N-1]];
            $i += $N; 
        }   
    
        if ($i <= $#$arr) {
            push @res, [@$arr[$i .. $#$arr]];
        }   
        return \@res;
    }
    
    print Dumper partition(
        ['foo', 'bar', 'qux', 'foo1', 'bar', 'qux2', 3, 4, 5], 
        3   
    );
    

    输出:

    $VAR1 = [
              [
                'foo',
                'bar',
                'qux'
              ],
              [
                'foo1',
                'bar',
                'qux2'
              ],
              [
                3,
                4,
                5
              ]
            ];
    

    【讨论】:

      【解决方案7】:

      作为学习经验,我决定在 Perl6 中进行此操作

      我尝试的第一个,也许是最简单的方法是使用map

      my @output := @array.map: -> $a, $b?, $c? { [ $a, $b // Nil, $c // Nil ] };
      .say for @output;
      
      foo bar qux
      foo1 bar qux2
      3 4 5
      

      这似乎不太可扩展。如果我想一次 take 列表中的 10 个项目怎么办,那写起来会很烦人。 ...嗯,我刚刚提到了"take",并且有一个名为take 的关键字让我们在子例程中尝试一下,使其更普遍有用。

      sub at-a-time ( Iterable \sequence, Int $n where $_ > 0 = 1 ){
        my $is-lazy = sequence.is-lazy;
        my \iterator = sequence.iterator;
      
        # gather is used with take
        gather loop {
          my Mu @current;
          my \result = iterator.push-exactly(@current,$n);
      
          # put it into the sequence, and yield
          take @current.List;
      
          last if result =:= IterationEnd;
        }.lazy-if($is-lazy)
      }
      

      为了好玩,让我们针对斐波那契数列的无限列表尝试一下

      my $fib = (1, 1, *+* ... *);
      my @output = at-a-time( $fib, 3 );
      .say for @output[^5]; # just print out the first 5
      
      (1 1 2)
      (3 5 8)
      (13 21 34)
      (55 89 144)
      (233 377 610)
      

      请注意,我使用了$fib 而不是@fib。这是为了防止 Perl6 缓存斐波那契数列的元素。
      将其放入子例程中以在每次需要时创建一个新序列可能是个好主意,这样当您完成这些值时,它们就会被垃圾收集。
      如果输入序列是,我还使用.is-lazy.lazy-if 将输出序列标记为惰性。由于它要进入数组@output,它会尝试从无限列表中生成所有元素,然后再继续下一行。


      等一下,我刚想起.rotor

      my @output = $fib.rotor(3);
      
      .say for @output[^5]; # just print out the first 5
      
      (1 1 2)
      (3 5 8)
      (13 21 34)
      (55 89 144)
      (233 377 610)
      

      .rotor 实际上比我演示的要强大得多。

      如果您希望它在最后返回部分匹配,则需要添加:partial to the arguments of .rotor

      【讨论】:

        【解决方案8】:

        在 CPAN 上使用 List::NSect 包中的 spart 函数。

            perl -e '
            use List::NSect qw{spart};
            use Data::Dumper qw{Dumper};
            my @array = ("foo", "bar", "qux", "foo1", "bar", "qux2", 3, 4, 5);
            my $var = spart(3, @array);
            print Dumper $var;
            '
        
            $VAR1 = [
                  [
                    'foo',
                    'bar',
                    'qux'
                  ],
                  [
                    'foo1',
                    'bar',
                    'qux2'
                  ],
                  [
                    3,
                    4,
                    5
                  ]
                ];
        

        【讨论】:

          【解决方案9】:

          下面是一个更通用的问题解决方案:

          my @array = ('foo', 'bar', 1, 2);
          my $n = 3;
          my @VAR = map { [] } 1..$n;
          my @idx = sort map { $_ % $n } 0..$#array;
          
          for my $i ( 0..$#array ){
                  push @VAR[ $idx[ $i ] ], @array[ $i ];
          }
          

          当数组中的项目数不是 3 时,这也有效。 在上面的例子中,其他解决方案例如splice 会产生两个长度为 2 的数组和一个长度为 0 的数组。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2023-03-03
            • 1970-01-01
            • 1970-01-01
            • 2023-01-07
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多