Elenco di Big-O per funzioni PHP


345

Dopo aver usato PHP per un po 'di tempo, ho notato che non tutte le funzioni PHP integrate sono veloci come previsto. Considera queste due possibili implementazioni di una funzione che rileva se un numero è primo usando una matrice di numeri primi memorizzata nella cache.

//very slow for large $prime_array
$prime_array = array( 2, 3, 5, 7, 11, 13, .... 104729, ... );
$result_array = array();
foreach( $prime_array => $number ) {
    $result_array[$number] = in_array( $number, $large_prime_array );
}

//speed is much less dependent on size of $prime_array, and runs much faster.
$prime_array => array( 2 => NULL, 3 => NULL, 5 => NULL, 7 => NULL,
                       11 => NULL, 13 => NULL, .... 104729 => NULL, ... );
foreach( $prime_array => $number ) {
    $result_array[$number] = array_key_exists( $number, $large_prime_array );
}

Questo perché in_arrayè implementato con una ricerca lineare O (n) che rallenterà linearmente man mano che $prime_arraycresce. Dove ilarray_key_exists funzione è implementata con una ricerca hash O (1) che non rallenterà a meno che la tabella hash non sia estremamente popolata (nel qual caso è solo O (n)).

Finora ho dovuto scoprire i big-O tramite tentativi ed errori, e occasionalmente guardando il codice sorgente . Ora per la domanda ...

Esiste un elenco dei grandi O teorici (o pratici) per tutte le * funzioni PHP integrate?

* o almeno quelli interessanti

Ad esempio, trovo molto difficile prevedere il grande O di funzioni elencate perché la possibile implementazione dipende strutture di dati Sconosciuto fondamentali di PHP: array_merge, array_merge_recursive, array_reverse, array_intersect, array_combine, str_replace(con ingressi matrice), etc.


31
Totalmente fuori tema ma, 1 non è un numero primo.
Jason Punyon

24
Le matrici in PHP sono hashtabili. Questo dovrebbe dirti tutto ciò che devi sapere. La ricerca di una chiave in una tabella hash è O (1). La ricerca di un valore è O (n) - che non puoi battere su un set non ordinato. La maggior parte delle funzioni di cui sei curioso sono probabilmente O (n). Naturalmente, se vuoi davvero saperlo, puoi leggere la fonte: cvs.php.net/viewvc.cgi/php-src/ext/standard/…
Frank Farmer,

11
Per la cronaca, l'implementazione più rapida di ciò che stai cercando di fare sarebbe (invece di utilizzare NULL per i tuoi valori) utilizzare truee quindi verificare la presenza utilizzando isset($large_prime_array[$number]). Se ricordo bene, è nell'ordine di essere centinaia di volte più veloce della in_arrayfunzione.
Mattbasta,

3
La notazione Big O non riguarda la velocità. Si tratta di limitare il comportamento.
Gumbo,

3
@Kendall Non sto confrontando array_key_exists, sto confrontando in_array. in_arrayscorre ogni elemento nell'array e confronta il valore con l'ago che gli viene passato. Se capovolgi i valori sulla chiave (e sostituisci ciascuno di essi con un valore fittizio come true, l'utilizzo issetè molte volte più veloce. Questo perché le chiavi di un array sono indicizzate da PHP (come una tabella hash). Di conseguenza, la ricerca un array in questo modo può avere un significativo miglioramento della velocità
mattbasta

Risposte:


649

Dal momento che non sembra che nessuno l'abbia fatto prima ho pensato che sarebbe stata una buona idea averlo come riferimento da qualche parte. Ci sono passato e tramite benchmark o skoding di codice per caratterizzare ilarray_* funzioni. Ho provato a mettere il Big-O più interessante vicino alla cima. Questo elenco non è completo.

Nota: Tutti i Big-O dove calcolati assumendo una ricerca hash è O (1) anche se in realtà è O (n). Il coefficiente di n è così basso che l'overhead del ram di memorizzare un array abbastanza grande ti danneggerebbe prima che le caratteristiche della ricerca Big-O iniziassero ad avere effetto. Ad esempio, la differenza tra una chiamata array_key_existsa N = 1 e N = 1.000.000 è un aumento del tempo del 50% circa.

Punti interessanti :

  1. isset/ array_key_existsè molto più veloce di in_arrayearray_search
  2. +(unione) è un po 'più veloce di array_merge(e sembra più bello). Ma funziona in modo diverso, quindi tienilo a mente.
  3. shuffle è sullo stesso livello Big-O di array_rand
  4. array_pop/ array_pushè più veloce di array_shift/ a array_unshiftcausa della penalità di reindicizzazione

Ricerche :

array_key_existsO (n) ma molto vicino a O (1) - questo è dovuto al polling lineare nelle collisioni, ma poiché la possibilità di collisioni è molto piccola, anche il coefficiente è molto piccolo. Trovo che tratti le ricerche di hash come O (1) per dare un big-O più realistico. Ad esempio, la differenza tra N = 1000 e N = 100000 rallenta solo del 50% circa.

isset( $array[$index] )O (n) ma molto vicino a O (1) - utilizza la stessa ricerca di array_key_exists. Poiché è il costrutto del linguaggio, memorizzerà nella cache la ricerca se la chiave è codificata, aumentando la velocità nei casi in cui la stessa chiave viene utilizzata ripetutamente.

in_array O (n) - questo perché esegue una ricerca lineare nell'array fino a quando non trova il valore.

array_search O (n): utilizza la stessa funzione principale di in_array ma restituisce valore.

Funzioni di coda :

array_push O (∑ var_i, per tutti i)

array_pop O (1)

array_shift O (n) - deve reindicizzare tutte le chiavi

array_unshift O (n + ∑ var_i, per tutti i) - deve reindicizzare tutte le chiavi

Intersezione di array, unione, sottrazione :

array_intersect_key se intersezione 100% fa O (Max (param_i_size) * ∑param_i_count, per tutti i), se intersezione 0% interseca O (∑param_i_size, per tutti i)

array_intersect se l'intersezione 100% fa O (n ^ 2 * ∑param_i_count, per tutti i), se l'intersezione 0% interseca O (n ^ 2)

array_intersect_assoc se intersezione 100% fa O (Max (param_i_size) * ∑param_i_count, per tutti i), se intersezione 0% interseca O (∑param_i_size, per tutti i)

array_diff O (π param_i_size, per tutti i) - Questo è il prodotto di tutti i param_sizes

array_diff_key O (∑ param_i_size, per i! = 1) - questo perché non è necessario eseguire l'iterazione sul primo array.

array_merge O (∑ array_i, i! = 1) - non è necessario scorrere il primo array

+ (unione) O (n), dove n è la dimensione del secondo array (ovvero array_first + array_second) - meno sovraccarico di array_merge poiché non deve rinumerare

array_replace O (∑ array_i, per tutti i)

Casuale :

shuffle Su)

