meonkeys has asked for the wisdom of the Perl Monks concerning the following question:

Pop Quiz:

I want to break down the elements of a variable array as follows.
If the array has 24 elements, break it up into three arrays of 8, 8, and 8 elements.
If the array has 25 elements, break it up into three arrays of 9, 8, and 8 elements.
If the array has 26 elements, break it up into three arrays of 9, 9, and 8 elements.


PS- Not homework. I promise. I'll post my own meager solution when I come up with it.

Replies are listed 'Best First'.
Re: Filling buckets
by dws (Chancellor) on Jan 03, 2001 at 07:52 UTC
    Here's a solution for n>0 buckets.
    #/usr/bin/perl -w use strict; my @array = qw(a b c d e f g h i j k l m n o p q r s t u v w x y z); my $nbuckets = 3; # number of buckets to divide @array into my %bucket; # @{$bucket{0 .. $nbuckets - 1}} are the buckets foreach my $n( 0 .. $nbuckets - 1) { foreach my $size (1 .. int(0.9999 + @array /($nbuckets - $n))) { push @{$bucket{$n}}, shift @array; } } foreach my $n (0 .. $nbuckets - 1) { print "bucket $n: ", join(' ', @{$bucket{$n}}), "\n"; }
    yields
    bucket 0: a b c d e f g h i bucket 1: j k l m n o p q r bucket 2: s t u v w x y z
    And thanks for the problem. It gave me an excuse to rethink some ugly old code that populates an HTML table.
Re: Filling buckets
by danger (Priest) on Jan 03, 2001 at 09:17 UTC

    Yet another n-buckets routine:

    #!/usr/bin/perl -w use strict; my @array = ('a' .. 'z'); my @buckets = n_buckets(3, @array); foreach my $aref (@buckets) { print scalar @$aref, ":@$aref\n"; } # n_buckets(n, list) sub n_buckets { my @buckets = ([]) x shift; my @list = @_; my $mod = @list % @buckets; my $inc = int(@list / @buckets); map{ [splice @list, 0, $inc + (--$mod >= 0)] } 0 .. $#buckets; } __END__
      Sexy code there... that is better than what I came up with. =)

      --
      $you = new YOU;
      honk() if $you->love(perl)

Re: Filling buckets
by eg (Friar) on Jan 03, 2001 at 08:38 UTC

    Splitting an array into two or three are special cases where you can simply use stack operations.

    print map { @$_, "\n" } split_into_three( 'a'..($ARGV[0] || 'z') ); sub split_into_three { my @a = @_; my (@b, @c); while (@b < @a) { push(@b, shift(@a)); unshift(@c, pop(@a)); } push(@a, shift(@c)) if (@a < @c); return \@b, \@a, \@c; }
Re: Filling buckets
by tilly (Archbishop) on Jan 03, 2001 at 08:59 UTC
    Do you really care which get 8 and which get 9? The following almost does it:
    sub split_to_n { my $n = shift; my @p = map {(@_)*$_/$n} 0..$n; map {[@_[$p[$_-1]..($p[$_]-1)]]} 1..$n; }
    Indeed try some sample code out like this:
    use Data::Dumper; $Data::Dumper::Indent = 1; print Dumper split_to_n(3, 'a'..'z');
    and it works except that the group of 9 is the last rather than the first. The following less compact version, however, satisfies the given spec perfectly:
    sub split_to_n { my $n = shift; my @p = map {(@_)*$_/$n} 0..$n; @_ = reverse @_; map {[reverse @_[$p[$_-1]..($p[$_]-1)]]} reverse 1..$n; }
    May I submit that the spec you gave is twisted? :-)

    PS This is the kind of code which could benefit from an explanatory comment about the interface. :-)

      #another twist sub split_to_n { my $n = shift; my @p = map {(@_)*$_/$n} -$n..0; map {[@_[$p[$_-1]..($p[$_]-1)]]} 1..$n; }

        io,

        While I am greatly enjoying your terse and good solutions (Even attempting to emulate your approach to answering questions to some extent), I find I do have a request. If possible, please use slightly more verbosity in your variables. It improves the readability of the code for us less fluent, and allows less expirienced monks like myself to focus on your construction more closely.

        coreolyn -- Working to have perl code flying off his fingers instead of spelling mistakes.

