"Per valori piccoli di n, O (n) può essere trattato come se fosse O (1)"


26

Ho sentito più volte che per valori sufficientemente piccoli di n, O (n) può essere pensato / trattato come se fosse O (1).

Esempio :

La motivazione per farlo si basa sull'idea errata che O (1) sia sempre migliore di O (lg n), sia sempre migliore di O (n). L'ordine asintotico di un'operazione è rilevante solo se in condizioni realistiche la dimensione del problema diventa effettivamente grande. Se n rimane piccolo, allora ogni problema è O (1)!

Cosa è sufficientemente piccolo? 10? 100? 1000? A che punto dici "non possiamo più trattarlo come un'operazione gratuita"? Esiste una regola empirica?

Sembra che potrebbe essere specifico per dominio o caso specifico, ma ci sono delle regole generali su come pensare a questo?


4
La regola empirica dipende dal problema che si desidera risolvere. Essere veloci sui sistemi embedded con ? Pubblica in teoria della complessità? n100
Raffaello

3
Pensandoci di più, è praticamente impossibile trovare una singola regola empirica, perché i requisiti di prestazione sono determinati dal tuo dominio e dai suoi requisiti aziendali. In ambienti non limitati alle risorse, n potrebbe essere abbastanza grande. In ambienti fortemente vincolati, potrebbe essere piuttosto piccolo. Ciò sembra ovvio ora col senno di poi.
rianjs,

12
@rianjs Sembra che tu stia scambiando O(1)per libera . Il ragionamento alla base delle prime frasi è che O(1)è costante , che a volte può essere follemente lento. Un calcolo che richiede mille miliardi di anni indipendentemente dall'input è un O(1)calcolo.
Mooing Duck il

1
Domanda correlata sul perché usiamo gli asintotici in primo luogo.
Raffaello

3
@rianjs: fai attenzione alle battute sulla falsariga di "un pentagono è approssimativamente un cerchio, per valori sufficientemente grandi di 5". La frase di cui stai chiedendo ha senso, ma dal momento che ti ha creato un po 'di confusione, potrebbe valere la pena chiedere a Eric Lippert in che misura questa scelta esatta di frase fosse per effetto umoristico. Avrebbe potuto dire "se c'è un limite superiore su allora ogni problema è " ed era ancora matematicamente corretto. "Piccolo" non fa parte della matematica. O ( 1 )nO(1)
Steve Jessop,

Risposte:


21

Tutti gli ordini di grandezza implicano una costante , molti in realtà. Quando il numero di elementi è abbastanza grande, tale costante è irrilevante. La domanda è se il numero di elementi è abbastanza piccolo da dominare quella costante.C

Ecco un modo visivo per pensarci.

inserisci qui la descrizione dell'immagine

Tutti hanno una costante di avvio che determina il loro punto di partenza sull'asse Y. Ognuno ha anche una costante critica domina la velocità con cui aumenterà.C

  • Per , determina il tempo.CO(1)C
  • C × n CO(n) è in realtà , dove determina l'angolo.C×nC
  • ( C × n ) 2 CO(n2) è davvero , dove determina la nitidezza della curva.(C×n)2C

Per determinare quale algoritmo dovresti usare, devi stimare il punto in cui i runtime si intersecano. Ad esempio, una soluzione con un tempo di avvio elevato o una elevata perderà a una soluzione con un tempo di avvio basso e una bassa a un numero abbastanza elevato di elementi.C O ( n ) CO(1)CO(n)C

Ecco un esempio del mondo reale. Devi spostare un mucchio di mattoni attraverso un cortile. Puoi spostarli un po 'alla volta con le mani o andare a prendere un'enorme e lenta terna per sollevarli e guidarli in un solo viaggio. Qual è la tua risposta se ci sono tre mattoni? Qual è la tua risposta se ce ne sono tremila?

Ecco un esempio CS. Diciamo che hai bisogno di un elenco che è sempre ordinato. È possibile utilizzare un albero che si manterrà in ordine per . Oppure è possibile utilizzare un elenco non ordinato e riordinare dopo ogni inserimento o eliminazione in . Poiché le operazioni sugli alberi sono complicate (hanno una costante elevata) e l'ordinamento è così semplice (costante bassa), l'elenco probabilmente vincerà fino a centinaia o migliaia di elementi.O ( n log n )O(logn)O(nlogn)

