È questa una "regola" corretta per identificare la notazione "Big O" di un algoritmo?


30

Ho imparato di più sulla Big O Notation e su come calcolarla in base a come è scritto un algoritmo. Mi sono imbattuto in una serie interessante di "regole" per il calcolo di una notazione Big O algoritmi e volevo vedere se sono sulla buona strada o fuori strada.

Notazione O grande: N

function(n) {
    For(var a = 0; i <= n; i++) { // It's N because it's just a single loop
        // Do stuff
    }
}

Notazione O grande: N 2

function(n, b) {
    For(var a = 0; a <= n; a++) {
        For(var c = 0; i <= b; c++) { // It's N squared because it's two nested loops
            // Do stuff
        }
    }
}

Notazione O grande: 2N

function(n, b) {
    For(var a = 0; a <= n; a++) {
        // Do stuff
    }
    For(var c = 0; i <= b; c++) { // It's 2N the loops are outside each other
        // Do stuff
    }
}

Notazione O grande: NLogN

function(n) {
    n.sort(); // The NLogN comes from the sort?
    For(var a = 0; i <= n; i++) {
        // Do stuff
    }
}

I miei esempi e la notazione successiva sono corretti? Ci sono ulteriori notazioni di cui dovrei essere a conoscenza?


3
Chiamalo una regola empirica anziché una formula e probabilmente sei sulla strada giusta. Certo, dipende completamente da cosa fa esattamente "fare cose". Log (N) in genere proviene da algoritmi che eseguono una sorta di partizionamento binario / ad albero. Ecco un eccellente post sul blog sull'argomento.
Daniel B,

15
Non esiste una cosa come 2Nnella notazione big-O.
vartec,

15
@ JörgWMittag perché O (2n) = O (n) per definizione di Big O
maniaco del cricchetto

3
@ JörgWMittag: questo non è davvero il luogo per la pesca a traina.
vartec,

3
@vartec - Non credo che JörgWMittag stia trollando intenzionalmente. Nella mia recente ricerca, ho notato molta confusione tra la rigida notazione Big-O e il "vernacolo comune" che mescola Big-O, Theta e altri derivati. Non sto dicendo che l'uso comune sia corretto; solo che succede molto.

Risposte:


27

Formalmente, la notazione big-O descrive il grado di complessità.

Per calcolare la notazione big-O:

  1. identificare la formula per la complessità dell'algoritmo. Diciamo, ad esempio, due loop con un altro nidificato all'interno, quindi altri tre loop non nidificati:2N² + 3N
  2. rimuovi tutto tranne il termine più alto: 2N²
  3. rimuove tutte le costanti:

In altre parole, due loop con un altro nidificato all'interno, quindi altri tre loop non nidificati è O (N²)

Questo ovviamente presuppone che ciò che hai nei tuoi loop siano semplici istruzioni. Se hai ad esempio sort()all'interno del ciclo, dovrai moltiplicare la complessità del ciclo per la complessità dell'implementazione utilizzata dalla sort()tua lingua / libreria sottostante.


A rigor di termini "rimuovere tutte le costanti" si trasformerebbe 2N³in N. "rimuovere tutte le costanti additive e moltiplicative" sarebbe più vicino alla verità.
Joachim Sauer,

@JoachimSauer: N² = N * N, non c'è alcuna costante lì.
vartec,

@vartec: secondo lo stesso argomento 2N = N+N.
Joachim Sauer,

2
@JoachimSauer, il tuo "rigorosamente parlando" come assolutamente non convenzionale. Vedi en.wikipedia.org/wiki/Constant_(mathematics) . Quando si parla di polinomi, "costante" si riferisce sempre solo ai coefficienti, non agli esponenti.
Ben Lee,

1
@vartec, vedi il mio commento sopra. Il tuo uso di "costante" qui è stato assolutamente corretto e convenzionale.
Ben Lee,

7

Se vuoi analizzare questi algoritmi devi definire // dostuff, in quanto ciò può davvero cambiare il risultato. Supponiamo che Dostuff richieda un numero costante di operazioni O (1).

Ecco alcuni esempi con questa nuova notazione:

Per il tuo primo esempio, l'attraversamento lineare: questo è corretto!

SU):

for (int i = 0; i < myArray.length; i++) {
    myArray[i] += 1;
}

Perché è lineare (O (n))? Man mano che aggiungiamo ulteriori elementi all'input (array), la quantità di operazioni in corso aumenta proporzionalmente al numero di elementi che aggiungiamo.

Quindi, se ci vuole un'operazione per incrementare un numero intero da qualche parte nella memoria, possiamo modellare il lavoro che il ciclo fa con f (x) = 5x = 5 operazioni aggiuntive. Per 20 elementi aggiuntivi, eseguiamo 20 operazioni aggiuntive. Un singolo passaggio di un array tende ad essere lineare. Quindi sono algoritmi come l'ordinamento bucket, che sono in grado di sfruttare la struttura dei dati per eseguire un ordinamento in un singolo passaggio di un array.

Anche il tuo secondo esempio sarebbe corretto e si presenta così:

O (N ^ 2):

for (int i = 0; i < myArray.length; i++) {
    for (int j = 0; j < myArray.length; j++) {
        myArray[i][j] += 1;
    }
}

