Big O: limite superiore
"Big O" ( ) è di gran lunga il più comune. Quando si analizza la complessità di un algoritmo, la maggior parte delle volte, ciò che conta è avere un limite superiore alla velocità con cui il tempo di esecuzione¹ aumenta quando aumenta la dimensione dell'input. Fondamentalmente vogliamo sapere che l'esecuzione dell'algoritmo non richiederà "troppo tempo". Non possiamo esprimerlo in unità di tempo effettive (secondi), perché ciò dipenderebbe dall'implementazione precisa (il modo in cui è scritto il programma, quanto è buono il compilatore, quanto è veloce il processore della macchina, ...). Quindi valutiamo ciò che non dipende da tali dettagli, ovvero quanto tempo ci vuole per eseguire l'algoritmo quando gli forniamo input più grandi. E ci preoccupiamo soprattutto quando possiamo essere sicuri che il programma sia stato completato, quindi di solito vogliamo sapere che ci vorrà un tale tempo o meno.O
Dire che un algoritmo ha un tempo di esecuzione di per una dimensione di input n significa che esiste una costante K tale che l'algoritmo completa al massimo KO ( f( n ) )nK passi, ovvero il tempo di esecuzione dell'algoritmo cresce al più velocemente di f (fino a un fattore di ridimensionamento). Notando T ( n ) il tempo di esecuzione dell'algoritmo per la dimensione di input n , O ( n ) significa informalmente che T ( n ) ≤ f ( n ) fino ad un fattore di ridimensionamento.Kf( n )fT( n )nO ( n )T( n ) ≤ f( n )
Limite inferiore
A volte, è utile avere più informazioni di un limite superiore. è il contrario di O : esprime che una funzione cresce almeno altrettanto velocemente di un'altra. T ( n ) = Ω ( g ( n ) ) significa che T ( N ) ≥ K ′ g ( n ) per una costante K ′ , o per dirla in modo informale, T ( n ) ≥ g ( n ) fino ad un certo ridimensionamento fattore.ΩOT( n ) = Ω ( g( n ) )T( N) ≥ K'g( n )K'T( n ) ≥ g( n )
Quando il tempo di esecuzione dell'algoritmo può essere determinato con precisione, combina O e Ω : esprime che è noto il tasso di crescita di una funzione, fino a un fattore di ridimensionamento. T ( n ) = Θ ( h ( n ) ) significa che K h ( n ) ≥ T ( n ) ≥ K ′ h ( n ) per alcune costanti K e K ′ . Informalmente parlando, T (ΘOΩT(n)=Θ(h(n))Kh(n)≥T(n)≥K′h(n)KK′ fino ad un certo fattore di scala.T(n)≈h(n)
Ulteriori considerazioni
I "piccoli" e ω sono usati molto meno spesso nell'analisi della complessità. La piccola o è più forte della grande O ; dove O indica una crescita che non è più veloce, o indica che la crescita è strettamente più lenta. Al contrario, ω indica una crescita strettamente più rapida.oωoOOoω
Sono stato leggermente informale nella discussione sopra. Wikipedia ha tutte le definizioni e un approccio più matematico.
Tieni presente che l'uso del segno uguale in e simili è un termine improprio. A rigor di termini, O ( f ( n ) ) è un insieme di funzioni della variabile n , e dovremmo scrivere T ∈ O ( f ) .T(n)=O(f(n))O(f(n))nT∈O(f)
Esempio: alcuni algoritmi di ordinamento
Dato che è piuttosto secco, lasciami fare un esempio. La maggior parte degli algoritmi di ordinamento ha un tempo di esecuzione quadratico nel caso peggiore, ovvero per un input di dimensione , il tempo di esecuzione dell'algoritmo è O ( n 2 ) . Ad esempio, l' ordinamento di selezione ha un tempo di esecuzione O ( n 2 ) , poiché la selezione dell'elemento k th richiede confronti n - k , per un totale di n ( n - 1 ) / 2 confronti. In effetti, il numero di confronti è sempre esattamente n ( n -nO(n2)O(n2)kn−kn(n−1)/2 , che cresce come n 2 . Quindi possiamo essere più precisi sulla complessità temporale dell'ordinamento per selezione: è Θ ( n 2 ) .n(n−1)/2n2Θ(n2)
Ora prendi il tipo di unione . Unisci ordinamento è anche quadratico ( ). Questo è vero, ma non molto preciso. Unisci ordinamento infatti ha un tempo di esecuzione di O ( nO(n2) nel peggiore dei casi. Come per l'ordinamento di selezione, il flusso di lavoro dell'ordinamento di unione è essenzialmente indipendente dalla forma dell'input e il suo tempo di esecuzione è sempre nO(nlg(n)) fino a un fattore moltiplicativo costante, cioè è Θ ( nnlg(n) .Θ(nlg(n))
Quindi, considera quicksort . Quicksort è più complesso. È certamente . Inoltre, il caso peggiore di quicksort è quadratico: il caso peggiore è Θ ( n 2 ) . Tuttavia, il caso migliore di quicksort (quando l'ingresso è già ordinato) è lineare: il migliore che possiamo dire per un limite inferiore a quicksort in generale è Ω ( n ) . Non ripeterò qui la prova, ma la complessità media di quicksort (la media rilevata su tutte le possibili permutazioni dell'input) è Θ ( nO(n2)Θ(n2)Ω(n) .Θ(nlg(n))
Ci sono risultati generali sulla complessità degli algoritmi di ordinamento in impostazioni comuni. Supponiamo che un algoritmo di ordinamento possa confrontare solo due elementi alla volta, con un risultato sì o no (o oppure x > y ). Quindi è ovvio che il tempo di esecuzione di qualsiasi algoritmo di ordinamento è sempre Ω ( n ) (dove n è il numero di elementi da ordinare), perché l'algoritmo deve confrontare ogni elemento almeno una volta per sapere dove si adatterà. Questo limite inferiore può essere soddisfatto, ad esempio, se l'input è già ordinato e l'algoritmo confronta semplicemente ciascun elemento con quello successivo e li mantiene in ordine (ovvero n - 1x≤yx>yΩ(n)nn−1i confronti). Ciò che è meno ovvio è che il tempo di funzionamento massimo è necessariamente . È possibile che l'algoritmo a volte comporti un numero inferiore di confronti, ma deve esserci una costante K tale che per qualsiasi dimensione di input n , vi sia almeno un input su cui l'algoritmo effettua più di K n l g ( n ) confronti. L'idea della dimostrazione è costruire l'albero decisionale dell'algoritmo, cioè seguire le decisioni che l'algoritmo prende dal risultato di ogni confronto. Poiché ogni confronto restituisce un risultato sì o no, l'albero decisionale è un albero binario. Ci sono n !Ω(nlg(n))KnKnlg(n)n!possibili permutazioni dell'input e l'algoritmo deve distinguere tra tutti loro, quindi la dimensione dell'albero decisionale è . Poiché l'albero è un albero binario, richiede una profondità di Θ ( l g ( n ! ) ) = Θ ( nn! per adattarsi a tutti questi nodi. La profondità è il numero massimo di decisioni che l'algoritmo prende, quindi l'esecuzione dell'algoritmo comporta almeno questo numero di confronti: il tempo di esecuzione massimo è Ω ( nΘ(lg(n!))=Θ(nlg(n)) .Ω(nlg(n))
¹ O altro consumo di risorse come spazio di memoria. In questa risposta, prendo in considerazione solo il tempo di esecuzione.