Ci sono casi in cui preferiresti un algoritmo di complessità temporale big-O maggiore rispetto a quello inferiore?


242

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?


67
Preferirei un O(log n)algoritmo a un O(1)algoritmo se capissi il primo, ma non il secondo ...
Codor

14
Ci sono tonnellate di strutture dati impraticabili con operazioni O (1) dall'informatica teorica. Un esempio potrebbe essere select () su bitvector, che può essere supportato in o (n) spazio extra e O (1) per operazione, usando 5 livelli di riferimento indiretto. La semplice ricerca binaria combinata con O (1) rank () risulta essere più veloce in pratica secondo l'autore della Succinct Data Structure Library
Niklas B.

17
Una minore complessità asintotica non garantisce tempi di esecuzione più rapidi. Moltiplicazione della matrice di ricerca per un esempio concreto.
Connor Clark,

54
Inoltre ... qualsiasi algoritmo può essere convertito in O (1), data una ricerca di tabella sufficientemente grande;)
Connor Clark

19
@Hoten - Questo presuppone che la ricerca della tabella sia O (1), il che non è affatto scontato per la dimensione dei tavoli di cui stai parlando! :)
Jander

Risposte:


267

Ci possono essere molti motivi per preferire un algoritmo con una maggiore complessità temporale O rispetto a quello inferiore:

  • la maggior parte delle volte, la complessità della big-O inferiore è più difficile da ottenere e richiede un'implementazione qualificata, molta conoscenza e molti test.
  • big-O nasconde i dettagli di una costante : l'algoritmo che esegue in 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 nil 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.
  • big-O ha senso quando calcoli su qualcosa di grande. Se devi ordinare una matrice di tre numeri, importa davvero poco se usi O(n*log(n))o O(n^2)algoritmo.
  • a volte il vantaggio della complessità temporale minuscola può essere davvero trascurabile. Ad esempio, esiste una struttura di dati tango che fornisce una 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^20differenze è trascurabile.
  • la complessità temporale non è tutto. Immagina un algoritmo che funziona 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 algoritmo
  • la parallelizzazione è una buona caratteristica nel nostro mondo distribuito. Esistono algoritmi facilmente parallelizzabili e alcuni non si parallelizzano affatto. A volte ha senso eseguire un algoritmo su 1000 macchine di materie prime con una complessità maggiore rispetto all'utilizzo di una macchina con una complessità leggermente migliore.
  • in alcuni punti (sicurezza) una complessità può essere un requisito. Nessuno vuole avere un algoritmo di hash che possa hash sorprendentemente veloce (perché allora altre persone possono farti forza molto più velocemente)
  • sebbene ciò non sia correlato al cambio di complessità, ma alcune delle funzioni di sicurezza dovrebbero essere scritte in modo da prevenire l'attacco temporale . Rimangono per lo più nella stessa classe di complessità, ma sono modificati in modo da fare sempre peggio per fare qualcosa. Un esempio sta confrontando che le stringhe sono uguali. Nella maggior parte delle applicazioni ha senso rompere rapidamente se i primi byte sono diversi, ma in sicurezza attendi ancora fino alla fine per dire le cattive notizie.
  • qualcuno ha brevettato l'algoritmo a bassa complessità ed è più economico per un'azienda utilizzare una complessità maggiore rispetto a pagare denaro.
  • alcuni algoritmi si adattano bene a situazioni particolari. L'ordinamento per inserzione, ad esempio, ha una complessità temporale media di O(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.

6
Inoltre, ho visto alcune volte che le persone erano concentrate sul big-O del loro algoritmo centrale, ma ignorando i costi di installazione. La creazione di una tabella hash, ad esempio, può essere più costosa che passare attraverso un array in modo lineare se non è necessario farlo più e più volte. In effetti, a causa del modo in cui sono costruite le moderne CPU, anche qualcosa come la ricerca binaria può essere altrettanto veloce su array ordinati come una ricerca lineare: la profilazione è una necessità.
Luaan,

@Luaan "In effetti, grazie al modo in cui sono costruite le moderne CPU, anche qualcosa come la ricerca binaria può essere altrettanto veloce su array ordinati come una ricerca lineare - la profilazione è una necessità." Interessante! Puoi spiegare come la ricerca binaria e la ricerca lineare potrebbero richiedere lo stesso tempo su una CPU moderna?
DJG

3
@Luaan - Non importa, ho trovato questo: schani.wordpress.com/2010/04/30/linear-vs-binary-search
DJG

2
@DenisdeBernardy: No, in realtà no. Potrebbero essere algoritmi in P. E anche se non lo fossero, secondo le ragionevoli definizioni di cosa significhi parallelizzare, ciò non implicherebbe nemmeno P! = NP. Ricorda anche che la ricerca nello spazio di possibili percorsi di una macchina di turing non deterministica è abbastanza parallelizzabile.
einpoklum,

228

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.


