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

Dear monks,

I'm getting myself a bit confused. I have three arrays of values, all I want to do is get a list of those values that appear in any two of the arrays (not all three).

1) array1: 1,2,5,9,11,12,13 2) array2: 1,2,5,11,12,13 3) array3: 1,2,5,7,9,12,13
Therefore, get numbers 9 and 11 only as they appear in two of the arrays.

I have so far maganged to find those that appear in all arrays but am not sure how to adapt my code to get those that appear in two arrays only.

Hope someone can advise me!

my %union1; my %isect1; + # FIND THINGS THAT ARE IN BOTH ARRAY1 AND AND ARRAY2 foreach my $e (@array1) { $union1{$e}=1} foreach my $g (@array2) { if ($union1{$g}) { $isect1{$g}=1} $union1{$g}=1; } + my @isect1=keys %isect1; # FIND THINGS THAT ARE IN ALL THREE ARRAYS my %union2; my %isect2; + + foreach my $e (@isect1) { $union2{$e}=1} foreach my $g (@array3) { if ($union2{$g}) { $isect2{$g}=1} $union2{$g}=1; } + # @ISECT2 CONTAINS COMMON VALUES IN ALL THREE ARRAYS + my @isect2=keys %isect2;

Replies are listed 'Best First'.
Re: array comparison question
by Fletch (Bishop) on Aug 23, 2005 at 13:48 UTC
    my %present; for my $a ( \@array1, \@array2, \@array3 ) { $present{ $_ }++ for @{ $ +a } } my @only_two = grep $present{ $_ } == 2, keys %present;

    --
    We're looking for people in ATL

      If we go with the assumption that an element never occurs more than once in a single array, we don't need to track the arrays seperately, and can use Perl's array flattening:

      my %present; for my $a ( @array1, @array2, @array3 ) { $present{ $a }++ } my @only_two = grep $present{ $_ } == 2, keys %present;

      If we have to deal with multiples in any of the arrays:

      use List::MoreUtils qw(uniq); my %present = (); for my $a ( uniq(@array1), uniq(@array2), uniq(@array3) ) { $present{ +$a }++ } my @only_two = grep $present{ $_ } == 2, keys %present;
      This was my first thought too, it only works if you don't have the same value in any of the arrays more than once.
      my @array1 = (1,2,2); # 2 would be in the final array my @array2 = (1); my @array3 = (1);
      ---
      my name's not Keith, and I'm not reasonable.
        Modifying Fletch answer a little:
        my @array1 =( 1, 3, 6, 8); my @array2 =( 1, 2, 3, 4, 4); my @array3 =( 1, 2, 3, 5, 6); my %present; my @arrays = ( \@array1, \@array2, \@array3); for my $i ( 0 .. @arrays - 1) { $present{ $_ }{ $i } = 1 for @{ $arrays[ $i ] }; } my @only_two = grep { 2 == scalar keys %{$present{ $_ }} } keys %pre +sent; print "@only_two\n";

        OK, then how about removing duplicates before you check the arrays?

Re: array comparison question
by Util (Priest) on Aug 23, 2005 at 19:20 UTC

    No one has mentioned the CPAN modules that help solve this sort of problem: List::Compare, Set::Scalar, and Set::Array. If "elements in exactly 2 lists" was the only 'set' operation in a program, I would use jhourcle's second solution. If more than one 'set' operation is needed, the modules start to really pay off.

    Set::Scalar has a flexible syntax, allowing for a dense mix of overloaded operators and chained methods. It can solve the OP problem quite tersely:

    use strict; use warnings; my @array1 = ( 1, 2, 5, 9, 11, 12, 13 ); my @array2 = ( 1, 2, 5, 11, 12, 13 ); my @array3 = ( 1, 2, 5, 7, 9, 12, 13 ); use Set::Scalar; my $x = Set::Scalar->new(@array1); my $y = Set::Scalar->new(@array2); my $z = Set::Scalar->new(@array3); # Either way works. #my $answer = $x->union($y,$z) # - $x->unique($y,$z) # - $x->intersection($y,$z); my $answer = ($x + $y + $z) - ($x / $y / $z) - ($x * $y * $z); my @only_two = $answer->elements();

    List::Compare is better for a generalized form of the OP problem (find elements occurring exactly N times in X lists). In addition to the expected set functions (get_intersection, get_symmetric_difference, etc.), it provides are_members_which, which builds a hash like this:

    { 1 => [ 0, 1, 2 ], 2 => [ 0, 1, 2 ], 5 => [ 0, 1, 2 ], 7 => [ 2 ], 9 => [ 0, 2 ], 11 => [ 0, 1 ], 12 => [ 0, 1, 2 ], 13 => [ 0, 1, 2 ], }
    The number of elements in each of those value arrays is equal to the number of lists where key is found.
    use List::Compare; sub elements_found_in_exactly_n_lists { my ($n, @array_refs) = @_; my $lc = List::Compare->new(@array_refs); my @u = $lc->get_union(); my %h = %{ $lc->are_members_which(\@u) }; return grep { @{$h{$_}} == $n } keys %h; } my @only_two = elements_found_in_exactly_n_lists( 2, \@array1, \@array2, \@array3, );

