如何将Perl数组分成相等大小的块?

17

我有一个固定大小的数组,且数组大小总是3的倍数。

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

如何将数组成员聚类,使得我们可以获得一个每3个一组的二维数组:

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

2
小心,下面所有基于splice的选项都会破坏你的数组。如果你想保留原始数组,你需要在副本上操作。 - daotoad
1
这是关于splice的非常重要的注释。补充说明:natatime也使用splice实现,因此受上述注释的影响。 - DVK
9个回答

34
my @VAR;
push @VAR, [ splice @array, 0, 3 ] while @array;

或者你可以使用List::MoreUtils中的natatime

use List::MoreUtils qw(natatime);

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

@Brad - 对于List::MoreUtils,我也给予+1的支持 - 它是一个非常棒的宝石,即使不在这个答案之外。 - DVK
另外,请注意,至少在2009年2月时,natatime的XS版本存在内存泄漏问题(PP版本没有泄漏)。请参阅http://www.perlmonks.org/?node_id=742364。 - DVK
在 Perl 6 中,您可以将其编写为 @array.rotor(3,0) - Brad Gilbert
在Perl 6中,.rotor的设计已经改变,现在你只需要写成@array.rotor(3)。如果你需要在列表不能均匀分割时获取最后几个元素,你可以在方法调用中添加:partial - Brad Gilbert

6

我非常喜欢List::MoreUtils,并经常使用它。然而,我从来不喜欢natatime函数。它不能产生可用于for循环、map或grep的输出。

我喜欢在我的代码中链接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的建议的更新**

Chris,我可以看出您提出的在接口中添加代码引用的好处。这样就内置了一种类似地图的行为。

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

这段代码很简洁明了。但是为了保持{}代码引用语义的美观,我们将计数参数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更好。

大家有什么想法?

** 再次更新 **

克里斯指出可选的代码引用版本会强制用户执行:

group_by sub { foo }, 3, @array;

虽然这样做并不好,而且违反了期望。由于我不知道如何使用灵活的原型,这就否决了扩展API,并且我会坚持使用原始API。

另外一件事,我最初使用了替代API中的匿名子程序,但我将其更改为命名子程序,因为代码的外观给我留下了微妙的困扰。没有真正好的理由,只是直觉反应。我不知道两种方法是否都可以。


2
为什么不让 group_by 接受一个代码引用作为第一个参数,这样我们就可以确定如何处理我们的分组呢?用法:group_by { [ @_ ] } 3, @array; - Chris Lutz
1
理想的语法应该是 group_by 3 { [ @_ ] } @array;,但当然我们需要显式声明匿名的 sub,以免 Perl 抱怨。 - Chris Lutz
使用可选代码引用的第二个版本唯一的问题是,map { code } @list 语法仅在子例程原型为第一个参数是代码引用时才起作用。如所写,您需要明确指定代码块是一个 sub(或在其他位置声明 sub 并传递一个引用)。此外,我不会费心为 _copy_to_ref() 编写命名子例程,而只是说 my $code = sub { [ @_ ] }; 但那只是我的想法,你的方式可能更有效率。 - Chris Lutz
我同意natatime提供了非常有限的(并且明显不是现代Perl)API。没有链接,没有简单的迭代计数等功能。 - Davor Cubranic

5
或者这样:
my $VAR;
while( my @list = splice( @array, 0, 3 ) ) {
    push @$VAR, \@list;
}

5

另一个答案(在Tore的基础上进行变化,使用splice而不是while循环,更偏向于Perl-y map)

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

2
我不会仅仅因为使用了 map() 就称它更像 Perl - 它实际上更加混乱和难以理解。最“Perl”的解决方案是 natatime(),因为它来自 CPAN。 - Chris Lutz
2
嗯...我不能说我完全不同意你的看法,可能更难理解。但是作为一名多年的专业Perl开发人员,我遇到了足够多的糟糕到可怕的CPAN垃圾,我并不一定认为“使用来自CPAN的东西”就是perl解决方案的好印章。请注意,从今天我粗略的检查来看,List::MoreUtils似乎是一个非常整洁和有用的模块,因此它绝对不包括在上述抱怨中 :) - DVK
1
@DVK - 当我说“因为它来自CPAN”时,我是在爱抚我最喜欢的语言的趋势,而不是将其作为解决方案的全部。我们真的需要找到一种在互联网上表达讽刺的方式。 - Chris Lutz
2
抱歉。经过两个不眠之夜,我的讽刺模块无法加载。 - DVK

4

试试这个:

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

我不确定是因为可爱还是因为太过糟糕而将其+1或-1 :) 如果没有投票,它就保持不投票状态。 - DVK
把以下与编程有关的内容从英语翻译成中文。仅返回已翻译的文本:-1,因为我肯定会在其中犯错误 :) - Karel Bílek

3
作为一次学习体验,我决定使用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

这似乎不太具有可扩展性。如果我想每次从列表中10个项目,那么编写起来会非常烦人。... 嗯,我刚刚提到了"取",而且有一个名为的关键字,让我们尝试将其放在子例程中,使其更加通用。

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 实际上比我展示的要强大得多。

如果你想让它在末尾返回部分匹配,你需要在 .rotor 的参数中添加 :partial


3

另一种通用的解决方案,不会破坏原始数组:

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
          ]
        ];

3

请在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
          ]
        ];

1
以下是一个更通用的解决方案,针对这个问题:
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的一个数组。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接