我有一个固定大小的数组,且数组大小总是3的倍数。
my @array = ('foo', 'bar', 'qux', 'foo1', 'bar', 'qux2', 3, 4, 5);
如何将数组成员聚类,使得我们可以获得一个每3个一组的二维数组:
$VAR = [ ['foo','bar','qux'],
['foo1','bar','qux2'],
[3, 4, 5] ];
我有一个固定大小的数组,且数组大小总是3的倍数。
my @array = ('foo', 'bar', 'qux', 'foo1', 'bar', 'qux2', 3, 4, 5);
如何将数组成员聚类,使得我们可以获得一个每3个一组的二维数组:
$VAR = [ ['foo','bar','qux'],
['foo1','bar','qux2'],
[3, 4, 5] ];
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;
}
}
@array.rotor(3,0)
。 - Brad Gilbert.rotor
的设计已经改变,现在你只需要写成@array.rotor(3)
。如果你需要在列表不能均匀分割时获取最后几个元素,你可以在方法调用中添加:partial
。 - Brad Gilbert我非常喜欢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中的匿名子程序,但我将其更改为命名子程序,因为代码的外观给我留下了微妙的困扰。没有真正好的理由,只是直觉反应。我不知道两种方法是否都可以。
group_by
接受一个代码引用作为第一个参数,这样我们就可以确定如何处理我们的分组呢?用法:group_by { [ @_ ] } 3, @array;
- Chris Lutzgroup_by 3 { [ @_ ] } @array;
,但当然我们需要显式声明匿名的 sub
,以免 Perl 抱怨。 - Chris Lutzmap { code } @list
语法仅在子例程原型为第一个参数是代码引用时才起作用。如所写,您需要明确指定代码块是一个 sub
(或在其他位置声明 sub 并传递一个引用)。此外,我不会费心为 _copy_to_ref()
编写命名子例程,而只是说 my $code = sub { [ @_ ] };
但那只是我的想法,你的方式可能更有效率。 - Chris Lutznatatime
提供了非常有限的(并且明显不是现代Perl)API。没有链接,没有简单的迭代计数等功能。 - Davor Cubranicmy $VAR;
while( my @list = splice( @array, 0, 3 ) ) {
push @$VAR, \@list;
}
另一个答案(在Tore的基础上进行变化,使用splice而不是while循环,更偏向于Perl-y map)
my $result = [ map { [splice(@array, 0, 3)] } (1 .. (scalar(@array) + 2) % 3) ];
map()
就称它更像 Perl - 它实际上更加混乱和难以理解。最“Perl”的解决方案是 natatime()
,因为它来自 CPAN。 - Chris Lutz试试这个:
$VAR = [map $_ % 3 == 0 ? ([ $array[$_], $array[$_ + 1], $array[$_ + 2] ])
: (),
0..$#array];
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
。
另一种通用的解决方案,不会破坏原始数组:
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
]
];
请在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
]
];
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 ];
}
splice
等其他解决方案将产生长度为2的两个数组和长度为0的一个数组。