Comment puis-je vérifier si un tableau Perl contient une valeur particulière?


239

J'essaie de trouver un moyen de vérifier l'existence d'une valeur dans un tableau sans parcourir le tableau.

Je lis un fichier pour un paramètre. J'ai une longue liste de paramètres que je ne veux pas traiter. J'ai placé ces paramètres indésirables dans un tableau @badparams.

Je veux lire un nouveau paramètre et s'il n'existe pas @badparams, le traiter. S'il existe @badparams, passez à la lecture suivante.


3
Pour mémoire, la réponse dépend de votre situation. Il semble que vous souhaitiez effectuer des recherches répétées, il est donc bon d'utiliser un hachage comme le suggère jkramer. Si vous ne souhaitez effectuer qu'une seule recherche, vous pouvez tout aussi bien répéter. (Et dans certains cas, vous voudrez peut-être effectuer une recherche binaire au lieu d'utiliser un hachage!)
Cascabel


6
Pour mémoire (et cela peut être totalement inapplicable à votre situation), il est généralement préférable d'identifier les «bonnes valeurs» et d'ignorer le reste plutôt que d'essayer d'éliminer les «mauvaises valeurs» connues. La question que vous devez vous poser est de savoir s'il est possible qu'il y ait de mauvaises valeurs que vous ne connaissez pas encore.
Grant McLean

Réponses:


187

Transformez simplement le tableau en hachage:

my %params = map { $_ => 1 } @badparams;

if(exists($params{$someparam})) { ... }

Vous pouvez également ajouter d'autres paramètres (uniques) à la liste:

$params{$newparam} = 1;

Et plus tard, récupérez une liste de paramètres (uniques):

@badparams = keys %params;

38
Pour mémoire, ce code parcourt toujours le tableau. L'appel map {} rend simplement cette itération très facile à taper.
Kenny Wyland

3
Je ne ferais cela que si vos valeurs dans @badparams sont pseudo-statiques et que vous prévoyez de faire beaucoup de vérifications par rapport à la carte. Je ne recommanderais pas cela pour un seul chèque.
Aaron T Harris

Cela ne va-t-il pas aller pour un tableau avec plusieurs éléments avec la même valeur?
Rob Wells

3
@RobWells non, cela fonctionnera bien. La prochaine fois qu'il verra la même valeur, il écrasera simplement l'entrée dans le hachage, qui dans ce cas la remettra 1.
andrewrjones

222

Meilleur usage général - Tableaux particulièrement courts (1000 éléments ou moins) et codeurs qui ne savent pas quelles optimisations répondent le mieux à leurs besoins.

# $value can be any regex. be safe
if ( grep( /^$value$/, @array ) ) {
  print "found it";
}

Il a été mentionné que grep passe par toutes les valeurs même si la première valeur du tableau correspond. C'est vrai, cependant grep est toujours extrêmement rapide dans la plupart des cas . Si vous parlez de tableaux courts (moins de 1000 éléments), la plupart des algorithmes seront de toute façon assez rapides. Si vous parlez de tableaux très longs (1 000 000 éléments), grep est suffisamment rapide, que l'élément soit le premier, le milieu ou le dernier du tableau.

Cas d'optimisation pour les baies plus longues:

Si votre tableau est trié , utilisez une "recherche binaire".

Si le même tableau est recherché plusieurs fois à plusieurs reprises, copiez-le d'abord dans un hachage, puis vérifiez le hachage. Si la mémoire est un problème, déplacez chaque élément du tableau dans le hachage. Plus efficace en mémoire mais détruit la baie d'origine.

Si les mêmes valeurs sont recherchées à plusieurs reprises dans le tableau, créez paresseusement un cache. (comme chaque élément est recherché, vérifiez d'abord si le résultat de la recherche a été stocké dans un hachage persistant. si le résultat de la recherche n'est pas trouvé dans le hachage, puis recherchez dans le tableau et placez le résultat dans le hachage persistant de sorte que la prochaine fois nous le trouver dans le hachage et sauter la recherche).

Remarque: ces optimisations ne seront plus rapides que pour les tableaux longs. N'optimisez pas trop.


12
Le double tilde a été introduit en Perl 5.10
Pause jusqu'à nouvel ordre.


5
Évitez le smartmatch dans le code de production. C'est instable / expérimental en attendant un nouvel avis.
Vector Gorgoth

1
Je le trouve aussi plus lisible mais ne pas utiliser dit qu'il n'est pas efficace et vérifie chaque élément même s'il est le premier.
giordano

7
N'utilisez pas if ("value" ~~ @array). ~~ est une fonctionnalité expérimentale appelée Smartmatch. L'expérience semble être considérée comme un échec et sera supprimée ou modifiée dans une future version de Perl.
yahermann

120

Vous pouvez utiliser la fonction smartmatch dans Perl 5.10 comme suit:

Pour la recherche de valeur littérale, faire ci-dessous fera l'affaire.

if ( "value" ~~ @array ) 

Pour la recherche scalaire, faire ci-dessous fonctionnera comme ci-dessus.

if ($val ~~ @array)

Pour le tableau en ligne faisant ci-dessous, fonctionnera comme ci-dessus.

if ( $var ~~ ['bar', 'value', 'foo'] ) 

Dans Perl 5.18, smartmatch est marqué comme expérimental, vous devez donc désactiver les avertissements en activant le pragma expérimental en ajoutant ci-dessous à votre script / module:

use experimental 'smartmatch';

Alternativement, si vous voulez éviter l'utilisation de smartmatch - alors comme Aaron l'a dit, utilisez:

if ( grep( /^$value$/, @array ) ) {
  #TODO:
}

4
C'est bien mais cela semble être nouveau pour Perl 5.10. Il a fallu un certain temps avant de comprendre pourquoi je reçois des erreurs de syntaxe.
Igor Skochinsky

17
Attention: vous voudrez peut-être éviter celui-ci, car l'opérateur a apparemment un comportement différent dans différentes versions et a entre-temps été marqué comme expérimental . Donc, à moins que vous ayez un contrôle total sur votre version de Perl (et qui l'a), vous devriez probablement l'éviter.
Maze