array_rand O (n) - Richiede un sondaggio lineare.

Ovvio Big-O :

array_fill Su)

array_fill_keys Su)

range Su)

array_splice O (offset + lunghezza)

array_slice O (offset + lunghezza) o O (n) se lunghezza = NULL

array_keys Su)

array_values Su)

array_reverse Su)

array_pad O (pad_size)

array_flip Su)

array_sum Su)

array_product Su)

array_reduce Su)

array_filter Su)

array_map Su)

array_chunk Su)

array_combine Su)

Vorrei ringraziare Eureqa per aver reso facile trovare il Big-O delle funzioni. È un fantastico gratuito programma in grado di trovare la migliore funzione di adattamento per dati arbitrari.

MODIFICARE:

Per coloro che dubitano che lo siano le ricerche di array PHP O(N), ho scritto un benchmark per testarlo (sono ancora efficaci O(1)per valori più realistici).

grafico di ricerca dell'array php

$tests = 1000000;
$max = 5000001;


for( $i = 1; $i <= $max; $i += 10000 ) {
    //create lookup array
    $array = array_fill( 0, $i, NULL );

    //build test indexes
    $test_indexes = array();
    for( $j = 0; $j < $tests; $j++ ) {
        $test_indexes[] = rand( 0, $i-1 );
    }

    //benchmark array lookups
    $start = microtime( TRUE );
    foreach( $test_indexes as $test_index ) {
        $value = $array[ $test_index ];
        unset( $value );
    }
    $stop = microtime( TRUE );
    unset( $array, $test_indexes, $test_index );

    printf( "%d,%1.15f\n", $i, $stop - $start ); //time per 1mil lookups
    unset( $stop, $start );
}

5
@Kendall: grazie! Ho fatto un po 'di lettura e risulta che PHP usa hashtables' nidificati 'per le collisioni. Cioè, invece di una struttura logn per le collisioni, utilizza semplicemente un'altra tabella hash. E capisco che praticamente gli hashtable PHP danno prestazioni O (1), o almeno O (1) in media - ecco a cosa servono gli hashtable. Ero solo curioso di sapere perché hai detto che sono "davvero O (n)" e non "davvero O (logn)". Ottimo post tra l'altro!
Cam

10
Le complessità temporali dovrebbero essere incluse nella documentazione! Scegliere la funzione giusta può farti risparmiare così tanto tempo o dirti di evitare di fare ciò che avevi pianificato: p Grazie per questo elenco già!
Samuel,

41
So che questo è vecchio ... ma cosa? Quella curva non mostra affatto O (n), mostra O (log n), en.wikipedia.org/wiki/Logarithm . Che è anche preciso con quello che ti aspetteresti per le mappe hash nidificate.
Andreas,

5
Qual è il Big-O di unset su un elemento di un array?
Chandrew,