In questo caso, per ogni elemento aggiuntivo nel primo array, i, dobbiamo elaborare TUTTO j. L'aggiunta di 1 a i aggiunge effettivamente (lunghezza di j) a j. Quindi, hai ragione! Questo modello è O (n ^ 2), o nel nostro esempio è in realtà O (i * j) (o n ^ 2 se i == j, che è spesso il caso delle operazioni con matrici o di una struttura di dati quadrata.

Il tuo terzo esempio è dove le cose cambiano a seconda del cibo; Se il codice è come scritto e fare cose è una costante, in realtà è solo O (n) perché abbiamo 2 passaggi di un array di dimensioni n e 2n si riduce a n. I loop che si trovano uno fuori dall'altro non sono il fattore chiave che può produrre codice 2 ^ n; ecco un esempio di una funzione che è 2 ^ n:

var fibonacci = function (n) {
    if (n == 1 || n == 2) {
        return 1;
    }

    else {
        return (fibonacci(n-2) + fibonacci(n-1));
    }
}

Questa funzione è 2 ^ n, poiché ogni chiamata alla funzione produce DUE chiamate aggiuntive alla funzione (Fibonacci). Ogni volta che chiamiamo la funzione, la quantità di lavoro che dobbiamo fare raddoppia! Questo cresce molto rapidamente, come tagliare la testa di un'idra e far spuntare due nuovi ogni volta!

Per il tuo esempio finale, se stai usando un ordinamento nlgn come merge-sort, hai ragione che questo codice sarà O (nlgn). Tuttavia, è possibile sfruttare la struttura dei dati per sviluppare ordinamenti più rapidi in situazioni specifiche (come in un intervallo di valori noto e limitato come 1-100). Si ha ragione nel pensare, tuttavia, che domini il codice di ordine più elevato; quindi se un ordinamento O (nlgn) è vicino a qualsiasi operazione che impiega meno del tempo O (nlgn), la complessità del tempo totale sarà O (nlgn).

In JavaScript (almeno in Firefox) l'ordinamento predefinito in Array.prototype.sort () è effettivamente MergeSort, quindi stai guardando O (nlgn) per il tuo scenario finale.


Il tuo esempio di Fibonacci è in realtà Fibonacci? So che questo non discute contro il punto che stavi cercando di fare, ma il nome potrebbe essere fuorviante per gli altri e quindi essere fonte di distrazione se non è in realtà Fibonacci.
Paul Nikonowicz,

1

Il tuo secondo esempio (loop esterno da 0 a n , loop interno da 0 a b ) sarebbe O ( nb ), non O ( n 2 ). La regola è che stai calcolando qualcosa n volte, e per ognuno di quelli stai calcolando qualcos'altro b volte, quindi la crescita di questa funzione dipende esclusivamente dalla crescita di n * b .

Il tuo terzo esempio è solo O ( n ): puoi rimuovere tutte le costanti poiché non crescono con n e la crescita è la notazione di Big-O.

Per quanto riguarda il tuo ultimo esempio, sì, la tua notazione Big-O verrà sicuramente dal metodo di ordinamento che sarà, se è basato sul confronto (come in genere il caso), nella sua forma più efficiente, O ( n * logn ) .


0

Ricordiamo che questa è una rappresentazione approssimativa del tempo di esecuzione. La "regola empirica" ​​è approssimativa perché imprecisa ma fornisce una buona approssimazione del primo ordine a fini di valutazione.

Il tempo di esecuzione effettivo dipenderà dalla quantità di spazio heap, dalla velocità del processore, dal set di istruzioni, dall'uso di prefisso o dagli operatori di incremento post-correzione, ecc., Yadda. Una corretta analisi del tempo di esecuzione consentirà la determinazione dell'accettazione, ma la conoscenza delle nozioni di base consente di programmare fin dall'inizio.

Sono d'accordo che tu sia sulla buona strada per capire come Big-O è razionalizzato da un libro di testo a un'applicazione pratica. Questo potrebbe essere il difficile ostacolo da superare.

Il tasso di crescita asintotico diventa importante su set di dati di grandi dimensioni e programmi di grandi dimensioni, quindi per esempi tipici si dimostra che non è altrettanto importante per la sintassi e la logica appropriate.


-1

Grande oh, per definizione significa: per una funzione f (t) esiste una funzione c * g (t) dove c è una costante arbitraria tale che f (t) <= c * g (t) per t> n dove n è una costante arbitraria, quindi f (t) esiste in O (g (t)). Questa è una notazione matematica che viene utilizzata nell'informatica per analizzare gli algoritmi. Se sei confuso, consiglierei di esaminare le relazioni di chiusura, in questo modo puoi vedere in una visione più dettagliata come questi algoritmi ottengono questi valori di grande oh.

Alcune conseguenze di questa definizione: O (n) è in realtà congruente con O (2n).

Inoltre ci sono molti diversi tipi di algoritmi di ordinamento. Il valore minimo di Big-Oh per un ordinamento di confronto è O (nlogn) ma ci sono molti tipi con big-oh peggiore. Ad esempio, l'ordinamento per selezione ha O (n ^ 2). Alcuni ordinamenti senza confronto potrebbero mai avere valori big-oh migliori. Un ordinamento bucket, ad esempio, ha O (n).

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.