Quali sono le funzioni asintotiche? Che cos'è un asintoto, comunque?
Data una funzione f (n) che descrive la quantità di risorse (tempo CPU, RAM, spazio su disco, ecc.) Consumata da un algoritmo quando applicata a un input di dimensione n , definiamo fino a tre notazioni asintotiche per descriverne le prestazioni per grande n .
Un asintoto (o funzione asintotica) è semplicemente un'altra funzione (o relazione) g (n) a cui f (n) si avvicina sempre di più man mano che n diventa sempre più grande, ma non raggiunge mai del tutto. Il vantaggio di parlare di funzioni asintotiche è che sono generalmente molto più semplici da parlare anche se l'espressione per f (n) è estremamente complicata. Le funzioni asintotiche sono utilizzate come parte delle notazioni di delimitazione che limitano f (n) sopra o sotto.
(Nota: nel senso qui impiegato, le funzioni asintotiche sono vicine alla funzione originale solo dopo aver corretto un fattore diverso da zero, poiché tutte e tre le notazioni big-O / Θ / Ω ignorano questi fattori costanti dalla loro considerazione.)
Quali sono le tre notazioni di delimitazione asintotica e come sono diverse?
Tutte e tre le notazioni vengono utilizzate in questo modo:
f (n) = O (g (n))
dove f (n) qui è la funzione di interesse, e g (n) è un'altra funzione asintotica con cui stai provando ad approssimare f (n) . Questo non dovrebbe essere preso come uguaglianza in senso rigoroso, ma un'affermazione formale tra quanto velocemente f (n) cresce rispetto a n rispetto a g (n) , poiché n diventa grande. I puristi useranno spesso la notazione alternativa f (n) ∈ O (g (n)) per sottolineare che il simbolo O (g (n)) è in realtà un'intera famiglia di funzioni che condividono un tasso di crescita comune.
La notazione Big-ϴ (Theta) afferma un'uguaglianza sulla crescita di f (n) fino a un fattore costante (ne parleremo più avanti). Si comporta in modo simile a un =
operatore per i tassi di crescita.
La notazione Big-O descrive un limite superiore sulla crescita di f (n) . Si comporta in modo simile a un ≤
operatore per i tassi di crescita.
La notazione Big-Ω (Omega) descrive un limite inferiore su una crescita di f (n) . Si comporta in modo simile a un ≥
operatore per i tassi di crescita.
Esistono molte altre notazioni asintotiche , ma non si verificano quasi altrettanto spesso nella letteratura informatica.
Le notazioni Big-O e il suo genere sono spesso un modo per confrontare la complessità temporale .
Cos'è la complessità temporale?
La complessità temporale è un termine elaborato per la quantità di tempo T (n) impiegata per l'esecuzione di un algoritmo in funzione della sua dimensione di input n . Questo può essere misurato in termini di tempo reale (ad es. Secondi), numero di istruzioni della CPU, ecc. Di solito si presume che l'algoritmo verrà eseguito sul computer dell'architettura von Neumann di tutti i giorni . Ma ovviamente puoi usare la complessità del tempo per parlare di sistemi di elaborazione più esotici, dove le cose potrebbero essere diverse!
È anche comune parlare della complessità dello spazio usando la notazione Big-O. La complessità dello spazio è la quantità di memoria (memoria) richiesta per completare l'algoritmo, che potrebbe essere RAM, disco, ecc.
È possibile che un algoritmo sia più lento ma utilizzi meno memoria, mentre un altro sia più veloce ma utilizzi più memoria. Ognuno può essere più appropriato in circostanze diverse, se le risorse sono vincolate in modo diverso. Ad esempio, un processore incorporato può avere una memoria limitata e favorire l'algoritmo più lento, mentre un server in un data center può avere una grande quantità di memoria e favorire l'algoritmo più veloce.
Calcolo di Big-ϴ
Il calcolo del Big-ϴ di un algoritmo è un argomento che può riempire un piccolo libro di testo o all'incirca mezzo semestre di corso di laurea: questa sezione tratterà le basi.
Data una funzione f (n) in pseudocodice:
int f(n) {
int x = 0;
for (int i = 1 to n) {
for (int j = 1 to n) {
++x;
}
}
return x;
}
Qual è la complessità temporale?
Il ciclo esterno viene eseguito n volte. Per ogni volta che il ciclo esterno viene eseguito, il ciclo interno viene eseguito n volte. Questo mette il tempo di funzionamento in T (n) = n 2 .
Considera una seconda funzione:
int g(n) {
int x = 0;
for (int k = 1 to 2) {
for (int i = 1 to n) {
for (int j = 1 to n) {
++x;
}
}
}
return x;
}
L'anello esterno viene eseguito due volte. Il ciclo centrale viene eseguito n volte. Per ogni volta che il ciclo centrale viene eseguito, il ciclo interno viene eseguito n volte. Questo mette il tempo di funzionamento a T (n) = 2n 2 .
Ora la domanda è: qual è il tempo di esecuzione asintotico di entrambe le funzioni?
Per calcolare questo, eseguiamo due passaggi:
- Rimuovi costanti. Poiché gli algoritmi aumentano nel tempo a causa degli input, gli altri termini dominano il tempo di esecuzione, rendendoli non importanti.
- Rimuovi tutto tranne il termine più grande. Mentre n va all'infinito, n 2 supera rapidamente n .
La chiave qui è concentrarsi sui termini dominanti e semplificarli .
T (n) = n 2 ∈ ϴ (n 2 )
T (n) = 2n 2 ∈ ϴ (n 2 )
Se abbiamo un altro algoritmo con più termini, lo semplificheremmo usando le stesse regole:
T (n) = 2n 2 + 4n + 7 ∈ ϴ (n 2 )
La chiave di tutti questi algoritmi è che ci concentriamo sui termini più grandi e rimuoviamo le costanti . Non stiamo guardando il tempo di esecuzione effettivo, ma la relativa complessità .
Calcolo di Big-Ω e Big-O
Prima di tutto, tieni presente che nella letteratura informale , "Big-O" è spesso trattato come un sinonimo di Big-Θ, forse perché le lettere greche sono difficili da scrivere. Quindi, se qualcuno di punto in bianco ti chiede il Big-O di un algoritmo, probabilmente vogliono il suo Big-Θ.
Ora, se vuoi davvero calcolare Big-Ω e Big-O nei sensi formali definiti in precedenza, hai un grosso problema: ci sono infinite descrizioni Big-Ω e Big-O per ogni data funzione! È come chiedere quali siano i numeri inferiori o uguali a 42. Ci sono molte possibilità
Per un algoritmo con T (n) ∈ ϴ (n 2 ) , è necessario formulare una delle seguenti affermazioni formalmente valide:
- T (n) ∈ O (n 2 )
- T (n) ∈ O (n 3 )
- T (n) ∈ O (n 5 )
- T (n) ∈ O (n 12345 × e n )
- T (n) ∈ Ω (n 2 )
- T (n) ∈ Ω (n)
- T (n) ∈ Ω (log (n))
- T (n) ∈ Ω (log (log (n)))
- T (n) ∈ Ω (1)
Ma non è corretto dichiarare T (n) ∈ O (n) o T (n) ∈ Ω (n 3 ) .
Qual è la complessità relativa? Quali classi di algoritmi ci sono?
Se confrontiamo due diversi algoritmi, la loro complessità man mano che l'input passa all'infinito aumenterà normalmente. Se osserviamo diversi tipi di algoritmi, possono rimanere relativamente gli stessi (diciamo, differendo per un fattore costante) o possono divergere notevolmente. Questo è il motivo per eseguire l'analisi Big-O: per determinare se un algoritmo funzionerà ragionevolmente con input di grandi dimensioni.
Le classi di algoritmi si suddividono come segue:
Θ (1) - costante. Ad esempio, la selezione del primo numero in un elenco richiederà sempre lo stesso tempo.
Θ (n) - lineare. Ad esempio, l'iterazione di un elenco richiederà sempre un tempo proporzionale alla dimensione dell'elenco, n .
Θ (log (n)) - logaritmico (la base normalmente non ha importanza). Gli algoritmi che dividono lo spazio di input in ogni passaggio, come la ricerca binaria, sono esempi.
Θ (n × log (n)) - tempi lineari logaritmici ("linearithmic"). Questi algoritmi in genere dividono e conquistano ( log (n) ) mentre ripetono ( n ) tutto l'input. Molti algoritmi di ordinamento popolari (merge sort, Timsort) rientrano in questa categoria.
Θ (n m ) - polinomiale ( n elevato a qualsiasi costante m ). Questa è una classe di complessità molto comune, spesso presente nei loop nidificati.
Θ (m n ) - esponenziale (qualsiasi costante m elevata a n ). Molti algoritmi ricorsivi e grafici rientrano in questa categoria.
Θ (n!) - fattoriale. Alcuni algoritmi grafici e combinatori sono complessità fattoriale.
Questo ha qualcosa a che fare con il caso migliore / medio / peggiore?
No. Big-O e la sua famiglia di notazioni parlano di una specifica funzione matematica . Sono strumenti matematici impiegati per aiutare a caratterizzare l'efficienza degli algoritmi, ma la nozione di migliore / medio / peggiore caso non è correlata alla teoria dei tassi di crescita qui descritta.
Per parlare del Big-O di un algoritmo, è necessario impegnarsi in uno specifico modello matematico di un algoritmo con esattamente un parametro n
, che dovrebbe descrivere la "dimensione" dell'input, in qualunque senso sia utile. Ma nel mondo reale, gli input hanno una struttura molto più ampia delle loro lunghezze. Se questo era un algoritmo di ordinamento, che potrebbe alimentare nelle stringhe "abcdef"
, "fedcba"
oppure "dbafce"
. Sono tutti della lunghezza 6, ma uno è già ordinato, uno è invertito e l'ultimo è solo un miscuglio casuale. Alcuni algoritmi di ordinamento (come Timsort) funzionano meglio se l'input è già ordinato. Ma come si può incorporare questa disomogeneità in un modello matematico?
L'approccio tipico è semplicemente supporre che l'input provenga da una distribuzione casuale e probabilistica. Quindi, si calcola la media della complessità dell'algoritmo su tutti gli input con lunghezza n
. Questo ti dà un modello di complessità di caso medio dell'algoritmo. Da qui, puoi usare le notazioni Big-O / Θ / Ω come al solito per descrivere il comportamento medio del caso.
Ma se sei preoccupato per gli attacchi denial-of-service, potresti dover essere più pessimista. In questo caso, è più sicuro supporre che gli unici input siano quelli che causano la maggior quantità di dolore al tuo algoritmo. Questo ti dà un modello di complessità nel caso peggiore dell'algoritmo. Successivamente, puoi parlare di Big-O / Θ / Ω ecc . Del modello peggiore .
Allo stesso modo, puoi anche concentrare il tuo interesse esclusivamente sugli input con i quali l'algoritmo ha il minor numero di problemi per arrivare a un modello del caso migliore , quindi guarda Big-O / Θ / Ω ecc.