Questa domanda precedente affronta alcuni dei fattori che potrebbero far sì che un algoritmo abbia complessità O (log n).
Cosa farebbe sì che un algoritmo abbia complessità temporale O (log log n)?
Questa domanda precedente affronta alcuni dei fattori che potrebbero far sì che un algoritmo abbia complessità O (log n).
Cosa farebbe sì che un algoritmo abbia complessità temporale O (log log n)?
Risposte:
I termini O (log log n) possono essere visualizzati in una varietà di luoghi diversi, ma in genere ci sono due percorsi principali che arriveranno a questo runtime.
Come menzionato nella risposta alla domanda collegata, un modo comune per un algoritmo di avere complessità temporale O (log n) è che quell'algoritmo funzioni riducendo ripetutamente la dimensione dell'input di qualche fattore costante ad ogni iterazione. Se questo è il caso, l'algoritmo deve terminare dopo O (log n) iterazioni, perché dopo aver eseguito O (log n) divisioni per una costante, l'algoritmo deve ridurre la dimensione del problema fino a 0 o 1. Questo è il motivo, ad esempio , la ricerca binaria ha complessità O (log n).
È interessante notare che esiste un modo simile per ridurre le dimensioni di un problema che produce tempi di esecuzione della forma O (log log n). Invece di dividere l'input a metà su ogni livello, cosa succede se prendiamo la radice quadrata della dimensione su ogni livello?
Ad esempio, prendiamo il numero 65.536. Quante volte dobbiamo dividerlo per 2 finché non arriviamo a 1? Se lo facciamo, otteniamo
Questo processo richiede 16 passaggi ed è anche il caso che 65.536 = 2 16 .
Ma se prendiamo la radice quadrata a ogni livello, otteniamo
Notare che sono necessari solo quattro passaggi per arrivare fino a 2. Perché?
Innanzitutto, una spiegazione intuitiva. Quante cifre ci sono nei numeri ne √n? Ci sono approssimativamente log n cifre nel numero n, e approssimativamente log (√n) = log (n 1/2 ) = (1/2) log n cifre in √n. Ciò significa che, ogni volta che prendi una radice quadrata, stai approssimativamente dimezzando il numero di cifre nel numero. Poiché puoi solo dimezzare una quantità k O (log k) volte prima che scenda a una costante (diciamo 2), questo significa che puoi prendere solo radici quadrate O (log log n) volte prima di ridurre il numero verso il basso a qualche costante (diciamo 2).
Ora, facciamo un po 'di matematica per renderlo rigoroso. Le'ts riscrive la sequenza precedente in termini di potenze di due:
Si noti che abbiamo seguito la sequenza 2 16 → 2 8 → 2 4 → 2 2 → 2 1 . Ad ogni iterazione, dimezziamo l'esponente della potenza di due. È interessante, perché si ricollega a ciò che già sappiamo: puoi solo dividere il numero k per metà O (log k) volte prima che scenda a zero.
Quindi prendi un numero qualsiasi n e scrivilo come n = 2 k . Ogni volta che si prende la radice quadrata di n, si dimezza l'esponente in questa equazione. Pertanto, possono essere applicate solo radici quadrate O (log k) prima che k scenda a 1 o inferiore (nel qual caso n scende a 2 o inferiore). Poiché n = 2 k , ciò significa che k = log 2 n, e quindi il numero di radici quadrate prese è O (log k) = O (log log n). Pertanto, se è presente un algoritmo che funziona riducendo ripetutamente il problema a un sottoproblema di dimensione che è la radice quadrata della dimensione del problema originale, tale algoritmo terminerà dopo O (log log n) passaggi.
Un esempio nel mondo reale di questo è l' albero di van Emde Boas(vEB-tree) struttura dati. Un vEB-tree è una struttura dati specializzata per memorizzare interi nell'intervallo 0 ... N - 1. Funziona come segue: il nodo radice dell'albero ha puntatori √N, dividendo l'intervallo 0 ... N - 1 in √N bucket ciascuno contenente un intervallo di circa √N numeri interi. Questi bucket vengono quindi suddivisi internamente in √ (√ N) bucket, ciascuno dei quali contiene all'incirca √ (√ N) elementi. Per attraversare l'albero, si inizia dalla radice, si determina a quale bucket si appartiene, quindi si prosegue ricorsivamente nella sottostruttura appropriata. A causa del modo in cui è strutturato l'albero vEB, puoi determinare in O (1) tempo in quale sottostruttura scendere, e così dopo O (log log N) passi raggiungerai il fondo dell'albero. Di conseguenza, le ricerche in un albero vEB richiedono solo tempo O (log log N).
Un altro esempio è l' algoritmo della coppia di punti più vicina di Hopcroft-Fortune . Questo algoritmo tenta di trovare i due punti più vicini in una raccolta di punti 2D. Funziona creando una griglia di bucket e distribuendo i punti in tali bucket. Se in qualsiasi punto dell'algoritmo viene trovato un bucket che contiene più di √N punti, l'algoritmo elabora ricorsivamente quel bucket. La profondità massima della ricorsione è quindi O (log log n) e utilizzando un'analisi dell'albero di ricorsione si può dimostrare che ogni strato dell'albero funziona O (n). Pertanto, il tempo di esecuzione totale dell'algoritmo è O (n log log n).
Esistono altri algoritmi che raggiungono tempi di esecuzione O (log log n) utilizzando algoritmi come la ricerca binaria su oggetti di dimensione O (log n). Ad esempio, la struttura dati x-fast trie esegue una ricerca binaria sui livelli dell'albero di altezza O (log U), quindi il runtime per alcune delle sue operazioni è O (log log U). Il trie y-fast correlato ottiene alcuni dei suoi tempi di esecuzione O (log log U) mantenendo BST bilanciati di O (log U) nodi ciascuno, consentendo alle ricerche in quegli alberi di essere eseguite nel tempo O (log log U). L' albero di tango e le relative strutture di dati dell'albero multisplay finiscono con un termine O (log log n) nelle loro analisi perché mantengono alberi che contengono O (log n) elementi ciascuno.
Altri algoritmi raggiungono il runtime O (log log n) in altri modi. La ricerca per interpolazione prevedeva che il runtime O (log log n) trovasse un numero in un array ordinato, ma l'analisi è abbastanza coinvolta. In definitiva, l'analisi funziona mostrando che il numero di iterazioni è uguale al numero k tale che n 2 -k ≤ 2, per cui log log n è la soluzione corretta. Alcuni algoritmi, come l' algoritmo MST Cheriton-Tarjan , arrivano a un runtime che coinvolge O (log log n) risolvendo un complesso problema di ottimizzazione vincolata.
Spero che questo ti aiuti!
Un modo per vedere il fattore O (log log n) nella complessità temporale è per divisione come cose spiegate nell'altra risposta, ma c'è un altro modo per vedere questo fattore, quando vogliamo fare uno scambio tra tempo e spazio / tempo e approssimazione / tempo e durezza / ... di algoritmi e abbiamo qualche iterazione artificiale sul nostro algoritmo.
Ad esempio SSSP (Single source shortest path) ha un algoritmo O (n) sui grafi planari, ma prima di quel complicato algoritmo c'era un algoritmo molto più semplice (ma comunque piuttosto difficile) con tempo di esecuzione O (n log log n), il la base dell'algoritmo è la seguente (solo una descrizione molto approssimativa, e mi offrirei di saltare la comprensione di questa parte e leggere l'altra parte della risposta):
Ma il punto è che qui scegliamo che la divisione sia di dimensione O (log n / (log log n)). Se scegliamo altre divisioni come O (log n / (log log n) ^ 2) che potrebbe essere più veloce e portare un altro risultato. Voglio dire, in molti casi (come in algoritmi di approssimazione o algoritmi randomizzati, o algoritmi come SSSP come sopra), quando iteriamo su qualcosa (sottoproblemi, possibili soluzioni, ...), scegliamo il numero di iterazione corrispondente al commercio di quello abbiamo (tempo / spazio / complessità dell'algoritmo / fattore costante dell'algoritmo, ...). Quindi forse vediamo cose più complicate di "log log n" in algoritmi di lavoro reali.