Re: Filling buckets
by Fastolfe (Vicar) on Jan 03, 2001 at 09:15 UTC
    Benchmarked under 5.6 (500_000 iterations, 5-30 seconds), fixed and updated 3 Jan 18:45 CST:
    Rate 2501 tilly2 merlyn dominus dws tilly eg io repson + fastsp1 danger2 fast fast_c 2501 5263/s -- -30% -36% -39% -46% -46% -60% -64% -66% + -68% -70% -73% -89% tilly2 7491/s 42% -- -10% -13% -23% -23% -43% -49% -52% + -55% -57% -61% -84% merlyn 8278/s 57% 11% -- -3% -15% -15% -37% -43% -47% + -50% -52% -57% -83% dominus 8569/s 63% 14% 4% -- -12% -12% -35% -41% -45% + -49% -50% -55% -82% dws 9709/s 84% 30% 17% 13% -- -0% -26% -33% -38% + -42% -44% -49% -80% tilly 9737/s 85% 30% 18% 14% 0% -- -26% -33% -37% + -42% -44% -49% -80% eg 13089/s 149% 75% 58% 53% 35% 34% -- -10% -16% + -22% -24% -32% -73% io 14599/s 177% 95% 76% 70% 50% 50% 12% -- -6% + -13% -15% -24% -69% repson 15552/s 195% 108% 88% 81% 60% 60% 19% 7% -- + -7% -10% -19% -67% fastsp1 16694/s 217% 123% 102% 95% 72% 71% 28% 14% 7% + -- -3% -13% -65% danger2 17271/s 228% 131% 109% 102% 78% 77% 32% 18% 11% + 3% -- -10% -64% fast 19194/s 265% 156% 132% 124% 98% 97% 47% 31% 23% + 15% 11% -- -60% fast_c 47847/s 809% 539% 478% 458% 393% 391% 266% 228% 208% + 187% 177% 149% --
      Good data, Fastolfe! It's fun to look back at the fragments and guess how well they compare against one another, and then have hard data to check your guesses against.

      In this particular case, though, I'll bet a round of virtual drinks that performance isn't an issue, and that the split-into-three-lists routine gets called no more than a handful of times per CGI invocation.

        Yeah I'd say performance probably doesn't really matter.

        If it really isn't an issue then it would be better to rate the solutions on efficiency, maintainability, flexibility and coolness.

        eg's solution may be the fastest in this set of benchmarks, but it doesn't include a variable number of buckets (which may be useful later in a project) and it isn't one the one that I personally (not begin a perl god) can understand and alter the fastest.

        Maybe someone else can come up with a better, and overall rating....

      Fastolfe, could you post your benchmark code? Perhaps I am not using Benchmark.pm correctly, but I do not obtain similar standings.
      thanks.

        It's rather long, but here it is. By all means let me know if I've done something wrong.

        I've removed the code because this node was really long and annoying. It's in the source code of the page if you want it, but it's out of date since others have been added since then.

