Cosa farebbe sì che un algoritmo abbia complessità O (log n)?


106

La mia conoscenza del big-O è limitata e quando i termini logaritmici compaiono nell'equazione mi sconcerta ancora di più.

Qualcuno può forse spiegarmi in termini semplici cos'è un O(log n)algoritmo? Da dove viene il logaritmo?

Questo è emerso specificamente quando stavo cercando di risolvere questa domanda di pratica di medio termine:

Siano X (1..n) e Y (1..n) contengono due liste di numeri interi, ciascuno ordinato in ordine non decrescente. Fornisci un algoritmo di tempo O (log n) per trovare la mediana (o l'ennesimo numero intero più piccolo) di tutti i 2n elementi combinati. Ad es. X = (4, 5, 7, 8, 9) e Y = (3, 5, 8, 9, 10), quindi 7 è la mediana della lista combinata (3, 4, 5, 5, 7 , 8, 8, 9, 9, 10). [Suggerimento: utilizza i concetti di ricerca binaria]


29
O(log n)può essere visto come: se raddoppi la dimensione del problema n, il tuo algoritmo necessita solo di un numero costante di passaggi in più.
phimuemue

3
Questo sito web mi ha aiutato a capire la notazione Big O: recursive-design.com/blog/2010/12/07/…
Brad

1
Mi chiedo perché 7 sia la mediana dell'esempio sopra, fwiw potrebbe essere anche 8. Non è un buon esempio, vero?
stryba

13
Un buon modo per pensare agli algoritmi O (log (n)) è che in ogni passaggio riducono della metà la dimensione del problema. Prendi l'esempio della ricerca binaria: in ogni passaggio controlli il valore al centro dell'intervallo di ricerca, dividendo l'intervallo a metà; dopodiché si elimina una delle metà dall'intervallo di ricerca e l'altra metà diventa l'intervallo di ricerca per il passaggio successivo. E così in ogni passaggio il tuo intervallo di ricerca viene dimezzato in termini di dimensioni, quindi O (log (n)) complessità dell'algoritmo. (la riduzione non deve essere esattamente della metà, può essere di un terzo, del 25%, qualsiasi percentuale costante; la metà è più comune)
Krzysztof Kozielczyk

grazie ragazzi, lavorando su un problema precedente e arriveremo presto a questo, apprezziamo molto le risposte! torneremo più tardi per studiarlo
user1189352

Risposte:


290

Devo ammettere che è piuttosto strano la prima volta che vedi un algoritmo O (log n) ... da dove diavolo viene quel logaritmo? Tuttavia, si scopre che ci sono diversi modi in cui è possibile visualizzare un termine di log in notazione O grande. Eccone alcuni:

Divisione ripetuta per una costante

Prendi qualsiasi numero n; diciamo 16. Quante volte puoi dividere n per due prima di ottenere un numero minore o uguale a uno? Per 16, abbiamo quello

16 / 2 = 8
 8 / 2 = 4
 4 / 2 = 2
 2 / 2 = 1

Si noti che questo richiede quattro passaggi per il completamento. È interessante notare che abbiamo anche quel log 2 16 = 4. Hmmm ... che dire di 128?

128 / 2 = 64
 64 / 2 = 32
 32 / 2 = 16
 16 / 2 = 8
  8 / 2 = 4
  4 / 2 = 2
  2 / 2 = 1

Ci sono voluti sette passaggi e log 2 128 = 7. È una coincidenza? No! C'è una buona ragione per questo. Supponiamo di dividere un numero n per 2 i volte. Quindi otteniamo il numero n / 2 i . Se vogliamo risolvere per il valore di i dove questo valore è al massimo 1, otteniamo

n / 2 i ≤ 1

n ≤ 2 i

log 2 n ≤ i

In altre parole, se scegliamo un intero i tale che i ≥ log 2 n, dopo aver diviso n a metà i volte avremo un valore che è al massimo 1. Il più piccolo i per il quale è garantito è approssimativamente log 2 n, quindi se abbiamo un algoritmo che divide per 2 finché il numero non diventa sufficientemente piccolo, allora possiamo dire che termina con O (log n) passi.

Un dettaglio importante è che non importa per quale costante stai dividendo n (purché sia ​​maggiore di uno); se dividi per la costante k, ci vorrà log k n passi per raggiungere 1. Quindi qualsiasi algoritmo che divide ripetutamente la dimensione dell'input per qualche frazione avrà bisogno di O (log n) iterazioni per terminare. Queste iterazioni potrebbero richiedere molto tempo e quindi il net runtime non deve essere O (log n), ma il numero di passaggi sarà logaritmico.

Allora da dove viene questo? Un classico esempio è la ricerca binaria , un algoritmo veloce per cercare un valore in un array ordinato. L'algoritmo funziona in questo modo:

  • Se l'array è vuoto, restituisci che l'elemento non è presente nell'array.
  • Altrimenti:
    • Guarda l'elemento centrale della matrice.
    • Se è uguale all'elemento che stiamo cercando, restituisci il successo.
    • Se è maggiore dell'elemento che stiamo cercando:
      • Getta via la seconda metà della matrice.
      • Ripetere
    • Se è inferiore all'elemento che stiamo cercando:
      • Getta via la prima metà della matrice.
      • Ripetere

Ad esempio, per cercare 5 nell'array

1   3   5   7   9   11   13

Diamo prima un'occhiata all'elemento centrale:

1   3   5   7   9   11   13
            ^

Poiché 7> 5, e poiché l'array è ordinato, sappiamo per certo che il numero 5 non può essere nella metà posteriore dell'array, quindi possiamo semplicemente scartarlo. Questo se ne va

1   3   5

Quindi ora guardiamo l'elemento centrale qui:

1   3   5
    ^

Poiché 3 <5, sappiamo che 5 non può apparire nella prima metà dell'array, quindi possiamo lanciare la prima metà dell'array per lasciare

        5

Di nuovo guardiamo al centro di questo array:

        5
        ^

Poiché questo è esattamente il numero che stiamo cercando, possiamo segnalare che 5 è effettivamente nell'array.

Quindi quanto è efficiente questo? Bene, ad ogni iterazione buttiamo via almeno la metà degli elementi rimanenti dell'array. L'algoritmo si arresta non appena l'array è vuoto o troviamo il valore che vogliamo. Nel peggiore dei casi, l'elemento non è presente, quindi continuiamo a dimezzare la dimensione dell'array finché non esauriamo gli elementi. Quanto tempo ci vuole? Bene, dal momento che continuiamo a tagliare l'array a metà più e più volte, avremo eseguito al massimo O (log n) iterazioni, poiché non possiamo tagliare l'array a metà più di O (log n) volte prima di eseguire elementi dell'array.

Gli algoritmi che seguono la tecnica generale del divide et impera (tagliare il problema in pezzi, risolvere quei pezzi, quindi rimettere insieme il problema) tendono ad avere termini logaritmici in essi per lo stesso motivo: non puoi continuare a tagliare qualche oggetto metà in più di O (log n) volte. Potresti voler considerare l' ordinamento di unione come un ottimo esempio di questo.

Elaborazione dei valori una cifra alla volta

Quante cifre ci sono nel numero in base 10 n? Bene, se ci sono k cifre nel numero, allora avremmo che la cifra più grande è un multiplo di 10 k . Il più grande numero di k cifre è 999 ... 9, k volte, e questo è uguale a 10 k + 1 - 1. Di conseguenza, se sappiamo che n contiene k cifre, allora sappiamo che il valore di n è al massimo 10 k + 1 - 1. Se vogliamo risolvere per k in termini di n, otteniamo

n ≤ 10 k + 1 - 1

n + 1 ≤ 10 k + 1

registro 10 (n + 1) ≤ k + 1

(log 10 (n + 1)) - 1 ≤ k

Da cui si ricava che k è approssimativamente il logaritmo in base 10 di n. In altre parole, il numero di cifre in n è O (log n).

Ad esempio, pensiamo alla complessità dell'aggiunta di due numeri grandi che sono troppo grandi per essere contenuti in una parola macchina. Supponiamo di avere quei numeri rappresentati in base 10 e chiameremo i numeri me n. Un modo per aggiungerli è tramite il metodo della scuola elementare: scrivi i numeri una cifra alla volta, quindi lavora da destra a sinistra. Ad esempio, per aggiungere 1337 e 2065, inizieremo scrivendo i numeri come

    1  3  3  7
+   2  0  6  5
==============

Aggiungiamo l'ultima cifra e portiamo l'1:

          1
    1  3  3  7
+   2  0  6  5
==============
             2

Quindi aggiungiamo la penultima ("penultima") cifra e portiamo l'1:

       1  1
    1  3  3  7
+   2  0  6  5
==============
          0  2

Successivamente, aggiungiamo la terzultima cifra ("terzultima"):

       1  1
    1  3  3  7
+   2  0  6  5
==============
       4  0  2

Infine, aggiungiamo la quartultima cifra ("preantepenultimate" ... I love English):

       1  1
    1  3  3  7
+   2  0  6  5
==============
    3  4  0  2

Ora, quanto lavoro abbiamo fatto? Facciamo un totale di O (1) lavoro per cifra (cioè una quantità costante di lavoro) e ci sono O (max {log n, log m}) cifre totali che devono essere elaborate. Questo dà un totale di complessità O (max {log n, log m}), perché dobbiamo visitare ogni cifra nei due numeri.

Molti algoritmi ottengono un termine O (log n) lavorando una cifra alla volta in una base. Un classico esempio è l' ordinamento digitale , che ordina gli interi una cifra alla volta. Esistono molti tipi di ordinamento digitale, ma di solito vengono eseguiti nel tempo O (n log U), dove U è il numero intero più grande possibile che viene ordinato. La ragione di ciò è che ogni passaggio dell'ordinamento richiede tempo O (n) e ci sono un totale di iterazioni O (log U) necessarie per elaborare ciascuna delle cifre O (log U) del numero più grande da ordinare. Molti algoritmi avanzati, come l'algoritmo dei percorsi più brevi di Gabow o la versione in scala dell'algoritmo del flusso massimo di Ford-Fulkerson , hanno un termine logaritmico nella loro complessità perché lavorano una cifra alla volta.


Per quanto riguarda la tua seconda domanda su come risolvi il problema, potresti dare un'occhiata a questa domanda correlata che esplora un'applicazione più avanzata. Data la struttura generale dei problemi descritti qui, ora puoi avere un'idea migliore di come pensare ai problemi quando sai che nel risultato c'è un termine logaritmico, quindi ti sconsiglio di guardare la risposta finché non l'hai fornita qualche pensiero.

Spero che questo ti aiuti!


8

Quando parliamo di descrizioni big-Oh, di solito parliamo del tempo necessario per risolvere problemi di una determinata dimensione . E di solito, per problemi semplici, quella dimensione è solo caratterizzata dal numero di elementi di input, e di solito è chiamata n, o N. (Ovviamente non è sempre vero - i problemi con i grafici sono spesso caratterizzati da numeri di vertici, V e numero di bordi, E; ma per ora parleremo di elenchi di oggetti, con N oggetti negli elenchi.)

Diciamo che un problema "è grande-Oh di (qualche funzione di N)" se e solo se :

Per ogni N> qualche N_0 arbitrario, esiste una costante c, tale che il tempo di esecuzione dell'algoritmo è inferiore a quella costante c volte (una funzione di N.)

In altre parole, non pensare a piccoli problemi in cui il "sovraccarico costante" della creazione del problema è importante, pensa a grandi problemi. E quando si pensa a grandi problemi, big-Oh di (qualche funzione di N) significa che il tempo di esecuzione è fermo sempre meno di alcuni momenti costanti quella funzione. Sempre.

In breve, quella funzione è un limite superiore, fino a un fattore costante.

Quindi, "big-Oh di log (n)" significa la stessa cosa che ho detto sopra, tranne che "una qualche funzione di N" è sostituita con "log (n)".

Quindi, il tuo problema ti dice di pensare alla ricerca binaria, quindi pensiamoci. Supponiamo di avere, diciamo, un elenco di N elementi ordinati in ordine crescente. Vuoi scoprire se un determinato numero esiste in quella lista. Un modo per fare ciò che non è una ricerca binaria è semplicemente scansionare ogni elemento dell'elenco e vedere se è il tuo numero di destinazione. Potresti essere fortunato e trovarlo al primo tentativo. Ma nel peggiore dei casi, controllerai N volte diverse. Questa non è una ricerca binaria, e non è grande-Oh di log (N) perché non c'è modo di forzarla nei criteri che abbiamo abbozzato sopra.

Puoi scegliere che la costante arbitraria sia c = 10, e se la tua lista ha N = 32 elementi, stai bene: 10 * log (32) = 50, che è maggiore del tempo di esecuzione di 32. Ma se N = 64 , 10 * log (64) = 60, che è inferiore al runtime di 64. Puoi scegliere c = 100, o 1000, o un gazillion, e sarai ancora in grado di trovare qualche N che viola quel requisito. In altre parole, non c'è N_0.

Se facciamo una ricerca binaria, però, scegliamo l'elemento centrale e facciamo un confronto. Quindi buttiamo fuori metà dei numeri e lo facciamo ancora, e ancora, e così via. Se il tuo N = 32, puoi farlo solo circa 5 volte, che è log (32). Se il tuo N = 64, puoi farlo solo circa 6 volte, ecc. Ora puoi scegliere quella costante arbitraria c, in modo tale che il requisito sia sempre soddisfatto per valori grandi di N.

Con tutto questo background, ciò che O (log (N)) di solito significa è che hai un modo per fare una cosa semplice, che dimezza la dimensione del tuo problema. Proprio come la ricerca binaria sta facendo sopra. Una volta tagliato a metà il problema, puoi tagliarlo a metà ancora, ancora e ancora. Ma, in modo critico, ciò che non puoi fare è un passaggio di pre-elaborazione che richiederebbe più tempo di quel tempo O (log (N)). Quindi, ad esempio, non puoi mescolare le tue due liste in una grande lista, a meno che tu non riesca a trovare un modo per farlo anche nel tempo O (log (N)).

(NOTA: Quasi sempre, Log (N) significa log-base-due, che è quello che presumo sopra.)


4

Nella soluzione seguente, tutte le righe con una chiamata ricorsiva vengono eseguite sulla metà delle dimensioni date dei sotto-array di X e Y. Altre righe vengono eseguite in un tempo costante. La funzione ricorsiva è T (2n) = T (2n / 2) + c = T (n) + c = O (lg (2n)) = O (lgn).

Inizi con MEDIAN (X, 1, n, Y, 1, n).

MEDIAN(X, p, r, Y, i, k) 
if X[r]<Y[i]
    return X[r]
if Y[k]<X[p]
    return Y[k]
q=floor((p+r)/2)
j=floor((i+k)/2)
if r-p+1 is even
    if X[q+1]>Y[j] and Y[j+1]>X[q]
        if X[q]>Y[j]
            return X[q]
        else
            return Y[j]
    if X[q+1]<Y[j-1]
        return MEDIAN(X, q+1, r, Y, i, j)
    else
        return MEDIAN(X, p, q, Y, j+1, k)
else
    if X[q]>Y[j] and Y[j+1]>X[q-1]
        return Y[j]
    if Y[j]>X[q] and X[q+1]>Y[j-1]
        return X[q]
    if X[q+1]<Y[j-1]
        return MEDIAN(X, q, r, Y, i, j)
    else
        return MEDIAN(X, p, q, Y, j, k)

3

Il termine Log compare molto spesso nell'analisi della complessità degli algoritmi. Ecco alcune spiegazioni:

1. Come rappresenti un numero?

Prendiamo il numero X = 245436. Questa notazione di "245436" contiene informazioni implicite. Rendere esplicite queste informazioni:

X = 2 * 10 ^ 5 + 4 * 10 ^ 4 + 5 * 10 ^ 3 + 4 * 10 ^ 2 + 3 * 10 ^ 1 + 6 * 10 ^ 0

Che è l'espansione decimale del numero. Quindi, la quantità minima di informazioni di cui abbiamo bisogno per rappresentare questo numero è di 6 cifre. Non è una coincidenza, poiché qualsiasi numero inferiore a 10 ^ d può essere rappresentato in d cifre.

Quindi quante cifre sono necessarie per rappresentare X? È uguale al massimo esponente di 10 in X più 1.

==> 10 ^ d> X
==> log (10 ^ d)> log (X)
==> d * log (10)> log (X)
==> d> log (X) // E appare il log di nuovo ...
==> d = floor (log (x)) + 1

Si noti inoltre che questo è il modo più conciso per indicare il numero in questo intervallo. Qualsiasi riduzione porterà alla perdita di informazioni, poiché una cifra mancante può essere mappata su altri 10 numeri. Ad esempio: 12 * può essere mappato a 120, 121, 122,…, 129.

2. Come si cerca un numero in (0, N - 1)?

Prendendo N = 10 ^ d, usiamo la nostra osservazione più importante:

La quantità minima di informazioni per identificare in modo univoco un valore in un intervallo compreso tra 0 e N - 1 = log (N) cifre.

Ciò implica che, quando viene chiesto di cercare un numero sulla riga intera, compreso tra 0 e N - 1, è necessario che almeno log (N) cerchi di trovarlo. Perché? Qualsiasi algoritmo di ricerca dovrà scegliere una cifra dopo l'altra nella ricerca del numero.

Il numero minimo di cifre che deve scegliere è log (N). Quindi il numero minimo di operazioni intraprese per cercare un numero in uno spazio di dimensione N è log (N).

Riuscite a indovinare le complessità dell'ordine della ricerca binaria, della ricerca ternaria o della ricerca deca?
È O (log (N))!

3. Come si ordina una serie di numeri?

Quando viene chiesto di ordinare un insieme di numeri A in un array B, ecco come appare ->

Permute Elements

Ogni elemento dell'array originale deve essere mappato al suo indice corrispondente nell'array ordinato. Quindi, per il primo elemento, abbiamo n posizioni. Per trovare correttamente l'indice corrispondente in questo intervallo da 0 a n - 1, abbiamo bisogno di… operazioni log (n).

L'elemento successivo necessita di operazioni di registro (n-1), il registro successivo (n-2) e così via. Il totale risulta essere:

==> log (n) + log (n - 1) + log (n - 2) +… + log (1)

Utilizzando log (a) + log (b) = log (a * b),

==> log (n!)

Questo può essere approssimato a nlog (n) - n.
Che è O (n * log (n))!

Quindi concludiamo che non ci può essere alcun algoritmo di ordinamento che possa fare meglio di O (n * log (n)). E alcuni algoritmi con questa complessità sono i popolari Merge Sort e Heap Sort!

Questi sono alcuni dei motivi per cui vediamo apparire così spesso log (n) nell'analisi della complessità degli algoritmi. Lo stesso può essere esteso ai numeri binari. Ho fatto un video su questo qui.
Perché log (n) appare così spesso durante l'analisi della complessità dell'algoritmo?

Saluti!


2

Chiamiamo la complessità temporale O (log n), quando la soluzione è basata su iterazioni su n, dove il lavoro svolto in ogni iterazione è una frazione dell'iterazione precedente, poiché l'algoritmo lavora verso la soluzione.


1

Non posso ancora commentare ... necro lo è! La risposta di Avi Cohen non è corretta, prova:

X = 1 3 4 5 8
Y = 2 5 6 7 9

Nessuna delle condizioni è vera, quindi MEDIAN (X, p, q, Y, j, k) taglierà entrambi i cinque. Queste sono sequenze non decrescenti, non tutti i valori sono distinti.

Prova anche questo esempio di lunghezza pari con valori distinti:

X = 1 3 4 7
Y = 2 5 6 8

Ora MEDIAN (X, p, q, Y, j + 1, k) taglierà i quattro.

Invece offro questo algoritmo, chiamalo con MEDIAN (1, n, 1, n):

MEDIAN(startx, endx, starty, endy){
  if (startx == endx)
    return min(X[startx], y[starty])
  odd = (startx + endx) % 2     //0 if even, 1 if odd
  m = (startx+endx - odd)/2
  n = (starty+endy - odd)/2
  x = X[m]
  y = Y[n]
  if x == y
    //then there are n-2{+1} total elements smaller than or equal to both x and y
    //so this value is the nth smallest
    //we have found the median.
    return x
  if (x < y)
    //if we remove some numbers smaller then the median,
    //and remove the same amount of numbers bigger than the median,
    //the median will not change
    //we know the elements before x are smaller than the median,
    //and the elements after y are bigger than the median,
    //so we discard these and continue the search:
    return MEDIAN(m, endx, starty, n + 1 - odd)
  else  (x > y)
    return MEDIAN(startx, m + 1 - odd, n, endy)
}
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.