Puoi guardare questo genere di cose, ma alla fine il benchmarking è ciò che lo farà. Devi anche controllare quanti oggetti hai in genere e mitigare il rischio di essere consegnato di più. Dovrai anche documentare il tuo presupposto come "le prestazioni si ridurranno rapidamente rispetto agli elementi " o "supponiamo che la dimensione massima impostata sia ".XXX

Poiché questi requisiti sono soggetti a modifiche, è importante mettere questo tipo di decisioni dietro un'interfaccia. Nell'esempio dell'albero / elenco sopra, non esporre l'albero o l'elenco. In questo modo, se i tuoi presupposti risultano errati o trovi un algoritmo migliore, puoi cambiare idea. Puoi persino fare un ibrido e cambiare in modo dinamico gli algoritmi man mano che aumenta il numero di elementi.


Non ha senso dire . Quello che realmente dire è che se il tempo di esecuzione è poi (in molti casi) . Se in molti casi , o più formalmente . E così via. Si noti, tuttavia, che in altri casi la costante varia con , entro certi limiti. T = O ( 1 ) T C T = O ( n ) T C n T = C n + o ( n ) C nO(1)=O(C)T=O(1)TCT=O(n)TCnT=Cn+o(n)Cn
Yuval Filmus,

@YuvalFilmus Questo è il motivo per cui mi piacciono i grafici.
Schwern,

Questa è di gran lunga la risposta migliore, il punto è quanto velocemente cresce la funzione.
Ricardo,

1
Bel grafico, ma l' asse dovrebbe essere etichettato "tempo", non "velocità". y
Ilmari Karonen,

1
La linea è davvero una parabola, lì? Sembra molto piatto per piccoli e molto ripido per grandi . n nO(n2)nn
David Richerby,

44

Questo è in gran parte appoggiato sulle risposte già pubblicate, ma può offrire una prospettiva diversa.

È rivelatore che la domanda discute "valori sufficientemente piccoli di n ". Il punto centrale di Big-O è descrivere come cresce l'elaborazione in funzione di ciò che viene elaborato. Se i dati elaborati rimangono piccoli, è irrilevante discutere di Big-O, perché non sei interessato alla crescita (che non sta accadendo).

Detto in altro modo, se stai percorrendo una breve distanza in fondo alla strada, può essere altrettanto veloce camminare, usare una bicicletta o guidare. Potrebbe anche essere più veloce camminare se ci vorrà del tempo per trovare le chiavi della tua auto, o se la tua auto ha bisogno di benzina, ecc.

Per i piccoli n , usa tutto ciò che è conveniente.

Se stai facendo una gita in un altro paese, devi cercare modi per ottimizzare la guida, il chilometraggio del gas, ecc.


5
"Per la piccola n, usa tutto ciò che è conveniente." - se esegui spesso l'operazione , scegli la più veloce (per la tua ). Vedi anche qui . n
Raffaello

4
Grande metafora!
Evorlor,

1
Da un punto di vista puramente matematico, la complessità asintotica non dice nulla quando n < infinity.
Gordon Gustafson,

15

La citazione è piuttosto vaga e imprecisa. Esistono almeno tre modi correlati in cui può essere interpretato.

Il punto matematico letterale alla base è che, se sei interessato solo ad istanze di dimensioni fino a un certo limite, allora ci sono solo finitamente molte possibili istanze. Ad esempio, ci sono solo molti grafici finiti su un massimo di cento vertici. Se ci sono solo un numero finito di istanze, in linea di principio puoi risolvere il problema semplicemente costruendo una tabella di ricerca di tutte le risposte a tutte le possibili istanze. Ora puoi trovare la risposta controllando prima che l'input non sia troppo grande (il che richiede un tempo costante: se l'input è più lungo di k, non è valido), quindi cerca la risposta nella tabella (che richiede tempo costante: esiste un numero fisso di voci nella tabella). Si noti, tuttavia, che la dimensione effettiva della tabella è probabilmente non misurabile. Ho detto che ci sono solo un numero finito di grafici su cento vertici ed è vero. È solo che il numero finito è maggiore del numero di atomi nell'universo osservabile.

