Хотя @Marc дал (я думаю, что это) отличный анализ, некоторые люди могут предпочесть рассмотреть вещи с несколько иной точки зрения.
Один из них - рассмотреть несколько иной способ перераспределения. Вместо того, чтобы сразу копировать все элементы из старого хранилища в новое хранилище, рассмотрите возможность копирования только одного элемента за раз - т.е. каждый раз, когда вы выполняете push_back, он добавляет новый элемент в новое пространство и копирует ровно один существующий элемент из старого пространства в новое пространство. Если предположить, что фактор роста равен 2, то совершенно очевидно, что когда новое пространство заполнено, мы закончили бы копирование всех элементов из старого пространства в новое пространство, и каждый push_back был точно постоянным временем. В этот момент мы отбрасываем старое пространство, выделяем новый блок памяти с вдвое большим усилением и повторяем процесс.
Совершенно ясно, что мы можем продолжать это бесконечно (или до тех пор, пока есть доступная память), и каждый push_back будет включать добавление одного нового элемента и копирование одного старого элемента.
Типичная реализация по-прежнему имеет точно такое же количество копий, но вместо того, чтобы делать копии по одной, она копирует все существующие элементы одновременно. С одной стороны, вы правы: это означает, что если вы посмотрите на отдельные вызовы push_back, некоторые из них будут значительно медленнее, чем другие. Однако, если мы посмотрим на долгосрочное среднее значение, количество копий, выполняемых за один вызов push_back, остается постоянным, независимо от размера вектора.
Хотя это не имеет отношения к вычислительной сложности, я думаю, что стоит указать, почему выгодно делать вещи, как они, вместо того, чтобы копировать один элемент на push_back, поэтому время на push_back остается постоянным. Есть как минимум три причины для рассмотрения.
Первый - это просто доступность памяти. Старая память может быть освобождена для других целей только после завершения копирования. Если вы копируете только один элемент за раз, старый блок памяти будет выделяться гораздо дольше. Фактически у вас будет один старый блок и один новый блок, выделенный практически все время. Если вы выбрали фактор роста меньше двух (который вам обычно нужен), вам понадобится еще больше памяти, выделяемой постоянно.
Во-вторых, если вы копируете только один старый элемент за раз, индексация в массиве будет немного сложнее - каждая операция индексации должна будет выяснить, находится ли элемент с данным индексом в настоящее время в старом блоке памяти или новый. Это совсем не сложно, но для элементарной операции, такой как индексация в массиве, почти любое замедление может быть значительным.
В-третьих, копируя все сразу, вы получаете гораздо больше преимуществ от кэширования. Копируя все сразу, вы можете ожидать, что источник и место назначения в большинстве случаев будут находиться в кэше, поэтому стоимость пропуска кэша амортизируется по количеству элементов, которые поместятся в строке кэша. Если вы копируете по одному элементу за раз, вы можете легко пропустить кеш для каждого копируемого элемента. Это только меняет постоянный коэффициент, но не сложность, но он все еще может быть довольно значительным - для типичной машины можно легко ожидать коэффициент от 10 до 20.
Вероятно, стоит также рассмотреть другое направление: если вы разрабатываете систему с требованиями реального времени, вполне может иметь смысл копировать только один элемент за раз, а не все сразу. Хотя общая скорость может (или не может) быть ниже, вы все равно будете иметь жесткую верхнюю границу времени, необходимого для одного выполнения push_back - при условии, что у вас есть распределитель в реальном времени (хотя, конечно, многие в реальном времени системы просто запрещают динамическое распределение памяти вообще, по крайней мере, в частях с требованиями в реальном времени).