Bello grazie. Pensavo che valesse la pena considerare anche un algoritmo O (logn) per garantire la stabilità del programma (ad es. In alberi binari
autobilanciati

16
Un esempio che mi viene in mente: per un piccolo array ordinato, sarebbe più facile e compatto per il programmatore implementare una funzione di ricerca binaria piuttosto che scrivere un'implementazione completa della mappa hash e usarla invece.
Colonnello Thirty Two Two

5
Un esempio di complessità: trovare la mediana di un elenco non ordinato è facile da fare in O (n * log n) ma difficile da fare in O (n).
Paul Draper,

1
-1, non mettere tronchi nel tuo tostapane ... Scherzi a parte, questo è perfetto. lg nè così, così, così vicino kper grandi nche la maggior parte delle operazioni non noterebbe mai la differenza.
corsiKa

3
C'è anche il fatto che le complessità algoritmiche con cui la maggior parte delle persone ha familiarità non tengono conto degli effetti cache. Cercare qualcosa in un albero binario è O (log2 (n)) secondo la maggior parte delle persone, ma in realtà è molto peggio perché gli alberi binari hanno una cattiva località.
Doval,

57

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.


1
Alistra ha affrontato questo problema indirettamente parlando di "problemi di spazio"
Zach Saucier,

2
L'enorme quantità di mancati cache moltiplica solo l'esecuzione finale per un valore costante (che non è maggiore di 8 per CPU a 4 core da 3,2 GHz con ram da 1,6 GHz, di solito è molto più basso) quindi viene conteggiato come costante fissa nel -O notazione. Quindi l'unica cosa che la cache non riesce a causare è spostare la soglia di n dove quella soluzione O (n) inizia ad essere più lenta della soluzione O (1).
Marian Spanik,

1
@MarianSpanik Hai ovviamente ragione. Ma questa domanda ha chiesto una situazione in cui avremmo preferito 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.
Nose Knows Tutto il

@MarianSpanik non è una cache mancare fino a 300 cicli di clock? Da dove viene l'8?
Speriamo utile il

43

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 .


6
Mentre quello che hai detto è vero, in quel caso, un algoritmo in esecuzione in O (1) è per definizione invulnerabile agli attacchi di temporizzazione.
Justin Lessard,

17
@JustinLessard: Essere O (1) significa che c'è una dimensione di input dopo la quale il tempo di esecuzione dell'algoritmo è limitato da una costante. Cosa succede al di sotto di questa soglia non è noto. Inoltre, la soglia potrebbe non essere nemmeno raggiunta per qualsiasi uso reale dell'algoritmo. L'algoritmo potrebbe essere lineare e quindi perdere informazioni sulla lunghezza dell'input, ad esempio.
Jörg W Mittag,

12
Il runtime potrebbe anche variare in diversi modi, pur rimanendo limitato. Se il runtime è proporzionale a (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.
Christian Semrau,

Questo è fondamentalmente il motivo per cui bcrypt è considerato buono; rende le cose più lente
David dice Reinstate Monica il

@DavidGrinberg Questo è il motivo per cui viene utilizzato bcrypt e si adatta alla domanda. Ma questo non è correlato a questa risposta, che parla di attacchi temporali.
Christian Semrau,

37

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.)


2
Sono un po 'confuso qui. Stai parlando di creare un array da 10 miliardi di voci (la maggior parte delle quali sarà indefinito) e di trattare l'UPC come un indice in quell'array?
David Z,

7
@DavidZ Sì. Se si utilizza un array sparse, è possibile che non si ottenga O (1) ma utilizzerà solo 1 MB di memoria. Se si utilizza un array effettivo, si ottiene l'accesso O (1) garantito, ma verrà utilizzata la memoria da 1/3 TB.
Navin,

Su un sistema moderno, utilizzerà 1/3 TB di spazio di indirizzamento, ma ciò non significa che si avvicinerà molto alla memoria di backup allocata. La maggior parte dei sistemi operativi moderni non impegna l'archiviazione per le allocazioni fino a quando non è necessario. In questo modo, nascondi essenzialmente una struttura di ricerca associativa per i tuoi dati all'interno del sistema di memoria virtuale del sistema operativo / hardware.
Phil Miller,

@Novelocrat Vero, ma se lo fai a velocità RAM, il tempo di ricerca non ha importanza, non c'è motivo di usare 40mb invece di 1mb. La versione dell'array ha senso solo quando l'accesso all'archiviazione è costoso: stai andando sul disco.
Loren Pechtel,

1
O quando questa non è un'operazione critica per le prestazioni e il tempo per gli sviluppatori è costoso: dire malloc(search_space_size)e iscriversi a ciò che restituisce è facile.
Phil Miller,

36

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.


Non sono sicuro di come le operazioni che ti consentono di utilizzare l'array con la ricerca O (1) e gli aggiornamenti O (n) corrispondano all'albero rosso-nero, a cui la gente pensava (almeno io). Il più delle volte pensavo innanzitutto alla ricerca basata su chiavi per l'albero rosso-nero. Ma per abbinare l'array dovrebbe essere una struttura leggermente diversa che mantenga la quantità di sottonodi nei nodi superiori per fornire una ricerca basata sull'indice e reindicizzare all'inserimento. Anche se concordo sul fatto che il rosso-nero può essere utilizzato per mantenere l'equilibrio, è possibile utilizzare l'albero bilanciato se si desidera essere vaghi sui dettagli delle operazioni corrispondenti.
ony

@ony Un albero rosso-nero può essere usato per definire una struttura di tipo mappa / dizionario, ma non è necessario. I nodi possono essere solo elementi, essenzialmente implementando un elenco ordinato.
jpmc26,

elenco ordinato e matrice che definisce l'ordine degli elementi hanno una quantità diversa di informazioni. Uno si basa sull'ordine tra elementi e set e l'altro definisce una sequenza arbitraria che non è necessaria per definire l'ordine tra gli elementi. Un'altra cosa è che cos'è "accesso" e "ricerca" che dichiari di essere O(log n)"albero rosso-nero"? Verrà 5inserito 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]4O(log n)
ony