Re: array comparison question
by inman (Curate) on Aug 23, 2005 at 14:48 UTC
    Map each array into a hash and test for the existence of the value.
    my %array1 = map {$_ => $_}(1,2,5,9,11,12,13); my %array2 = map {$_ => $_}(1,2,5,11,12,13); my %array3 = map {$_ => $_}(1,2,5,7,9,12,13); my %all = map {$_=>$_} (keys %array1, keys %array2, keys %array3); foreach (sort {$a <=> $b} keys %all) { if (exists($array1{$_}) + exists($array2{$_}) + exists($array3{$_} +) == 2) { print "$_\n"; } }
Re: array comparison question
by Anonymous Monk on Aug 23, 2005 at 14:58 UTC
    Assuming the arrays are called @a1, @a2, @a3, the following ought to work (although I didn't test it). It should work correctly even if a value appears twice in a single array. This technique is trivially adaptable for different number of arrays, or different counts.
    my @twice = do { my %a; map {my $a = $_; map {$a{$_}{$a}++} @$a} \@a1, \@a2, \@a3; grep {2 == keys %{$a{$_}}} keys %a; };
      it's a good solution, but I hate to see 'map' used instead of a 'for' unnecessarily, why make perl go to the trouble of building a return array if you're just going to ignore it? .
      my @twice = do { my %tracker; foreach my $a_ref (\@a1, \@a2, \@a3) { foreach my $a_value (@$a_ref) { $tracker{$a_value}{$a_ref} = undef; } } grep {2 == keys %{$tracker{$_}}} keys %tracker; };
      ---
      my name's not Keith, and I'm not reasonable.

        So long as you're using 5.8.1 or newer map in a void context won't build a return list. See perldoc perl581delta in the section "Miscellaneous Enhancements".

        (Having said that, stylistically I'm with you. :)

        --
        We're looking for people in ATL

Re: array comparison question
by injunjoel (Priest) on Aug 23, 2005 at 18:14 UTC
    Greetings all,
    Update
    After re-reading the OP I have modified the following code to only identify those elements that are not unique to only one list and are not found in all lists.
    This has the benefit of not hard-coding in '2' as a comparison since this logic may need to be extended to more lists in the future.
    my @a1 = qw(1 2 5 9 11 12 13); my @a2 = qw(1 2 5 11 12 13); my @a3 = qw(1 2 5 7 9 12 13); my @shared = shared(\@a1, \@a2, \@a3); sub shared { my $lim = scalar(@_); my @lists = map{@{$_}}@_; return do{my %seen; $seen{$_}++ for(@lists); map{delete $seen{$_} if($seen{$_} == 1 || $seen{$_} == $lim)}k +eys %seen; sort {$a <=> $b} keys %seen; }; }

    Output is
    9 11


    -InjunJoel
    "I do not feel obliged to believe that the same God who endowed us with sense, reason and intellect has intended us to forego their use." -Galileo
Re: array comparison question
by sh1tn (Priest) on Aug 23, 2005 at 23:19 UTC
    @_1 = (1,2,5,9,11,12,13); @_2 = (1,2,5,11,12,13); @_3 = (1,2,5,7,9,12,13); map{ $u{$_}++ }@_1,@_2,@_3; map{ $u{$_} != 2 and delete $u{$_} }keys %u; print+(join ',', keys %u), $/