tags:

views:

17582

answers:

12

Let's say I have an array, and I know I'm going to be doing a lot of "Does the array contain X?" checks. The efficient way to do this is to turn that array into a hash, where the keys are the array's elements, and then you can just say

if($hash{X}) { ... }

Is there an easy way to do this array-to-hash conversion? Ideally, it should be versatile enough to take an anonymous array and return an anonymous hash.

+20  A: 
%hash = map { $_ => 1 } @array;

It's not as short as the "@hash{@array} = ..." solutions, but those ones require the hash and array to already be defined somewhere else, whereas this one can take an anonymous array and return an anonymous hash.

What this does is take each element in the array and pair it up with a "1". When this list of (key, 1, key, 1, key 1) pairs get assigned to a hash, the odd-numbered ones become the hash's keys, and the even-numbered ones become the respective values.

raldi
+9  A: 
@hash{@keys} = undef;

The syntax here where you are referring to the hash with an @ is a hash slice. We're basically saying $hash{$keys[0]} AND $hash{$keys[1]} AND $hash{$keys[2]} ... is a list on the left hand side of the =, an lvalue, and we're assigning to that list, which actually goes into the hash and sets the values for all the named keys. In this case, I only specified one value, so that value goes into $hash{$keys[0]}, and the other hash entries all auto-vivify (come to life) with undefined values. [My original suggestion here was set the expression = 1, which would've set that one key to 1 and the others to undef. I changed it for consistency, but as we'll see below, the exact values do not matter.]

When you realize that the lvalue, the expression on the left hand side of the =, is a list built out of the hash, then it'll start to make some sense why we're using that @. [Except I think this will change in Perl 6.]

The idea here is that you are using the hash as a set. What matters is not the value I am assigning; it's just the existence of the keys. So what you want to do is not something like: if ($hash{$key} == 1) # then key is in the hash instead: if (exists $hash{$key}) # then key is in the set

It's actually more efficient to just run an exists check than to bother with the value in the hash, although to me the important thing here is just the concept that you are representing a set just with the keys of the hash. Also, somebody pointed out that by using undef as the value here, we will consume less storage space than we would assigning a value. (And also generate less confusion, as the value does not matter, and my solution would assign a value only to the first element in the hash and leave the others undef, and some other solutions are turning cartwheels to build an array of values to go into the hash; completely wasted effort).