Un punto più pratico è che, quando diciamo che il tempo di esecuzione di un algoritmo è , che solo significa che è asintoticamente  passi, per qualche costante  . Cioè, c'è una costante tale che, per tutti , l'algoritmo compie circa passi. Ma forse e ti interessano solo istanze di dimensioni molto inferiori. Il limite quadratico asintotico potrebbe non applicarsi nemmeno alle tue piccole istanze. Potresti essere fortunato e potrebbe essere più veloce su piccoli input (o potresti essere sfortunato e averlo più lento). Ad esempio, per piccolo  ,c n 2 C n 0 n n 0 c n 2 n 0 = 100 , 000 , 000 n n 2 < 1000 n O ( n 2.3729 ) O ( n 2.8074 )Θ(n2) cn2Cn0nn0cn2n0=100,000,000nn2<1000nquindi preferiresti eseguire un algoritmo quadratico con costanti buone piuttosto che un algoritmo lineare con costanti cattive. Un esempio nella vita reale di ciò è che gli algoritmi di moltiplicazione di matrice asintoticamente più efficienti (varianti di Coppersmith – Winograd , in esecuzione nel tempo ) sono usati raramente in pratica perché di Strassen l'algoritmo è più veloce a meno che le tue matrici non siano davvero grandi.O(n2.3729) O(n2.8074)

Un terzo punto è che, se  è piccolo, e anche  sono piccoli. Ad esempio, se devi ordinare alcune migliaia di elementi di dati e devi ordinarli solo una volta, qualsiasi algoritmo di ordinamento è abbastanza buono: un algoritmo di avrà ancora bisogno forse solo di alcune decine di milioni di istruzioni per ordinare i tuoi dati, il che non richiede molto tempo su una CPU in grado di eseguire miliardi di istruzioni al secondo. OK, ci sono anche accessi alla memoria, ma anche un algoritmo lento richiederà meno di un secondo, quindi è probabilmente meglio usare un algoritmo semplice e lento e farlo bene piuttosto che usare un algoritmo complesso e veloce e scoprire che è velocissimo ma difettoso e in realtà non ordina i dati correttamente.n 2 n 3 Θ ( n 2 )nn2n3Θ(n2)


4
Mentre punti perfettamente corretti e validi, penso che tu abbia perso il punto. Sembra che intendessero dire che a volte l'algoritmo con funziona meglio di un algoritmo con , per abbastanza piccoli . Ciò accade, ad esempio, quando il primo ha un tempo di esecuzione di , mentre il secondo ha un tempo di esecuzione di . Quindi, per abbastanza piccolo, è effettivamente più veloce utilizzare il protocollo . O ( 1 ) n 10 n + 50 100000 n O ( n )O(n)O(1)n10n+50100000nO(n)
Ran G.

@Suonò. Non rientra nel mio secondo caso? (Soprattutto se lo modifico per dire qualcosa di più simile a "Un algoritmo lineare con costanti buone potrebbe battere un algoritmo costante / logaritmico con costanti cattive"?)
David Richerby,

1
Sarebbe bene menzionare esplicitamente l'importanza delle costanti quando n è piccolo. È qualcosa che probabilmente non verrebbe in mente a qualcuno che non l'ha mai sentito prima.
Rob Watts,

9

f(n)=O(n2)n0f(n)<cn2n>n0

cn2n2+1018

D'altra parte, se si incontrano solo i valori n = 1, 2 e 3, in pratica non fa differenza rispetto a f (n) per n ≥ 4, quindi si può anche considerare che f ( n) = O (1), con c = max (f (1), f (2), f (3)). Ed è ciò che significa sufficientemente piccolo: se l'affermazione che f (n) = O (1) non ti inganna se gli unici valori di f (n) che incontri sono "sufficientemente piccoli".


5

Se non cresce, è O (1)

La dichiarazione dell'autore è un po 'assiomatica.

Gli ordini di crescita descrivono cosa succede alla quantità di lavoro che devi fare quando Naumenta. Se sai che Nnon aumenta, il tuo problema è efficace O(1).