1
J'aime cette explication sur la raison pour laquelle le réglage use experimental 'smartmatch'est recommandé. Comme j'ai le contrôle de ma version Perl (système interne), j'utilise la no warnings 'experimental::smartmatch';déclaration.
lepe

43

Ce billet de blog discute des meilleures réponses à cette question.

En résumé, si vous pouvez installer des modules CPAN, les solutions les plus lisibles sont:

any(@ingredients) eq 'flour';

ou

@ingredients->contains('flour');

Cependant, un idiome plus courant est:

any { $_ eq 'flour' } @ingredients

Mais n'utilisez pas la first()fonction! Il n'exprime pas du tout l'intention de votre code. N'utilisez pas l' ~~opérateur "Smart match": il est cassé. Et n'utilisez grep()ni la solution avec un hachage: ils parcourent toute la liste.

any() s'arrêtera dès qu'il trouvera votre valeur.

Consultez le billet de blog pour plus de détails.


8
tous les besoinsuse List::Util qw(any);. List::Utilest dans les modules Core .
Onlyjob

13

Méthode 1: grep (peut faire attention alors que la valeur devrait être une expression régulière).

Essayez d'éviter d'utiliser grep, si vous regardez les ressources.

if ( grep( /^$value$/, @badparams ) ) {
  print "found";
}

Méthode 2: recherche linéaire

for (@badparams) {
    if ($_ eq $value) {
       print "found";
       last;
    }
}

Méthode 3: utilisez un hachage

my %hash = map {$_ => 1} @badparams;
print "found" if (exists $hash{$value});

Méthode 4: smartmatch

(ajouté en Perl 5.10, marqué est expérimental en Perl 5.18).

use experimental 'smartmatch';  # for perl 5.18
print "found" if ($value ~~ @badparams);

Méthode 5: utilisez le module List::MoreUtils

use List::MoreUtils qw(any);
@badparams = (1,2,3);
$value = 1;
print "found" if any {$_ == $value} @badparams;

12