skiphoppy
This one is preferable over the other because it doesn't make a temporary list to initialize the hash. This should be faster and consume less memory.
Leon Timmermans
This does not work when tested:test.pl: my @arr = ("foo","bar","baz"); my @hash{@arr} = 1;syntax error at test.pl line 2, near "@hash{"
Frosty
Frosty: You have to declare "my %hash" first, then do "@hash{@arr} = 1" (no "my").
Michael Carman
This only sets $hash{$keys[0]} = 1. The other hash values are undef. See @hash{@array} = (1) x @array instead xdg (0 seconds ago)
xdg
`= ()`, not `= undef`, just for consistency in implicitly using undef for all the values, not just all after the first. (As demonstrated in these comments, it's too easy to see the `undef` and think it can just be changed to 1 and affect all the hash values.)
ysth
As the values end up as "undef" here (and probably for not quite the reason you think - as ysth has pointed out) you can't just use the hash in code like "if ($hash{$value})". You'd need "if (exists $hash{$value})".
davorg
I would've never used it with any check other than if (exists). It's just a set to me.
skiphoppy
It'd be nice if you edited your answer to point out that it needs to be used with exists, that exists is more efficient than checking truthiness by actually loading the hash value, and that undef takes less space than 1.
BRH
If you want to use an anonymous array you can `@hash{@{[ ... ]}} = undef;`.
Brad Gilbert
A: 

Raldi's solution can be tightened up to this (the '=>' from the original is not necessary):

my %hash = map { $_,1 } @array;

This technique can also be used for turning text lists into hashes:

my %hash = map { $_,1 } split(",",$line)

Additionally if you have a line of values like this: "foo=1,bar=2,baz=3" you can do this:

my %hash = map { split("=",$_) } split(",",$line);

[EDIT to include]


Another solution offered (which takes two lines) is:

my %hash;
#The values in %hash can only be accessed by doing exists($hash{$key})
#The assignment only works with '= undef;' and will not work properly with '= 1;'
#if you do '= 1;' only the hash key of $array[0] will be set to 1;
@hash{@array} = undef;
Frosty
The different between $_ => 1 and $_,1 is purely stylistic. Personally I prefer => as it seems to indicate the key/value link more explicitly. Your @hash{@array} = 1 solution doesn't work. Only one of the values (the one associated with the first key in @array) gets set to 1.
davorg
Thank you for the clarification. I have edited the answer.
Frosty
+15  A: 
 @hash{@array} = (1) x @array;

It's a hash slice, a list of values from the hash, so it gets the list-y @ in front.

From the docs:

If you're confused about why you use an '@' there on a hash slice instead of a '%', think of it like this. The type of bracket (square or curly) governs whether it's an array or a hash being looked at. On the other hand, the leading symbol ('$' or '@') on the array or hash indicates whether you are getting back a singular value (a scalar) or a plural one (a list).

moritz
Wow, I never heard of (or thought of) that one. Thanks!I'm having trouble understanding how it works. Can you add an explanation? In particular, how can you take a hash named %hash and refer to it with an @ sign?
raldi
raldi: it's a hash slice, a list of values from the hash, so it gets the list-y @ in front. See http://perldoc.perl.org/perldata.html#Slices - particularly the last paragraph of the section
ysth
You should add that to your answer!
raldi
Could you explain the RHS as well? Thanks.
Bart J
(list) x $number replicates the list $number times. Using an array in scalar context returns the number of elements, so (1) x @array is a list of 1s with the same length as @array.
moritz
+2  A: 

You could also use Perl6::Junction.

use Perl6::Junction qw'any';

my @arr = ( 1, 2, 3 );

if( any(@arr) == 1 ){ ... }
Brad Gilbert
If doing it multiple times for a large array, that's potentially going to be a lot slower.
ysth
Actually doing it once is a lot slower. it has to create an object. Then shortly after, it will destroy that object. This is just an example of what is possible.
Brad Gilbert
+5  A: 

In perl 5.10, there's the close-to-magic ~~ operator:

sub invite_in {
    my $vampires = [ qw(Angel Darla Spike Drusilla) ];
    return ($_[0] ~~ $vampires) ? 0 : 1 ;
}

See here: http://dev.perl.org/perl5/news/2007/perl-5.10.0.html

RET
If doing it multiple times for a large array, that's potentially going to be a lot slower.
ysth
It'd the "smart match operator" :)
brian d foy
+6  A: 

Note that if typing if ( exists $hash{ key } ) isn’t too much work for you (which I prefer to use since the matter of interest is really the presence of a key rather than the truthiness of its value), then you can use the short and sweet

@hash{@key} = ();
Aristotle Pagaltzis
+3  A: 

There is a presupposition here, that the most efficient way to do a lot of "Does the array contain X?" checks is to convert the array to a hash. Efficiency depends on the scarce resource, often time but sometimes space and sometimes programmer effort. You are at least doubling the memory consumed by keeping a list and a hash of the the list around simultaneously. Plus you're writing more original code that you'll need to test, document, etc.

As an alternative, look at the List::MoreUtils module, specifically the functions any(), none(), true() and false(). They all take a block as the conditional and a list as the argument, similar to map() and grep():

print "At least one value undefined" if any { !defined($_) } @list;

I ran a quick test, loading in half of /usr/share/dict/words to an array (25000 words), then looking for eleven words selected from across the whole dictionary (every 5000th word) in the array, using both the array-to-hash method and the any() function from List::MoreUtils.

On Perl 5.8.8 built from source, the array-to-hash method runs almost 1100x faster than the any() method (1300x faster under Ubuntu 6.06's packaged Perl 5.8.7.)

That's not the full story however - the array-to-hash conversion takes about 0.04 seconds which in this case kills the time efficiency of array-to-hash method to 1.5x-2x faster than the any() method. Still good, but not nearly as stellar.

My gut feeling is that the array-to-hash method is going to beat any() in most cases, but I'd feel a whole lot better if I had some more solid metrics (lots of test cases, decent statistical analyses, maybe some big-O algorithmic analysis of each method, etc.) Depending on your needs, List::MoreUtils may be a better solution; it's certainly more flexible and requires less coding. Remember, premature optimization is a sin... :)

arclight
List::MoreUtils is a great tip, thanks.
SquareCog
A: 

You might also want to check out Tie::IxHash, which implements ordered associative arrays. That would allow you to do both types of lookups (hash and index) on one copy of your data.

+1  A: 

If you do a lot of set theoretic operations - you can also use Set::Scalar or similar module. Then $s = Set::Scalar->new( @array ) will build the Set for you - and you can query it with: $s->contains($m).

zby
+1  A: 

I always thought that

foreach my $item (@array) { $hash{$item} = 1 }

was at least nice and readable / maintainable.

Keith
A: 

You can place the code into a subroutine, if you don't want pollute your namespace.

my $hash_ref =
  sub{
    my %hash;
    @hash{ @{[ qw'one two three' ]} } = undef;
    return \%hash;
  }->();

Or even better:

sub keylist(@){
  my %hash;
  @hash{@_} = undef;
  return \%hash;
}

my $hash_ref = keylist qw'one two three';

# or

my @key_list = qw'one two three';
my $hash_ref = keylist @key_list;

If you really wanted to pass an array reference:

sub keylist(\@){
  my %hash;
  @hash{ @{$_[0]} } = undef if @_;
  return \%hash;
}

my @key_list = qw'one two three';
my $hash_ref = keylist @key_list;
Brad Gilbert