Ricorda che O(1)non significa "veloce". È un algoritmo che richiede sempre 1 trilione di passaggi per essere completato O(1). Un algoritmo che richiede ovunque 1-200 passaggi, ma mai di più, lo è O(1). [1]

Se il tuo algoritmo esegue esattamente i N ^ 3passaggi e sai che Nnon può essere più di 5, non può mai fare più di 125 passaggi, quindi è efficace O(1).

Ma ancora una volta, O(1)non significa necessariamente "abbastanza veloce". Questa è una domanda separata che dipende dal tuo contesto. Se ci vuole una settimana per finire qualcosa, probabilmente non ti importa se è tecnicamente O(1).


[1] Ad esempio, la ricerca in un hash lo è O(1), anche se le collisioni di hash significano che potresti dover esaminare diversi oggetti in un bucket, purché ci sia un limite rigido al numero di elementi in quel bucket.


1
Tutto ciò sembra valido, tranne per questo: "Se il tuo algoritmo prende esattamente N ^ 3 passi e sai che N non può essere più di 5, non può mai fare più di 125 passi, quindi è O (1)." . Ancora una volta, se un algoritmo accetta un numero intero e il mio supporto per numero intero massimo è 32767, è O (1)? Ovviamente no. Big-O non cambia in base ai limiti dei parametri. È O (n) anche se sai che 0 <n <3 perché n = 2 richiede il doppio del tempo n = 1.
JSobell,

3
@JSobell Ma è O (1). Se c'è una limitazione che limita la tua n per f (n), significa che non può crescere indefinitamente. Se il tuo n è limitato da 2 ^ 15 la tua grande funzione n ^ 2 è in realtà g(n) = min(f(2^15), f(n))- che è in O (1). Detto questo, in pratica le costanti contano molto e chiaramente n possono diventare abbastanza grandi da rendere utile un'analisi asintotica.
Voo

2
@JSobell Questo è simile alla domanda se i computer siano veramente "Turing Complete", dato che tecnicamente non possono avere uno spazio di archiviazione infinito. Tecnicamente, matematicamente, un computer non è un "vero" Turing Machine. In pratica, non esiste un "nastro infinito", ma i dischi rigidi si avvicinano abbastanza.
Kyle Strand,

Ho scritto diversi anni fa un sistema di Rischio finanziario che comportava n ^ 5 manipolazioni di matrici, quindi avevo un limite pratico di n = 20 prima che le risorse diventassero un problema.
JSobell,

Spiacenti, premere Invio troppo presto. Ho scritto diversi anni fa un sistema di Rischio finanziario che comportava n ^ 5 manipolazioni di matrici, quindi avevo un limite pratico di n = 20 prima che le risorse diventassero un problema. Secondo questa logica imperfetta, la funzione creata è O (1) perché ho un limite di 20. Quando il client dice "Hmm, forse dovremmo spostarlo su 40 come limite ... Sì, l'algoritmo è O (1 ) quindi non è un problema "... Ecco perché i limiti di un input sono privi di significato. La funzione era O (n ^ 5), non O (1), e questo è un esempio pratico del perché Big-O è indipendente dai limiti.
JSobell,

2