Le benchmark de @ eakssjo est cassé - mesure la création de hachages en boucle vs la création de regex en boucle. Version fixe (plus j'ai ajouté List::Util::firstet List::MoreUtils::any):

use List::Util qw(first);
use List::MoreUtils qw(any);
use Benchmark;

my @list = ( 1..10_000 );
my $hit = 5_000;
my $hit_regex = qr/^$hit$/; # precompute regex
my %params;
$params{$_} = 1 for @list;  # precompute hash
timethese(
    100_000, {
        'any' => sub {
            die unless ( any { $hit_regex } @list );
        },
        'first' => sub {
            die unless ( first { $hit_regex } @list );
        },
        'grep' => sub {
            die unless ( grep { $hit_regex } @list );
        },
        'hash' => sub {
            die unless ( $params{$hit} );
        },
    });

Et résultat (c'est pour 100_000 itérations, dix fois plus que dans la réponse de @ eakssjo):

Benchmark: timing 100000 iterations of any, first, grep, hash...
       any:  0 wallclock secs ( 0.67 usr +  0.00 sys =  0.67 CPU) @ 149253.73/s (n=100000)
     first:  1 wallclock secs ( 0.63 usr +  0.01 sys =  0.64 CPU) @ 156250.00/s (n=100000)
      grep: 42 wallclock secs (41.95 usr +  0.08 sys = 42.03 CPU) @ 2379.25/s (n=100000)
      hash:  0 wallclock secs ( 0.01 usr +  0.00 sys =  0.01 CPU) @ 10000000.00/s (n=100000)
            (warning: too few iterations for a reliable count)

6
Si vous souhaitez tester plusieurs éléments, la création à l'avance du hachage vous fait gagner du temps. Mais si vous voulez simplement savoir s'il contient un seul élément, vous n'avez pas déjà le hachage. Par conséquent, la création du hachage doit faire partie du temps de calcul. Encore plus pour l'expression régulière: vous avez besoin d'une nouvelle expression rationnelle pour chaque élément que vous recherchez.
fishinear

1
@fishinear True, mais si vous ne vous intéressez qu'à un seul chèque, pas à plusieurs chèques, il est évident que c'est de la microoptimisation pour vous demander quelle méthode est plus rapide car ces microsecondes n'ont pas d'importance. Si vous souhaitez refaire cette vérification, le hachage est le chemin à parcourir, car le coût de création du hachage une fois est suffisamment petit pour être ignoré. Les repères ci-dessus mesurent uniquement différentes méthodes de test, sans aucune configuration. Oui, cela peut être invalide dans votre cas d'utilisation, mais encore une fois - si vous ne faites qu'une seule vérification, vous devez utiliser ce qui est le plus lisible pour vous et vos amis.
Xaerxess

10

Même si elle est pratique à utiliser, il semble que la solution de conversion en hachage coûte beaucoup de performances, ce qui était un problème pour moi.

#!/usr/bin/perl
use Benchmark;
my @list;
for (1..10_000) {
    push @list, $_;
}

timethese(10000, {
  'grep'    => sub {
            if ( grep(/^5000$/o, @list) ) {
                # code
            }
        },
  'hash'    => sub {
            my %params = map { $_ => 1 } @list;
            if ( exists($params{5000}) ) {
                # code
            }
        },
});

Sortie du test de référence:

Benchmark: timing 10000 iterations of grep, hash...
          grep:  8 wallclock secs ( 7.95 usr +  0.00 sys =  7.95 CPU) @ 1257.86/s (n=10000)
          hash: 50 wallclock secs (49.68 usr +  0.01 sys = 49.69 CPU) @ 201.25/s (n=10000)

5
L'utilisation List::Util::firstest plus rapide car elle cesse d'itérer lorsqu'elle trouve une correspondance.
RobEarl

1
-1 Votre benchmark présente des défauts, grepest beaucoup plus lent que la création de hachage et la recherche, car l'ancien est O (n) et le dernier O (1). Il suffit de faire la création de hachage une seule fois (en dehors de la boucle) et de pré-calculer l'expression régulière pour mesurer les méthodes uniquement ( voir ma réponse ).
Xaerxess

4
@Xaerxess: Dans mon cas, je voulais faire une recherche, donc je pense qu'il est juste de compter à la fois la création de hachage / regex et la recherche / grep. Si la tâche consistait à effectuer de nombreuses recherches, je suppose que votre solution est meilleure.
aksel

3
Si vous ne voulez faire qu'une seule itération, la différence est indiscernable entre l'une des méthodes que vous choisissez, donc tout benchmark est faux car c'est une microoptimisation maléfique dans ce cas.
Xaerxess

2
Le regex est compilé une seule fois, car il a le drapeau «o».
Apoc

3

@files est un tableau existant

my @new_values =  grep(/^2[\d].[\d][A-za-z]?/,@files);

print join("\n", @new_values);

print "\n";

/^2[\d diplomate .\\d diplomatiqueA-za-z


2

Vous voulez certainement un hachage ici. Placez les mauvais paramètres comme clés dans le hachage, puis décidez si un paramètre particulier existe dans le hachage.

our %bad_params = map { $_ => 1 } qw(badparam1 badparam2 badparam3)

if ($bad_params{$new_param}) {
  print "That is a bad parameter\n";
}

Si vous êtes vraiment intéressé à le faire avec un tableau, regardez List::UtilouList::MoreUtils


0

Vous pouvez procéder de deux manières. Vous pouvez utiliser le jeter les valeurs dans un hachage pour une table de recherche, comme suggéré par les autres articles. (Je vais ajouter juste un autre idiome.)

my %bad_param_lookup;
@bad_param_lookup{ @bad_params } = ( 1 ) x @bad_params;

Mais s'il s'agit principalement de données de mots et pas trop de méta, vous pouvez les vider dans une alternance d'expressions régulières:

use English qw<$LIST_SEPARATOR>;

my $regex_str = do { 
    local $LIST_SEPARATOR = '|';
    "(?:@bad_params)";
 };

 # $front_delim and $back_delim being any characters that come before and after. 
 my $regex = qr/$front_delim$regex_str$back_delim/;

Cette solution devrait être réglée pour les types de «mauvaises valeurs» que vous recherchez. Et encore une fois, cela pourrait être totalement inapproprié pour certains types de cordes, donc avertissez emptor .


1
Vous pouvez également écrire @bad_param_lookup{@bad_params} = (), mais vous devrez utiliser existspour tester l'adhésion.
Greg Bacon

-1
my @badparams = (1,2,5,7,'a','zzz');

my $badparams = join('|',@badparams);   # '|' or any other character not present in params

foreach my $par (4,5,6,7,'a','z','zzz')
{
    if ($badparams =~ /\b$par\b/)
    {
        print "$par is present\n";
    }
    else
    {
        print "$par is not present\n";
    }
}

Vous pouvez vérifier la cohérence des espaces de début numériques

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.