Re: Filling buckets
by Fastolfe (Vicar) on Jan 03, 2001 at 08:13 UTC
    This was what I came up with after a few minutes. This solution sort of "grew", so I wouldn't be surprised if there was a more elegant way of doing it.
    sub split_into_three { my $first = @_; my $last = int($first / 3); $first -= $last; $first -= int($first / 2); return [ @_[0..$first-1] ], [ @_[$first..$#_-$last] ], [ @_[$#_-$last+1..$#_] ]; } my ($first, $second, $third) = split_into_three(@everything); my @buckets = split_into_three('a'..'z'); print join("\n", map { join(" ", @{$_}) } @buckets), "\n"; # a b c d e f g h i # j k l m n o p q r # s t u v w x y z
      Here's a nicer solution (IMO) using splice:
      sub split_into { my $howmany = shift; my @from = reverse @_; my @buckets; unshift(@buckets, [ reverse splice(@from, 0, @from / $howmany--) ]) + while $howmany; @buckets; }
      Or, dropping @buckets at the expense of readability (and perhaps efficiency):
      sub split_into { my $howmany = shift; my @from = reverse @_; reverse map { [ reverse splice(@from, 0, @from / $_) ] } reverse 1 .. $howmany; }

        ...but that second example just looks good...

Re: Filling buckets
by repson (Chaplain) on Jan 03, 2001 at 08:32 UTC
    Here's what jumped from my fingers...
    use Data::Dumper; my @array = 'a' .. 'w'; my $num = 3; # buckets my $cnt = @array; # total items my $base = int($cnt/$num); # } my $left = $cnt % $num; # } $cnt = ($base * $num) + $left my @buckets; for (1..$num) { push @buckets, [ splice(@array,0,$base + ($left-- > 0 && 1) ) +]; } print Dumper(\@buckets);
Re: Filling buckets
by tilly (Archbishop) on Jan 04, 2001 at 06:13 UTC
    You know, I realized today that you never said anything about order.

    So deal.

    sub split_to_n { my @buckets = map {[]} 1..shift; while (@_) { my $b = shift @buckets; push @$b, shift; push @buckets, $b; } @buckets; }
Re: Filling buckets
by Dominus (Parson) on Jan 04, 2001 at 07:25 UTC
    Has anyone done one like this yet? It's really short, but I didn't see it in the thread.

    sub split_into { my ($n, @from, @to) = @_; while (@from) { push @{$to[$i++]}, shift @from; i %= $n; } @to; } @results = split_into(3, 'a' .. 'z');

    Update: It's a lot like Tilly's last one. But it doesn't have the bad memory behavior. (Ben's has an array that marches downward indefinitely in memory, which is bad.) Mine also has the benefit of not requiring the explicit initialization of the destination array.

Re: Filling buckets
by Fastolfe (Vicar) on Jan 04, 2001 at 07:40 UTC
    OK, I couldn't resist:
    use Inline C => <<'EoF'; void fast_c (int num, ...) { Inline_Stack_Vars; int rnum = num; SV** buckets; int stack_ptr = Inline_Stack_Items - 1; buckets = (SV**)malloc(sizeof(SV*) * num); while (rnum) { int this = stack_ptr / rnum--; AV* bucket = newAV(); while (this--) { av_unshift(bucket, 1); av_store(bucket, 0, Inline_Stack_Item(stack_ptr--)); } buckets[rnum] = newRV_inc((SV*)bucket); } Inline_Stack_Reset; while (rnum < num) { Inline_Stack_Push(sv_2mortal(buckets[rnum++])); } free(buckets); Inline_Stack_Done; Inline_Stack_Return(num); } EoF
Re: Filling buckets
by dmckee (Scribe) on Jan 03, 2001 at 15:27 UTC
    Could you use the fact that the first (elements-24) arrays have nine elements?
    There probably is a neater way... TMTOWTDI.
    -- Dave
Re: Filling buckets
by 2501 (Pilgrim) on Jan 03, 2001 at 22:13 UTC
    these little games are always fun:)
    I like perl munchies:P
    If anyone would like to comment on a neater/more stylish way of handling the toggle, I would appreciate it:)
    sub foo{ my @list = @_; my $toggle = 0; my (@list0,@list1); my $element; while($#list > $#list0 && $#list > $#list1 ){ if($toggle == 2){ $toggle = 0; next; }else{ $element = pop(@list); if($toggle ==1){ push(@list1,$element); }else{ push(@list0,$element); } # end of else toggle == 1 $toggle++; } #end of else toggle == 2 } #end of while return \@list, \@list0, \@list1; } # end of sub;