Ora posso usare una tabella hash e avere ricerche O (1) (tralasciando l'implementazione specifica della tabella hash), ma se avessi, ad esempio, un elenco, avrei delle ricerche O (n). Dato questo assioma, questi due sono gli stessi se le raccolte sono abbastanza piccole. Ma ad un certo punto divergono ... qual è quel punto?

In pratica, è il punto in cui la creazione della tabella hash prende più del vantaggio che si ottiene dalle ricerche migliorate. Questo varierà molto in base alla frequenza con cui stai effettuando la ricerca, rispetto alla frequenza con cui stai facendo altre cose. O (1) vs O (10) non è un grosso problema se lo fai una volta. Se lo fai migliaia di volte al secondo, anche quello conta (anche se almeno conta a un ritmo lineare in aumento).


Se vuoi essere sicuro, fai alcuni esperimenti per vedere quale struttura di dati è migliore per i tuoi parametri.
Yuval Filmus,

@Telastyn Yuval Filmus ha ragione, se vuoi davvero esserne sicuro. Conosco una persona di nome Jim, i suoi parametri sono ok. Ma non ha ascoltato consigli come quello di Yuval. Dovresti davvero ascoltare Yuval per essere sicuro e sicuro.
Informato il

2

Mentre la citazione è vera (ma vaga) ci sono anche pericoli. Quindi dovresti guardare la complessità in ogni fase della tua applicazione.

È fin troppo facile da dire: hey ho solo un piccolo elenco, se voglio controllare se l'articolo A è nell'elenco, scriverò solo un semplice ciclo per attraversare l'elenco e confrontare gli elementi.

Quindi il tuo buddyprogrammer arriva deve usare l'elenco, vede la tua funzione ed è come: hey non voglio duplicati nell'elenco, quindi usa la funzione per ogni elemento aggiunto all'elenco.

(intendiamoci, è ancora un piccolo elenco di scenari.)

3 anni dopo arrivo e il mio capo ha appena fatto una grande vendita: il nostro software verrà utilizzato da un grande rivenditore nazionale. Prima servivamo solo piccoli negozi. E ora il mio capo mi viene a dire parolacce e urla, perché il software, che ha sempre "funzionato bene" ora è terribilmente lento.

A quanto pare, quell'elenco era un elenco di clienti, ei nostri clienti avevano solo forse 100 clienti, quindi nessuno se ne accorse. L'operazione di compilazione dell'elenco era sostanzialmente un'operazione O (1), poiché impiegava meno di un millisecondo. Bene, non tanto quando ci sono 10.000 clienti da aggiungere ad esso.

E anni dopo la cattiva decisione O (1) originale, la società ha quasi perso un grande cliente. Tutto a causa di un piccolo errore di progettazione / ipotesi anni prima.


Ma illustra anche un'importante caratteristica di molti sistemi del mondo reale: gli "algoritmi" che apprendi come studente universitario sono in realtà pezzi da cui sono fatti veri e propri "algoritmi". Questo è di solito accennato; per esempio, la maggior parte delle persone sa che quicksort è spesso scritto per ricorrere all'ordinamento di inserzione quando le partizioni diventano abbastanza piccole e che la ricerca binaria è spesso scritta per ricadere nella ricerca lineare. Ma non molte persone si rendono conto che unire l'ordinamento può beneficiare di una ricerca binaria.
Pseudonimo,

1

La motivazione per farlo si basa sull'idea errata che O (1) sia sempre migliore di O (lg n), sia sempre migliore di O (n). L'ordine asintotico di un'operazione è rilevante solo se in condizioni realistiche la dimensione del problema diventa effettivamente grande.

Se ho due algoritmi con questi tempi:

  • log (n) 10.000
  • n + 1

Quindi esiste un punto in cui si incrociano. Per di npiù, l'algoritmo "lineare" è più veloce, e per di npiù, l'algoritmo "logaritmico" è più veloce. Molte persone commettono l'errore di presumere che l'algoritmo logaritmico sia più veloce, ma per i piccoli nnon lo è.

Se n rimane piccolo, allora ogni problema è O (1)!

Immagino che cosa significhi qui è che se nè limitato, ogni problema è O (1). Ad esempio, se stiamo ordinando numeri interi, potremmo scegliere di utilizzare Quicksort. O(n*log(n))ovviamente. Ma se decidiamo che non ci potrà mai essere più di 2^64=1.8446744e+19interi, allora sappiamo che n*log(n)<= 1.8446744e+19*log(1.8446744e+19)<= 1.1805916e+21. Pertanto, l'algoritmo richiederà sempre meno di 1.1805916e+21"unità di tempo". Dato che è un tempo costante, possiamo dire che l'algoritmo può sempre essere fatto in quel tempo costante -> O(1). (Nota che anche se quelle unità di tempo sono nanosecondi, è un totale complessivo di oltre 37411 anni). Ma ancora O(1).


0

Sospetto che a molte di queste risposte manchi un concetto fondamentale. O (1): O (n) non è uguale a f (1): f (n) dove f è la stessa funzione, poiché O non rappresenta una singola funzione. Anche il bel grafico di Schwern non è valido perché ha lo stesso asse Y per tutte le linee. Per usare tutti lo stesso asse le linee dovrebbero essere fn1, fn2 e fn3, dove ognuna era una funzione le cui prestazioni potevano essere confrontate direttamente con le altre.

Ho sentito più volte che per valori sufficientemente piccoli di n, O (n) può essere pensato / trattato come se fosse O (1)

Bene, se n = 1 sono esattamente gli stessi? No. Una funzione che consente un numero variabile di iterazioni non ha nulla in comune con una che non ha importanza, la notazione big-O non importa, e nemmeno noi dovremmo.

La notazione Big-O è semplicemente lì per esprimere ciò che accade quando abbiamo un processo iterativo e in che modo le prestazioni (tempo o risorse) si deteriorano all'aumentare di 'n'.

Quindi, per rispondere alla domanda reale ... Direi che coloro che sostengono tale affermazione non comprendono correttamente la notazione Big-O, perché è un confronto illogico.

Ecco una domanda simile: se cerco una sequenza di caratteri e so che in generale le mie stringhe saranno inferiori a 10 caratteri, posso dire che è l'equivalente di O (1), ma se le mie stringhe fossero più lunghe di me direbbe che era O (n)?

No, perché una stringa di 10 caratteri richiede 10 volte più di una stringa di 1 carattere, ma 100 volte meno di una stringa di 1000 caratteri! E 'acceso).


O(1)f(i)imax{f(0),,f(10)}O(1)

Sì, e questo è un esempio di dove la notazione Big-O è comunemente fraintesa. Secondo il tuo argomento, se so che il valore massimo di n è 1.000.000, allora la mia funzione è O (1). In effetti, la mia funzione potrebbe essere al massimo O (1) e nel peggiore dei casi O (n). Questa notazione viene utilizzata per descrivere la complessità algoritmica, non un'implementazione concreta, e usiamo sempre la più costosa per descrivere uno scenario, non la migliore. In effetti, secondo il tuo argomento, ogni singola funzione che consente n <2 è O (1)! :)
JSobell il

n<2O(1)f(n)f(10)nO(1)

Ci dispiace, ma se dici che conoscere i limiti superiori di n fa una funzione O (1), allora stai dicendo che la rappresentazione notazionale è direttamente correlata al valore di n, e non lo è. Tutto il resto che dici è corretto, ma suggerendo che poiché n ha dei limiti è O (1) non è corretto. In pratica ci sono luoghi in cui ciò che descrivi può essere osservabile, ma qui stiamo osservando la notazione Big-O, non la codifica funzionale. Quindi di nuovo, perché suggeriresti che n avere un massimo di 10 lo renderebbe O (1)? Perché 10? Perché non 65535 o 2 ^ 64?
JSobell,

Detto questo, se scrivi una funzione che riempie una stringa di 10 caratteri, quindi passa sempre sopra la stringa, quindi è O (1) perché n è sempre 10 :)
JSobell

