Sebbene @Marc abbia fornito (quello che penso sia) un'analisi eccellente, alcune persone potrebbero preferire considerare le cose da un'angolazione leggermente diversa.
Uno è considerare un modo leggermente diverso di fare una riallocazione. Invece di copiare immediatamente tutti gli elementi dalla vecchia memoria alla nuova memoria, prendere in considerazione la copia di un solo elemento alla volta, ovvero ogni volta che si esegue un push_back, si aggiunge il nuovo elemento al nuovo spazio e si copia esattamente uno esistente elemento dal vecchio spazio al nuovo spazio. Supponendo un fattore di crescita di 2, è abbastanza ovvio che quando il nuovo spazio è pieno, avremmo finito di copiare tutti gli elementi dal vecchio spazio al nuovo spazio e ogni push_back è stato esattamente un tempo costante. A quel punto, avremmo scartato il vecchio spazio, allocato un nuovo blocco di memoria con un guadagno doppio e ripetuto il processo.
Abbastanza chiaramente, possiamo continuare indefinitamente (o fino a quando c'è memoria disponibile, comunque) e ogni push-back implicherebbe l'aggiunta di un nuovo elemento e la copia di un vecchio elemento.
Un'implementazione tipica ha esattamente lo stesso numero di copie, ma invece di eseguire le copie una alla volta, copia tutti gli elementi esistenti contemporaneamente. Da un lato, hai ragione: ciò significa che se guardi alle singole invocazioni di push_back, alcune di esse saranno sostanzialmente più lente di altre. Se osserviamo una media a lungo termine, tuttavia, la quantità di copie eseguite per invocazione di push_back rimane costante, indipendentemente dalle dimensioni del vettore.
Anche se è irrilevante per la complessità computazionale, penso che valga la pena sottolineare perché è vantaggioso fare le cose come fanno, invece di copiare un elemento per push_back, quindi il tempo per push_back rimane costante. Ci sono almeno tre ragioni da considerare.
Il primo è semplicemente la disponibilità della memoria. La vecchia memoria può essere liberata per altri usi solo al termine della copia. Se copiassi solo un elemento alla volta, il vecchio blocco di memoria rimarrebbe allocato molto più a lungo. In effetti, avresti un blocco vecchio e uno nuovo allocati essenzialmente per tutto il tempo. Se decidessi un fattore di crescita inferiore a due (che di solito vuoi) avresti bisogno di una quantità di memoria ancora maggiore.
In secondo luogo, se si copiasse solo un vecchio elemento alla volta, l'indicizzazione nell'array sarebbe un po 'più complicata: ogni operazione di indicizzazione avrebbe bisogno di capire se l'elemento in corrispondenza dell'indice dato era attualmente nel vecchio blocco di memoria o nuovo. Non è assolutamente complesso, ma per un'operazione elementare come l'indicizzazione in un array, quasi ogni rallentamento potrebbe essere significativo.
In terzo luogo, copiando tutto in una volta, si sfrutta molto meglio la memorizzazione nella cache. Copiando tutto in una volta, ci si può aspettare che sia l'origine che la destinazione siano nella cache nella maggior parte dei casi, quindi il costo di una mancata cache viene ammortizzato sul numero di elementi che si adatteranno in una riga della cache. Se copi un elemento alla volta, potresti facilmente perdere la cache per ogni elemento copiato. Ciò cambia solo il fattore costante, non la complessità, ma può comunque essere abbastanza significativo: per una macchina tipica, ci si potrebbe facilmente aspettare un fattore da 10 a 20.
Probabilmente vale la pena considerare anche l'altra direzione per un momento: se si stava progettando un sistema con requisiti in tempo reale, potrebbe avere senso copiare solo un elemento alla volta anziché tutti contemporaneamente. Anche se la velocità complessiva potrebbe (o potrebbe non essere) essere inferiore, avresti comunque un limite superiore al tempo impiegato per una singola esecuzione di push_back - presumendo di avere un allocatore in tempo reale (anche se, ovviamente, molti in tempo reale i sistemi proibiscono semplicemente l'allocazione dinamica della memoria, almeno in porzioni con requisiti in tempo reale).