Come posso verificare se un array Perl contiene un valore particolare?


239

Sto cercando di capire un modo per verificare l'esistenza di un valore in un array senza iterare attraverso l'array.

Sto leggendo un file per un parametro. Ho un lungo elenco di parametri che non voglio affrontare. Ho inserito questi parametri indesiderati in un array @badparams.

Voglio leggere un nuovo parametro e, se non esiste @badparams, elaborarlo. Se esiste in @badparams, vai alla lettura successiva.


3
Per la cronaca, la risposta dipende dalla tua situazione. Sembra che tu voglia fare ricerche ripetute, quindi usare un hash come suggerisce jkramer è buono. Se volessi fare solo una ricerca, potresti anche solo iterare. (E in alcuni casi potresti voler fare una ricerca binaria invece di usare un hash!)
Cascabel,


6
Per la cronaca (e questo potrebbe essere totalmente inapplicabile alla tua situazione) è generalmente una migliore idea identificare "buoni valori" e ignorare il resto piuttosto che cercare di eliminare i "cattivi valori" noti. La domanda che devi porre è se è possibile che ci siano alcuni valori negativi che non conosci ancora.
Grant McLean,

Risposte:


187

Trasforma semplicemente l'array in un hash:

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

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

Puoi anche aggiungere altri parametri (univoci) all'elenco:

$params{$newparam} = 1;

E più tardi ottieni un elenco di parametri (unici) indietro:

@badparams = keys %params;

38
Per la cronaca, questo codice continua a scorrere all'interno dell'array. La chiamata map {} semplifica la digitazione dell'iterazione.
Kenny Wyland

3
Lo farei solo se i tuoi valori in @badparams sono pseudo-statici e prevedi di fare molti controlli sulla mappa. Non lo consiglierei per un singolo controllo.
Aaron T Harris,

Non andrà questo botto per un array con più elementi con lo stesso valore?
Rob Wells,

3
@RobWells no, funzionerà benissimo. La prossima volta che vedrà lo stesso valore, sovrascriverà semplicemente la voce nell'hash, che in questo caso lo 1reimposta.
andrewrjones,

222

Migliore scopo generale - In particolare array brevi (1000 articoli o meno) e programmatori che non sono sicuri di quali ottimizzazioni soddisfino meglio le loro esigenze.

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

È stato menzionato che grep passa attraverso tutti i valori anche se il primo valore nella matrice corrisponde. Questo è vero, tuttavia grep è ancora estremamente veloce nella maggior parte dei casi . Se stai parlando di array corti (meno di 1000 articoli), la maggior parte degli algoritmi sarà comunque abbastanza veloce. Se stai parlando di array molto lunghi (1.000.000 di articoli), grep è accettabilmente veloce indipendentemente dal fatto che l'articolo sia il primo o il mezzo o l'ultimo dell'array.

Casi di ottimizzazione per array più lunghi:

Se il tuo array è ordinato , usa una "ricerca binaria".

Se lo stesso array viene ripetutamente cercato più volte, copiarlo prima in un hash e quindi controllare l'hash. Se la memoria è un problema, sposta ciascun elemento dall'array nell'hash. Più memoria efficiente, ma distrugge l'array originale.

Se gli stessi valori vengono cercati ripetutamente all'interno dell'array, creare pigramente una cache. (mentre ogni elemento viene cercato, controlla prima se il risultato della ricerca è stato memorizzato in un hash persistente. Se il risultato della ricerca non viene trovato nell'hash, quindi cerca l'array e inserisci il risultato nell'hash persistente in modo che la prossima volta trovalo nell'hash e salta la ricerca).

Nota: queste ottimizzazioni saranno più veloci solo quando si gestiscono array lunghi. Non ottimizzare eccessivamente.


12
La doppia tilde è stata introdotta in Perl 5.10
Sospesa fino a nuovo avviso.

15
@DennisWilliamson ... e in 5.18 è considerato sperimentale .
Xaerxess,

5
Evita smartmatch nel codice di produzione. È instabile / sperimentale in attesa di ulteriore avviso.
Vector Gorgoth,

1
Lo trovo anche più leggibile ma Non usare dice che non è efficiente e controlla ogni elemento anche se è il primo.
giordano,

7
Non utilizzare if ("value" ~~ @array). ~~ è una funzione sperimentale chiamata Smartmatch. L'esperimento sembra essere considerato un fallimento e verrà rimosso o modificato in una versione futura di Perl.
Yahermann,

120

È possibile utilizzare la funzione smartmatch in Perl 5.10 come segue:

Per la ricerca del valore letterale, fare di seguito farà il trucco.

if ( "value" ~~ @array ) 

Per la ricerca scalare, fare di seguito funzionerà come sopra.

if ($val ~~ @array)

Per la matrice inline che segue, funzionerà come sopra.

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

In Perl 5.18 smartmatch è contrassegnato come sperimentale, pertanto è necessario disattivare gli avvisi attivando il pragma sperimentale aggiungendo di seguito al proprio script / modulo:

use experimental 'smartmatch';

In alternativa, se vuoi evitare l'uso di smartmatch, allora come ha detto Aaron usa:

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

4
Questo è carino ma sembra essere nuovo in Perl 5.10. Ci è voluto un po 'di tempo prima di capire perché sto ricevendo errori di sintassi.
Igor Skochinsky,

17
Attenzione: potresti voler evitare questo, dal momento che l'operatore ha un comportamento apparentemente diverso in diverse versioni e nel frattempo è stato contrassegnato come sperimentale . Quindi, a meno che tu non abbia il pieno controllo della tua versione di Perl (e di chi ce l'ha) probabilmente dovresti evitarla.
Maze