0

Credo che il testo che hai citato sia abbastanza inacurrato (l'uso della parola "migliore" di solito è insignificante a meno che tu non fornisca il contesto: in termini di tempo, spazio, ecc.) Comunque, credo che la spiegazione più semplice sarebbe:

O(1)O(1)

Ora prendiamo un set relativamente piccolo di 10 elementi e abbiamo alcuni algoritmi per ordinarlo (solo un esempio). Supponiamo che manteniamo gli elementi in una struttura che ci fornisce anche un algoritmo in grado di ordinare gli elementi in tempo costante. Supponiamo che i nostri algoritmi di ordinamento possano avere le seguenti complessità (con notazione big-O):

  1. O(1)
  2. O(n)
  3. O(nlog(n))
  4. O(n2)

O(1)

Ora "sveliamo" le vere complessità degli algoritmi di ordinamento sopra menzionati (dove "vero" significa non nascondere la costante), rappresentati dal numero di passaggi necessari per terminare (e supponiamo che tutti i passaggi richiedano la stessa quantità di tempo):

  1. 200
  2. 11n
  3. 4nlog(n)
  4. 1n2

Se il nostro input è di dimensione 10, allora si tratta di quantità esatte di passaggi per ogni algoritmo sopra menzionato:

  1. 200
  2. 11×10=110
  3. 4×10×3.32134
  4. 1×100=100

O(n2)O(1),O(n)O(nlog(n))O(n2)O(1)O(n2)O(1)

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.