Ci sono casi in cui preferiresti la O(log n)
complessità O(1)
temporale alla complessità temporale? O O(n)
a O(log n)
?
Hai qualche esempio?
Ci sono casi in cui preferiresti la O(log n)
complessità O(1)
temporale alla complessità temporale? O O(n)
a O(log n)
?
Hai qualche esempio?
Risposte:
Ci possono essere molti motivi per preferire un algoritmo con una maggiore complessità temporale O rispetto a quello inferiore:
10^5
è migliore dal punto di vista big-O di 1/10^5 * log(n)
( O(1)
vs O(log(n)
), ma per la maggior parte ragionevole n
il primo funzionerà meglio. Ad esempio, la migliore complessità per la moltiplicazione delle matrici è, O(n^2.373)
ma la costante è così elevata che nessuna (a mia conoscenza) libreria computazionale la usa.O(n*log(n))
o O(n^2)
algoritmo.O(log log N)
complessità temporale per trovare un elemento, ma esiste anche una struttura binaria in cui trova lo stesso O(log n)
. Anche per un numero enorme di n = 10^20
differenze è trascurabile.O(n^2)
e richiede O(n^2)
memoria. Potrebbe essere preferibile nel O(n^3)
tempo e O(1)
nello spazio quando n non è molto grande. Il problema è che puoi aspettare a lungo, ma senza dubbio puoi trovare una RAM abbastanza grande da usarla con il tuo algoritmoO(n^2)
, peggiore di quicksort o mergesort, ma come algoritmo online può ordinare in modo efficiente un elenco di valori quando vengono ricevuti (come input dell'utente) in cui la maggior parte degli altri algoritmi può funzionare in modo efficiente su un elenco completo di valori.C'è sempre la costante nascosta, che può essere inferiore nell'algoritmo O (log n ). Quindi può funzionare più velocemente in pratica per i dati della vita reale.
Ci sono anche problemi di spazio (ad es. In esecuzione su un tostapane).
C'è anche preoccupazione per il tempo degli sviluppatori: O (log n ) potrebbe essere 1000 × più facile da implementare e verificare.
lg n
è così, così, così vicino k
per grandi n
che la maggior parte delle operazioni non noterebbe mai la differenza.
Sono sorpreso che nessuno abbia ancora menzionato applicazioni legate alla memoria.
Potrebbe esserci un algoritmo che ha meno operazioni in virgola mobile a causa della sua complessità (cioè O (1) < O (log n )) o perché la costante davanti alla complessità è più piccola (cioè 2 n 2 <6 n 2 ) . Indipendentemente da ciò, potresti comunque preferire l'algoritmo con più FLOP se l'algoritmo FLOP inferiore è più associato alla memoria.
Ciò che intendo per "limite di memoria" è che si accede spesso a dati costantemente fuori cache. Per recuperare questi dati, è necessario estrarre la memoria dallo spazio di memoria effettivo nella cache prima di poter eseguire l'operazione su di esso. Questo passaggio di recupero è spesso piuttosto lento, molto più lento della stessa operazione.
Pertanto, se l'algoritmo richiede più operazioni (tuttavia queste operazioni vengono eseguite su dati che sono già nella cache [e quindi non è necessario il recupero]), eseguirà comunque un superamento dell'algoritmo con un minor numero di operazioni (che devono essere eseguite su -cache data [e quindi richiede un recupero]) in termini di wall-time effettivo.
O(logn)
sopra O(1)
. Si potrebbe facilmente immaginare una situazione in cui, per quanto possibile n
, l'applicazione meno legata alla memoria verrebbe eseguita in tempi di esecuzione del wall più veloci, anche con una complessità maggiore.
Nei contesti in cui la sicurezza dei dati è un problema, un algoritmo più complesso può essere preferibile a un algoritmo meno complesso se l'algoritmo più complesso ha una migliore resistenza agli attacchi di temporizzazione .
(n mod 5) + 1
, è ancora O(1)
, ma rivela informazioni su n
. Quindi può essere preferibile un algoritmo più complesso con un tempo di esecuzione più regolare, anche se può essere asintoticamente (e forse anche nella pratica) più lento.
Alistra lo ha inchiodato ma non ha fornito alcun esempio, quindi lo farò.
Hai un elenco di 10.000 codici UPC per ciò che il tuo negozio vende. UPC a 10 cifre, numero intero per il prezzo (prezzo in penny) e 30 caratteri di descrizione per la ricevuta.
Approccio O (log N): si dispone di un elenco ordinato. 44 byte se ASCII, 84 se Unicode. In alternativa, considera l'UPC come un int64 e ottieni 42 e 72 byte. 10.000 record: nel caso più elevato, stai guardando un po 'sotto un megabyte di spazio di archiviazione.
Approccio O (1): non archiviare l'UPC, ma lo si utilizza come una voce nell'array. Nel caso più basso, stai visualizzando quasi un terzo di un terabyte di spazio di archiviazione.
L'approccio che usi dipende dal tuo hardware. Sulla maggior parte delle configurazioni moderne ragionevoli utilizzerai l'approccio log N. Posso immaginare che il secondo approccio sia la risposta giusta se per qualche motivo stai eseguendo in un ambiente in cui la RAM è estremamente breve ma hai un sacco di memoria di massa. Un terzo di terabyte su un disco non è un grosso problema, vale la pena ottenere i dati in un probe del disco. Il semplice approccio binario richiede in media 13. (Si noti, tuttavia, che raggruppando le chiavi è possibile ottenere ciò fino a 3 letture garantite e in pratica si memorizzerà nella cache il primo.)
malloc(search_space_size)
e iscriversi a ciò che restituisce è facile.
Considera un albero rosso-nero. Ha accesso, ricerca, inserimento ed eliminazione di O(log n)
. Confronta con un array, che ha accesso O(1)
e il resto delle operazioni sono O(n)
.
Quindi, data un'applicazione in cui inseriamo, eliminiamo o cerchiamo più spesso di quanto accediamo e una scelta tra solo queste due strutture, preferiremmo l'albero rosso-nero. In questo caso, potresti dire che preferiamo il O(log n)
tempo di accesso più ingombrante dell'albero rosso-nero .
Perché? Perché l'accesso non è la nostra preoccupazione principale. Stiamo facendo un compromesso: le prestazioni della nostra applicazione sono più fortemente influenzate da fattori diversi da questo. Consentiamo a questo particolare algoritmo di subire delle prestazioni perché otteniamo grandi guadagni ottimizzando altri algoritmi.
Quindi la risposta alla tua domanda è semplicemente questa: quando il tasso di crescita dell'algoritmo non è quello che vogliamo ottimizzare , quando vogliamo ottimizzare qualcos'altro. Tutte le altre risposte sono casi speciali di questo. A volte ottimizziamo il tempo di esecuzione di altre operazioni. A volte ottimizziamo per la memoria. A volte ottimizziamo per la sicurezza. A volte ottimizziamo la manutenibilità. A volte ottimizziamo per i tempi di sviluppo. Anche la costante prevalente essendo abbastanza bassa da essere importante è l'ottimizzazione per il tempo di esecuzione quando sai che il tasso di crescita dell'algoritmo non ha il massimo impatto sul tempo di esecuzione. (Se il tuo set di dati fosse al di fuori di questo intervallo, ottimizzeresti per il tasso di crescita dell'algoritmo perché alla fine dominerebbe la costante.) Tutto ha un costo e, in molti casi, scambiamo il costo di un tasso di crescita più elevato per il algoritmo per ottimizzare qualcos'altro.
O(log n)
"albero rosso-nero"? Verrà 5
inserito l' inserimento nella posizione 2 dell'array (l'elemento aggiornerà l'indice da 3 a 4). Come hai intenzione di ottenere questo comportamento nell '"albero rosso-nero" a cui fai riferimento come "elenco ordinato"? [1, 2, 1, 4]
[1, 2, 5, 1 4]
4
O(log n)
Sì.
In un caso reale, abbiamo eseguito alcuni test su come effettuare ricerche di tabella con chiavi stringa sia corte che lunghe.
Abbiamo usato a std::map
, a std::unordered_map
con un hash che campiona al massimo 10 volte lungo la lunghezza della stringa (le nostre chiavi tendono ad essere guid-like, quindi questo è decente), e un hash che campiona ogni carattere (in teoria riduce le collisioni), un vettore non ordinato in cui eseguiamo un ==
confronto e (se ricordo bene) un vettore non ordinato in cui memorizziamo anche un hash, prima confrontiamo l'hash, quindi confrontiamo i caratteri.
Questi algoritmi vanno da O(1)
(unordered_map) a O(n)
(ricerca lineare).
Per N di dimensioni modeste, abbastanza spesso O (n) batte O (1). Sospettiamo che ciò sia dovuto al fatto che i contenitori basati su nodo richiedevano al nostro computer di saltare di più nella memoria, mentre i contenitori basati su lineari no.
O(lg n)
esiste tra i due. Non ricordo come sia andata.
La differenza di prestazioni non era così grande, e su set di dati più grandi quello basato sull'hash si è comportato molto meglio. Quindi ci siamo bloccati con la mappa non ordinata basata su hash.
In pratica, per dimensioni ragionevoli n, O(lg n)
è O(1)
. Se il tuo computer ha solo 4 miliardi di voci nella tua tabella, allora O(lg n)
è limitato da 32
. (lg (2 ^ 32) = 32) (in informatica, lg è l'abbreviazione di log based 2).
In pratica, gli algoritmi lg (n) sono più lenti degli algoritmi O (1) non a causa del fattore di crescita logaritmica, ma perché la porzione lg (n) di solito significa che esiste un certo livello di complessità per l'algoritmo e che la complessità aggiunge un fattore costante maggiore di qualsiasi "crescita" dal termine lg (n).
Tuttavia, algoritmi complessi O (1) (come la mappatura hash) possono facilmente avere un fattore costante simile o maggiore.
La possibilità di eseguire un algoritmo in parallelo.
Non so se esiste un esempio per le classi O(log n)
e O(1)
, per alcuni problemi, si sceglie un algoritmo con una classe di complessità superiore quando l'algoritmo è più semplice da eseguire in parallelo.
Alcuni algoritmi non possono essere parallelizzati ma hanno una classe di complessità così bassa. Si consideri un altro algoritmo che raggiunge lo stesso risultato e può essere parallelizzato facilmente, ma ha una classe di complessità più elevata. Quando eseguito su una macchina, il secondo algoritmo è più lento, ma quando eseguito su più macchine, il tempo di esecuzione reale diventa sempre più basso mentre il primo algoritmo non può accelerare.
Supponiamo che tu stia implementando una lista nera su un sistema incorporato, in cui numeri tra 0 e 1.000.000 potrebbero essere inseriti nella lista nera. Questo ti lascia due possibili opzioni:
L'accesso al bitset avrà un accesso costante garantito. In termini di complessità temporale, è ottimale. Sia da un punto di vista teorico che pratico (è O (1) con un sovraccarico costante estremamente basso).
Tuttavia, potresti voler preferire la seconda soluzione. Soprattutto se si prevede che il numero di numeri nella lista nera sarà molto piccolo, poiché sarà più efficiente in termini di memoria.
E anche se non si sviluppa per un sistema embedded in cui la memoria è scarsa, posso solo aumentare il limite arbitrario da 1.000.000 a 1.000.000.000.000 e fare lo stesso argomento. Quindi il bitset richiederebbe circa 125G di memoria. Avere una complessità garantita nel caso peggiore di O (1) potrebbe non convincere il tuo capo a fornirti un server così potente.
Qui, preferirei fortemente una ricerca binaria (O (log n)) o albero binario (O (log n)) rispetto al bitset O (1). E probabilmente, una tabella di hash con la sua complessità peggiore di O (n) batterà tutti in pratica.
La mia risposta qui La rapida selezione ponderata casuale su tutte le righe di una matrice stocastica è un esempio in cui un algoritmo con complessità O (m) è più veloce di uno con complessità O (log (m)), quando m
non è troppo grande.
Le persone hanno già risposto alla tua domanda esatta, quindi affronterò una domanda leggermente diversa a cui le persone potrebbero effettivamente pensare quando vengono qui.
Un sacco di "O (1) tempo" algoritmi e strutture dati in realtà solo prendere atteso O (1) tempo, il che significa che la loro media tempo di esecuzione è O (1), eventualmente solo in determinate ipotesi.
Esempi comuni: hashtables, espansione di "liste di array" (ovvero matrici / vettori di dimensioni dinamiche).
In tali scenari, potresti preferire utilizzare strutture di dati o algoritmi il cui tempo è garantito per essere assolutamente limitato dal punto di vista logaritmico, anche se in media potrebbero avere prestazioni peggiori.
Un esempio potrebbe quindi essere un albero di ricerca binario bilanciato, il cui tempo di esecuzione è in media peggiore ma migliore nel caso peggiore.
Una questione più generale è se ci sono situazioni in cui si preferirebbe un O(f(n))
algoritmo per un O(g(n))
algoritmo, anche se g(n) << f(n)
, come n
tende all'infinito. Come altri hanno già detto, la risposta è chiaramente "sì" nel caso in cui f(n) = log(n)
e g(n) = 1
. Talvolta è sì anche nel caso che f(n)
sia polinomiale ma g(n)
esponenziale. Un esempio famoso e importante è quello dell'algoritmo Simplex per la risoluzione di problemi di programmazione lineare. Negli anni '70 è stato dimostrato di esserlo O(2^n)
. Pertanto, il suo comportamento peggiore è impossibile. Ma - il suo comportamento nella media dei casi è estremamente buono, anche per problemi pratici con decine di migliaia di variabili e vincoli. Negli anni '80, algoritmi temporali polinomiali (come aL'algoritmo del punto interno di Karmarkar ) è stato scoperto per la programmazione lineare, ma 30 anni dopo l'algoritmo simplex sembra ancora essere l'algoritmo di scelta (ad eccezione di alcuni problemi molto grandi). Questo è per l'ovvia ragione che il comportamento nel caso medio è spesso più importante del comportamento nel caso peggiore, ma anche per una ragione più sottile che l'algoritmo simplex è in un certo senso più informativo (ad esempio, le informazioni sulla sensibilità sono più facili da estrarre).
Per inserire i miei 2 centesimi in:
A volte viene selezionato un algoritmo di complessità peggiore al posto di uno migliore, quando l'algoritmo viene eseguito su un determinato ambiente hardware. Supponiamo che il nostro algoritmo O (1) acceda in modo non sequenziale a tutti gli elementi di un array di dimensioni fisse molto grandi per risolvere il nostro problema. Quindi posizionare tale array su un disco rigido meccanico o un nastro magnetico.
In tal caso, l'algoritmo O (logn) (supponiamo che acceda al disco in sequenza), diventa più favorevole.
C'è un buon caso d'uso per usare un algoritmo O (log (n)) invece di un algoritmo O (1) che le altre numerose risposte hanno ignorato: l'immutabilità. Le mappe hash hanno O (1) put e get, presupponendo una buona distribuzione dei valori hash, ma richiedono uno stato mutabile. Le mappe ad albero immutabili hanno O (log (n)) put e get, che è asintoticamente più lento. Tuttavia, l'immutabilità può essere abbastanza preziosa da compensare prestazioni peggiori e nel caso in cui debbano essere conservate più versioni della mappa, l'immutabilità ti consente di evitare di dover copiare la mappa, che è O (n), e quindi può migliorare prestazione.
Semplicemente: perché il coefficiente - i costi associati all'installazione, alla memorizzazione e ai tempi di esecuzione di quel passaggio - può essere molto, molto più grande con un problema di big-O minore che con uno maggiore. Big-O è solo una misura della scalabilità degli algoritmi .
Considera l'esempio seguente dal Dizionario dell'hacker, proponendo un algoritmo di ordinamento basato sull'interpretazione dei mondi multipli della meccanica quantistica :
- Permettere l'array in modo casuale usando un processo quantico,
- Se l'array non è ordinato, distruggi l'universo.
- Tutti gli universi rimanenti sono ora ordinati [incluso quello in cui ci si trova].
(Fonte: http://catb.org/~esr/jargon/html/B/bogo-sort.html )
Si noti che il big-O di questo algoritmo è O(n)
, che batte qualsiasi algoritmo di ordinamento noto fino ad oggi su articoli generici. Anche il coefficiente del passo lineare è molto basso (poiché è solo un confronto, non uno scambio, che viene eseguito in modo lineare). Un algoritmo simile potrebbe, in effetti, essere utilizzato per risolvere qualsiasi problema sia in NP che in co-NP in tempo polinomiale, poiché ogni possibile soluzione (o possibile prova che non esiste soluzione) può essere generata utilizzando il processo quantistico, quindi verificata in tempo polinomiale.
Tuttavia, nella maggior parte dei casi, probabilmente non vogliamo correre il rischio che Multiple Worlds non sia corretto, per non parlare del fatto che l'atto di implementare il passaggio 2 è ancora "lasciato come esercizio per il lettore".
In qualsiasi momento quando n è limitato e il moltiplicatore costante dell'algoritmo O (1) è superiore al limite su log (n). Ad esempio, la memorizzazione di valori in un hashset è O (1), ma potrebbe richiedere un calcolo costoso di una funzione hash. Se gli elementi di dati possono essere paragonati banalmente (rispetto a un certo ordine) e il limite su n è tale che il registro n è significativamente inferiore al calcolo dell'hash su un singolo elemento, la memorizzazione in un albero binario bilanciato potrebbe essere più veloce rispetto alla memorizzazione in un hashset.
In una situazione in tempo reale in cui è necessario un limite superiore stabile, selezionare ad esempio un heapsort anziché un Quicksort, poiché il comportamento medio di heapsort è anche il comportamento peggiore.
Aggiungendo alle già buone risposte. Un esempio pratico sarebbe indici di hash vs indici B-tree nel database postgres.
Gli indici hash formano un indice di tabella hash per accedere ai dati sul disco mentre btree come suggerisce il nome utilizza una struttura di dati Btree.
Nel tempo Big-O sono O (1) vs O (logN).
Gli indici hash sono attualmente scoraggiati in Postgres poiché in una situazione di vita reale, in particolare nei sistemi di database, ottenere hash senza collisione è molto difficile (può portare a una O (N) peggiore complessità) e per questo è ancora più difficile da rendere loro crash sicuro (chiamato scrivere in anticipo registrazione - WAL in postgres).
Questo compromesso è fatto in questa situazione poiché O (logN) è abbastanza buono per gli indici e l'implementazione di O (1) è piuttosto difficile e la differenza di tempo non avrebbe davvero importanza.
o
Questo è spesso il caso delle applicazioni di sicurezza che desideriamo progettare problemi i cui algoritmi sono appositamente lenti per impedire a qualcuno di ottenere una risposta a un problema troppo rapidamente.
Ecco un paio di esempi dalla parte superiore della mia testa.
O(2^n)
tempo speranzoso in cui si n
trova la lunghezza in bit della chiave (questa è la forza bruta).Altrove in CS, Quick Sort è O(n^2)
nel caso peggiore, ma nel caso generale lo è O(n*log(n))
. Per questo motivo, l'analisi "Big O" a volte non è l'unica cosa che ti interessa quando analizzi l'efficienza dell'algoritmo.
O(log n)
algoritmo a unO(1)
algoritmo se capissi il primo, ma non il secondo ...