@ony "l'elenco ordinato e l'array che definisce l'ordine degli elementi hanno una quantità diversa di informazioni." Sì, e questo è uno dei motivi per cui hanno caratteristiche prestazionali diverse. Ti manca il punto. Uno non è un calo in sostituzione dell'altro in tutte le situazioni. Essi ottimizzano cose diverse e fanno diversi compromessi , e il punto è che gli sviluppatori stanno facendo decisioni su quei compromessi costantemente.
jpmc26,

@ony Accesso, ricerca, inserimento ed eliminazione hanno significati specifici nel contesto delle prestazioni dell'algoritmo. L'accesso sta recuperando un elemento per posizione. La ricerca sta individuando un elemento in base al valore (che ha solo un'applicazione pratica come controllo di contenimento per una struttura non cartografica). Inserire ed eliminare dovrebbe essere semplice, però. Esempio di utilizzo può essere visto qui .
jpmc26,

23

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_mapcon 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.


21

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.


Ma tutto ciò che la parallelizzazione fa è ridurre il fattore costante di cui altri hanno parlato, giusto?
gengkev,

1
Sì, ma un algoritmo parallelo può dividere il fattore costante per 2 ogni volta che raddoppi il numero di macchine in esecuzione. Un altro algoritmo a thread singolo può ridurre il fattore costante solo una volta in modo costante. Quindi con un algoritmo parallelo puoi reagire dinamicamente alla dimensione di n ed essere più veloce nel tempo di esecuzione dell'orologio da parete.
Simulante il

15

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:

  1. Utilizzare un bitset di 1.000.000 di bit
  2. Utilizzare una matrice ordinata degli interi nella lista nera e utilizzare una ricerca binaria per accedervi

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.



12

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.


11

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 ntende 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).


10

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.


Potrei aggiungere qui che sull'unità o sul nastro ad accesso sequenziale, l'algoritmo O (1) diventa invece O (n), motivo per cui la soluzione sequenziale diventa più favorevole. Molte operazioni O (1) dipendono dal fatto che l'aggiunta e l'indicizzazione della ricerca sono un algoritmo a tempo costante, che non si trova in uno spazio ad accesso sequenziale.
TheHansinator,

9

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.


9

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 :

  1. Permettere l'array in modo casuale usando un processo quantico,
  2. Se l'array non è ordinato, distruggi l'universo.
  3. 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".


7

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.


6

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.


6

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.


4

Quando nè piccolo ed O(1)è costantemente lento.


3
  1. Quando l'unità di lavoro "1" in O (1) è molto alta rispetto all'unità di lavoro in O (log n) e la dimensione prevista prevista è piccola. Ad esempio, è probabilmente più lento calcolare i codici hash del dizionario piuttosto che iterare un array se ci sono solo due o tre elementi.

o

  1. Quando la memoria o altri requisiti di risorse non temporali dell'algoritmo O (1) sono eccezionalmente grandi rispetto all'algoritmo O (log n).

3
  1. durante la riprogettazione di un programma, viene trovata una procedura ottimizzata con O (1) invece di O (lgN), ma se non è il collo di bottiglia di questo programma ed è difficile capire l'alga O (1). Quindi non dovresti usare l'algoritmo O (1)
  2. quando O (1) richiede molta memoria che non è possibile fornire, mentre il tempo di O (lgN) può essere accettato.

1

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.

  • L'hash delle password viene talvolta reso arbitrariamente lento al fine di rendere più difficile indovinare le password con la forza bruta. Questo post sulla sicurezza delle informazioni ha un punto elenco (e molto altro).
  • Bit Coin utilizza un problema controllabile lento che una rete di computer può risolvere per "estrarre" le monete. Ciò consente alla valuta di essere estratta a un tasso controllato dal sistema collettivo.
  • Le cifre asimmetriche (come RSA ) sono progettate per rendere la decrittografia senza le chiavi intenzionalmente lente al fine di impedire a qualcun altro senza la chiave privata di violare la crittografia. Gli algoritmi sono progettati per essere craccati nel O(2^n)tempo speranzoso in cui si ntrova 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.

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.