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!
O(log n)
può essere visto come: se raddoppi la dimensione del probleman
, il tuo algoritmo necessita solo di un numero costante di passaggi in più.