Амортизированный анализ std:: векторная вставка

Как мы делаем анализ вставки сзади (push_back) в std::vector? Это амортизированное время составляет O (1) на вставку. В частности, в видео в channel9 от Stephan T Lavavej и в этом ( 17:42 года ) Он говорит, что для оптимальной производительности внедрения Microsoft этого метода увеличивает емкость вектора примерно на 1.5.

Как определяется эта константа?

3 ответов


предполагая, что вы имеете в виду push_back и не вставка, я считаю, что важной частью является умножение на некоторую константу (в отличие от захвата N больше элементов каждый раз), и пока вы это делаете, вы получите амортизированное постоянное время. Изменение фактора изменяет средний случай и худшую производительность.

конкретно: Если ваш постоянный фактор слишком велик, у вас будет хорошая средняя производительность, но плохая худшая производительность, особенно когда массивы становятся большими. Для например, представьте себе удвоение (2x) вектора размера 10000 только потому, что у вас есть 10001-й элемент. EDIT: как косвенно указал Майкл Берр, реальная стоимость здесь, вероятно, заключается в том, что вы вырастите свою память намного больше, чем вам нужно. Я бы добавил к этому, что есть проблемы с кэшем, которые влияют на скорость, если ваш фактор слишком велик. Достаточно сказать, что существуют реальные затраты (память и вычисления), если вы растете намного больше, чем вам нужно.

однако, если ваша константа фактор слишком мал, скажем (1.1 x), тогда у вас будет хорошая худшая производительность, но плохая средняя производительность, потому что вам придется нести расходы на перераспределение слишком много раз.

Также см. ответ Джона Скита на аналогичный вопрос ранее. (Спасибо @Bo Persson)

немного больше об анализе: скажем, у вас есть n детали вы отгоняете и кратность M. Затем количество перераспределений будет примерно лог база M of n (log_M(n)). И i - е перераспределение будет стоить пропорционально M^i (M до ith power). Тогда общее время всех откатов будет M^1 + M^2 + ... M^(log_M(n)). Количество отказов -n, и таким образом вы получаете этот ряд (который является геометрическим рядом и сводится примерно к (nM)/(M-1) в пределе) к n. Это примерно постоянная,M/(M-1).

для больших значений M вы проскочите a лот и выделяют гораздо больше, чем вам нужно разумно часто (о чем я упоминал выше). Для небольших значений M (близко к 1) эта константа M/(M-1) становится большим. Этот фактор напрямую влияет на среднее время.


вы можете сделать математику, чтобы попытаться понять, как это работает.

популярным методом работы с асимптотическим анализом является метод банкиров. То, что вы делаете, - это разметка всех ваших операций с дополнительной стоимостью, "экономя" ее на потом, чтобы заплатить за дорогостоящую операцию.


давайте сделаем некоторые предположения дампа, чтобы упростить математику:

  • запись в массив стоит 1. (То же самое для вставки и перемещения между массивы)
  • выделение большего массива является бесплатным.

и наш алгоритм выглядит так:

function insert(x){
    if n_elements >= maximum array size:
         move all elements to a new array that
         is K times larger than the current size
    add x to array
    n_elements += 1

очевидно, что "худший случай" происходит, когда мы должны переместить элементы в новый массив. Давайте попробуем амортизировать это, добавив постоянную разметку d к цене ввода, принося ее к итогу (1 + d) в эксплуатацию.

сразу после того, как массив был изменен, мы (1/K) его заполнили и не сэкономили деньги. К тому времени мы заполните массив, мы можем быть уверены, что по крайней мере d * (1 - 1/K) * N копил. Поскольку эти деньги должны быть в состоянии заплатить за все N перемещаемых элементов, мы можем выяснить отношение между K и d:

d*(1 - 1/K)*N = N
d*(K-1)/K = 1
d = K/(K-1)

полезная таблица:

k    d     1+d(total insertion cost)
1.0  inf   inf
1.1  11.0  12.0
1.5  3.0   4.0
2.0  2.0   3.0
3.0  1.5   2.5
4.0  1.3   2.3
inf  1.0   2.0

таким образом, из этого вы можете получить грубое математическое представление о том, как работает компромисс времени/памяти для этой проблемы. Конечно, есть некоторые предостережения: я не стал сокращать массив, когда он получает меньше элементов, это охватывает только худший случай, когда элементы никогда не удаляются, а затраты времени на выделение дополнительной памяти не учитываются.

они, скорее всего, провели кучу экспериментальных тестов, чтобы понять это, в конце концов, сделав большую часть того, что я написал, неактуальным.


мм, анализ очень прост, когда вы знакомы с системами счисления, такими как наш обычный десятичный.

для простоты предположим, что при каждом достижении текущей емкости выделяется новый 10x как большой буфер.

Если исходный буфер имеет размер 1, то первое перераспределение копирует 1 элемент, второе (где теперь буфер имеет размер 10) копирует 10 элементов и так далее. Так с пяти перераспределение, скажем, у вас есть 1+10+100+1000+10000 = 11111 выполнены копии элементов. Умножьте это на 9, и вы получите 99999; теперь добавьте 1 и у вас 100000 = 10^5. Или, другими словами, делая это назад, количество копий элементов, выполненных для поддержки этих 5 перераспределений, было (10^5-1) / 9.

и размер буфера после 5 перераспределений, 5 умножений на 10, равен 10^5. Это примерно в 9 раз больше, чем количество операций копирования элементов. Это означает, что время, затраченное на копирование, примерно линейно в результирующем буфере размер.

с базой 2 вместо 10 вы получаете (2^5-1)/1 = 2^5-1.

и так далее для других баз (или факторов для увеличения размера буфера).

Cheers & hth.