2015-01-20 20 views
6

ho letto here dalla risposta accettata che una std :: deque ha la seguente caratteristicaCome fa deque hanno un ammortamento costante complessità temporale

1- Random access - constant O(1) 
2- Insertion or removal of elements at the end or beginning - amortized constant O(1) 
3- Insertion or removal of elements - linear O(n) 

La mia domanda riguarda il punto 2. Come può un deque avere un ammortizzato inserimento costante alla fine o all'inizio?

Capisco che uno std::vector ha una complessità a tempo costante ammortizzato per gli inserimenti alla fine. Questo perché un vettore è continguente ed è un array dinamico. Quindi, quando esaurisce la memoria per uno push_back alla fine, allocherà un intero nuovo blocco di memoria, copierà gli elementi esistenti dalla vecchia posizione alla nuova posizione e quindi eliminerà gli elementi dalla vecchia posizione. Questa operazione capisco è ammortizzata costante. Come si applica a un deque? Come possono essere ammortizzate le inserzioni nella parte superiore e inferiore di una deque. Avevo l'impressione che doveva essere costante O (1). So che una deque è composta da blocchi di memoria.

+1

si applica lo stesso principio di 'std :: vector'. L'operazione è 'O (1)', con casi che richiedono una nuova allocazione quando gli slot allocati in precedenza sono pieni. – njzk2

+0

@ njzk2 ** Non è necessario copiare i vecchi valori. Basta allocare un nuovo blocco * di dimensioni fisse * e collegarlo all'inizio/alla fine.Quello è O (1); non ammortizzato O (1) [supponendo che l'assegnazione della quantità * fissa * di memoria sia O (1)]. Forse usa ammortizzato per consentire implementazioni usando un unico grande blocco di memoria contigua, tuttavia non sarebbe molto utile in pratica, tranne se il 'deque' non cambia molto e hai davvero bisogno di memoria contigua per eseguire alcuni particolari operazione molto efficiente. – Bakuriu

+0

@Bakuriu: ok. Solo nuove allocazioni, quindi. Grazie per il chiarimento. – njzk2

risposta

9

La normale implementazione di un deque è fondamentalmente un vettore di puntatori a nodi di dimensioni fisse.

Assegnazione chiaramente il nodo di dimensione fissa ha costante complessità, quindi è abbastanza facile da gestire - basta ammortizzare il costo di assegnazione di un singolo nodo attraverso il numero di elementi in quel nodo per ottenere una complessità costante per ogni.

Il vettore della parte dei puntatori è ciò che è (marginalmente) più interessante. Quando allociamo un numero sufficiente di nodi a dimensione fissa che il vettore dei puntatori è pieno, è necessario aumentare la dimensione del vettore. Come std::vector, dobbiamo copiare i suoi contenuti nel vettore appena assegnato, quindi la sua crescita deve seguire una progressione geometrica (piuttosto che aritmetica). Ciò significa che abbiamo più indicazioni per copiare la copia in modo sempre meno frequente, quindi il tempo totale dedicato ai puntatori di copia rimane costante.

Come nota a margine: la parte "vettoriale" viene normalmente trattata come un buffer circolare, quindi se si utilizza la coda come coda, l'aggiunta costante a un'estremità e la rimozione dall'altra non risultano in allocare il vettore - significa semplicemente spostare i puntatori testa e coda che tengono traccia di quale dei puntatori sono "attivi" in un dato momento.

+1

Ma se sto leggendo correttamente lo standard, 23.3.3.4/3 afferma che '... L'inserimento di un singolo elemento all'inizio o alla fine di un deque richiede sempre un tempo costante ...' dove la parola "sempre" sembra precludere anche una riallocazione geometrica. –

+0

@ MarkB: Penso che la tua lettura sia ragionevole, ma l'unico modo che conosco per soddisfare questo (e tutti gli altri) requisiti sarebbe quello di pre-allocare la parte vettoriale come la più grande che tu abbia mai supportato, quindi tu mai crescerlo affatto. Questo è chiaramente possibile, ma penso che la maggior parte delle persone preferirebbe che crescesse, anche a spese di una costante complessità ammortizzata. –

+0

Mi capita di essere d'accordo con la tua interpretazione, ma volevo assicurarmi che non mi mancasse qualcosa di completamente ovvio. –

1

L'(profano) risposta sta nel containers.requirements.general, 23.2.1/2:

Tutti i requisiti di complessità in questa clausola sono iscritti solo in termini di numero di operazioni sul oggetti contenuti.

riassegnazione della matrice di puntatori non è quindi coperto dalla garanzia complessità dello standard e possono prendere arbitrariamente lungo. Come accennato in precedenza, è probabile che aggiunga un overhead costante ammortizzato a ciascuna chiamata push_front()/push_back() (oa un modificatore equivalente) in qualsiasi implementazione "sana". Non raccomanderei comunque di usare deque nel codice RT-critical. In genere, in uno scenario RT, non si desidera avere code o stack illimitati (che in C++ utilizza per default deque come contenitore sottostante), né allocazioni di memoria che potrebbero non riuscire, quindi sarà molto probabile che si utilizzi un anello preallocato buffer (es. Boost's circular_buffer).

+0

'std :: vector >' con il wrapping per far sembrare che t 'T 'avrebbe quindi operazioni costanti per quasi tutto? – Yakk

+0

Questo particolare esempio non è valido perché lo standard garantisce che un vettore sia contiguo, in 23.3.7.1/1: "Gli elementi di un vettore sono memorizzati in modo contiguo, nel senso che se v è un vettore dove T è un tipo diverso dal bool, quindi obbedisce all'identità & v [n] == & v [0] + n per tutto 0 <= n

+0

Eh? Stavo solo sottolineando che hai scritto un contenitore come ho detto, sarebbe (secondo le regole standard) contare molte operazioni come O (1) non ammortizzato, incluse quelle come "cancella metà del contenitore", ma non "ordina il contenitore". Sì, non sarebbe un 'vector', ma sarebbe un contenitore. Mi è sembrato strano. – Yakk

Problemi correlati