12
Sebbene gli hashtable abbiano effettivamente la complessità di ricerca O (n) nel peggiore dei casi, il caso medio è O (1) e il caso particolare che il tuo benchmark sta testando è addirittura garantito O (1), poiché è a base zero, continuo, indicizzato numericamente array, che non avrà mai collisioni di hash. Il motivo per cui stai ancora vedendo una dipendenza dalle dimensioni dell'array non ha nulla a che fare con la complessità algoritmica, è causata dagli effetti della cache della CPU. Più è grande l'array, più è probabile che le ricerche ad accesso casuale provochino mancamenti nella cache (e mancano più nella cache nella gerarchia).
NikiC,

5

La spiegazione del caso che descrivi specificamente è che gli array associativi sono implementati come tabelle hash, quindi cerca per chiave (e, di conseguenza, array_key_exists ) è O (1). Tuttavia, le matrici non sono indicizzate per valore, quindi l'unico modo per scoprire se esiste un valore nella matrice è una ricerca lineare. Non c'è sorpresa lì.

Non credo che ci sia una documentazione completa specifica della complessità algoritmica dei metodi PHP. Tuttavia, se è una preoccupazione abbastanza grande da giustificare lo sforzo, puoi sempre consultare il codice sorgente .


Questa non è davvero una risposta. Come ho affermato nella domanda, ho già provato a cercare il codice sorgente di PHP. Poiché PHP è implementato è scritto in C facendo uso di macro complesse, che a volte può rendere difficile "vedere" la grande O sottostante per le funzioni.
Kendall Hopkins,

@Kendall Ho trascurato il tuo riferimento all'immersione nel codice sorgente. Tuttavia, c'è una risposta nella mia risposta: "Non credo che ci sia una documentazione completa specifica della complessità algoritmica dei metodi PHP." "No" è una risposta perfettamente valida. (c:
Dathan

4

Hai quasi sempre voglia di usare issetinvece di array_key_exists. Non sto guardando gli interni, ma sono abbastanza sicuro che array_key_existssia O (N) perché scorre su ogni chiave dell'array, mentreisset cerca di accedere all'elemento usando lo stesso algoritmo hash che viene utilizzato quando si accede un indice di array. Dovrebbe essere O (1).

Un "gotcha" a cui prestare attenzione è questo:

$search_array = array('first' => null, 'second' => 4);

// returns false
isset($search_array['first']);

// returns true
array_key_exists('first', $search_array);

Ero curioso, quindi ho confrontato la differenza:

<?php

$bigArray = range(1,100000);

$iterations = 1000000;
$start = microtime(true);
while ($iterations--)
{
    isset($bigArray[50000]);
}

echo 'is_set:', microtime(true) - $start, ' seconds', '<br>';

$iterations = 1000000;
$start = microtime(true);
while ($iterations--)
{
    array_key_exists(50000, $bigArray);
}

echo 'array_key_exists:', microtime(true) - $start, ' seconds';
?>

is_set:0,132308959961 secondi
array_key_exists:2,33202195168 secondi

Naturalmente, questo non mostra la complessità temporale, ma mostra come le 2 funzioni si confrontano tra loro.

Per verificare la complessità temporale, confrontare il tempo necessario per eseguire una di queste funzioni sul primo tasto e sull'ultimo tasto.


9
Questo è sbagliato. Sono sicuro al 100% che array_key_exists non debba iterare su ogni chiave. Se non credi, dai un'occhiata al link qui sotto. La ragione per cui isset è molto più veloce è che è un costrutto linguistico. Ciò significa che non ha il sovraccarico di eseguire una chiamata di funzione. Inoltre, penso che potrebbe essere la memorizzazione nella cache della ricerca, per questo motivo. Inoltre, questa non è una risposta a LA DOMANDA! Vorrei un elenco di funzioni Big (O) per PHP (come afferma la domanda). Non un singolo punto di riferimento dei miei esempi. svn.php.net/repository/php/php-src/branches/PHP_5_3/ext/…
Kendall Hopkins

Se ancora non mi credi, ho creato un piccolo punto di riferimento per dimostrare il punto. pastebin.com/BdKpNvkE
Kendall Hopkins

Ciò che è sbagliato nel tuo benchmark è che devi disabilitare xdebug. =)
Guilherme Blanco,

3
Esistono due motivi fondamentali per cui si desidera utilizzare isset su array_key_exists. Innanzitutto, isset è un costrutto linguistico che mitiga il costo di una chiamata di funzione. Questo è simile all'argomento $arrray[] = $appendvs. array_push($array, $append)In secondo luogo, array_key_exists distingue anche tra valori non impostati e valori null. Per $a = array('fred' => null); array_key_exists('fred', $a)restituirà vero mentre isset($['fred'])restituirà falso. Questo passaggio aggiuntivo non è banale e aumenta notevolmente i tempi di esecuzione.
orca,

0

Se in pratica le persone avessero dei problemi con le collisioni chiave, implementerebbero contenitori con una ricerca hash secondaria o un albero bilanciato. L'albero bilanciato darebbe O (log n) comportamento peggiore e O (1) avg. case (l'hash stesso). Il sovraccarico non vale la pena nella maggior parte dei casi pratici nelle applicazioni di memoria, ma forse ci sono database che implementano questa forma di strategia mista come il loro caso predefinito.

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.