1
Mi piace questa spiegazione sul perché use experimental 'smartmatch'si consiglia l' impostazione . Poiché controllo la mia versione di perl (sistema interno), utilizzo la no warnings 'experimental::smartmatch';dichiarazione.
lepe

43

Questo post sul blog illustra le migliori risposte a questa domanda.

In breve, se è possibile installare i moduli CPAN, le soluzioni più leggibili sono:

any(@ingredients) eq 'flour';

o

@ingredients->contains('flour');

Tuttavia, un linguaggio più comune è:

any { $_ eq 'flour' } @ingredients

Ma per favore non usare la first()funzione! Non esprime affatto l'intento del tuo codice. Non utilizzare l' ~~operatore "Abbinamento intelligente": è rotto. E non usare grep()né la soluzione con un hash: ripetono l'intero elenco.

any() si fermerà non appena trova il tuo valore.

Controlla il post sul blog per maggiori dettagli.


8
qualsiasi esigenzause List::Util qw(any);. List::Utilè nei moduli Core .
Onlyjob,

13

Metodo 1: grep (può fare attenzione mentre il valore dovrebbe essere una regex).

Cerca di evitare di usare grep, se osservi le risorse.

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

Metodo 2: Ricerca lineare

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

Metodo 3: utilizzare un hash

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

Metodo 4: smartmatch

(aggiunto in Perl 5.10, contrassegnato è sperimentale in Perl 5.18).

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

Metodo 5: utilizzare il modulo List::MoreUtils

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

12

Il benchmark di @ eakssjo è rotto - misure per la creazione di hash in loop e la creazione di regex in loop. Versione fissa (più ho aggiunto List::Util::firste 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} );
        },
    });

E risultato (è per 100_000 iterazioni, dieci volte di più rispetto alla risposta di @ 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
Se si desidera verificare più elementi, la creazione anticipata dell'hash consente di risparmiare tempo. Ma se vuoi solo sapere se contiene un singolo elemento, allora non hai già l'hash. Pertanto, la creazione dell'hash dovrebbe far parte del tempo di elaborazione. Ancora di più per l'espressione regolare: hai bisogno di una nuova regexp per ogni elemento che cerchi.
fishinearan

1
@fishinear Vero, ma se sei interessato solo a un controllo, non a più controlli, ovviamente è la microottimizzazione a chiedersi anche quale metodo sia più veloce perché quei microsecondi non contano. Se vuoi ripetere questo controllo, l'hash è la strada da percorrere, perché il costo della creazione dell'hash una volta è abbastanza piccolo da poter essere ignorato. I benchmark sopra misurano solo diversi modi di test, senza includere alcuna configurazione. Sì, questo potrebbe non essere valido nel tuo caso d'uso, ma di nuovo - se stai facendo un solo controllo, dovresti usare tutto ciò che è più leggibile per te e i tuoi compagni.
Xaerxess il

10

Anche se è comodo da usare, sembra che la soluzione di conversione in hash abbia un costo piuttosto elevato, il che è stato un problema per me.

#!/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
            }
        },
});

Uscita del test di riferimento:

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'uso List::Util::firstè più veloce in quanto interrompe l'iterazione quando trova una corrispondenza.
RobEarl,

1
-1 Il tuo benchmark ha difetti, grepè significativamente più lento rispetto alla creazione di hash e alla ricerca, poiché il primo è O (n) e il secondo O (1). Fai la creazione dell'hash solo una volta (al di fuori del ciclo) e precomputa regex per misurare solo i metodi ( vedi la mia risposta ).
Xaerxess il

4
@Xaerxess: Nel mio caso volevo fare una ricerca, quindi penso sia giusto contare sia la creazione di hash / regex che la ricerca / grep. Il compito sarebbe quello di fare molte ricerche, immagino che la tua soluzione sia migliore.
Akel

3
Se vuoi fare solo una iterazione, la differenza è indistinguibile tra uno qualsiasi dei metodi che scegli, quindi qualsiasi benchmark è sbagliato poiché in questo caso è una cattiva microottimizzazione.
Xaerxess il

2
Il regex è compilato una sola volta, poiché ha la bandiera 'o'.
Apoc

3

@files è un array esistente

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

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

print "\n";

/^2[\d[.[\d[[A-za-z[?/ = vaues a partire da 2 qui puoi inserire qualsiasi espressione regolare


2

Sicuramente vuoi un hash qui. Inserire i parametri errati come chiavi nell'hash, quindi decidere se esiste un determinato parametro nell'hash.

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

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

Se sei veramente interessato a farlo con un array, guarda List::UtiloList::MoreUtils


0

Ci sono due modi per farlo. Puoi usare il lancio dei valori in un hash per una tabella di ricerca, come suggerito dagli altri post. (Aggiungerò solo un altro idioma.)

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

Ma se sono dati per lo più caratteri di parole e non troppi meta, puoi scaricarli in un'alternativa regex:

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/;

Questa soluzione dovrebbe essere ottimizzata per i tipi di "valori errati" che stai cercando. E ancora, potrebbe essere del tutto inappropriato per alcuni tipi di stringhe, quindi avvertimento emptor .


1
Puoi anche scrivere @bad_param_lookup{@bad_params} = (), ma dovresti utilizzare existsper testare l'appartenenza.
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";
    }
}

Si consiglia di verificare la coerenza numerica degli spazi iniziali

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.