2017-02-05 7 views
3

В C++, STL, у нас есть класс шаблонов <vector>. Мы знаем, что он поддерживает O(1) случайный доступ и модификацию хвоста. Мой вопрос: почему мы не определяем push_front или pop_front в <vector>?Почему нет push/pop перед вектором?

Одно из объяснений состоит в том, что если мы хотим, чтобы элемент push/pop в начале вектора, мы должны перенести каждый элемент в массив на один шаг, и это будет стоить O(n).

Но я думаю, что это не всегда так. Учитывая, что если мы реализуем <vector> с круговым массивом, мы можем достичь O(1) push/pop с фронта и хвоста вектора, не теряя при этом возможности O(1) случайного доступа. Поэтому лично я не могу думать о какой-либо причине, а не только о незначительных накладных расходах, чтобы не реализовывать push_front/pop_front для <vector>. Есть предположения?

+5

Он был разработан так, как есть. Конец истории. –

+6

И накладные расходы на вычисления, необходимые для обработки кругового буфера, не стоит об этом думать? Речь идет не только о O(), но и о реальной реальной производительности. –

+1

Мы не можем определить std :: vector как круглый, из-за ограничений, установленных стандартом. –

ответ

7

Одним из объяснений является то, что если мы хотим, чтобы подтолкнуть/поп-элемент в передней части вектора, мы должны изменить каждый элемент массива на один шаг, и это будет стоить O (п)

Вы абсолютно правы, push_front не имеет возможности быстро запускать, потому что в дополнение к возможному перераспределению все элементы должны быть скопированы по одной позиции. Это дает вам амортизированную производительность O (n) для n объектов, которые разработчики библиотеки не хотели поощрять.

Учитывая, что, если мы реализуем <vector> с круговым массивом

Реализацией вектора с круговым массивом делает его намного труднее осуществить ряд важных гарантий, которые должны быть истинными для вектора. Например, вектор должен гарантировать, что если итератор a указывает на элемент с более низким индексом, чем итератор b, затем a < b. Когда вектор является линейным, сравнение сводится к сравнению адресов элементов, к которым указывают итераторы a и b. При реализации циклического массива нужно учитывать адрес начала координат, который теперь может находиться в середине выделенного блока памяти.

Другой гарантировала, что будет нарушено это:

Когда v является vector<T>, T любого типа, кроме bool и n число между нулем и размером вектора, &v[n] == &v[0] + n идентичность должна быть правдой.

Это не может быть реализовано с помощью круглой матрицы.

+0

Я думаю, что сравнение итераторов на самом деле может быть довольно легко реализовать. Например, мы можем взять остаток расстояния между определенным указателем, чтобы начать указатель, и просто сравнить этот остаток. Это вызывает проблемы при реализации? –

+0

@TaylorHuang Это, безусловно, может быть реализовано, но оно толкает вектор дальше от хорошей замены массива. По дизайну векторы предлагают чрезвычайно низкие накладные расходы; круговые массивы близки по производительности, но все же не свободны. Для дизайнеров библиотек гибкость для добавления элементов на передний план не оправдывала никаких жертв. – dasblinkenlight

+0

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

9

У нас уже есть что-то, что вы описываете в STL. Он называется deque.

Как вы писали, на самом деле есть некоторые накладные расходы. Поэтому, если вам нужна эта функциональность, и у вас нет проблем с накладными расходами, используйте deque. Если вы этого не требуете, вам не нужны накладные расходы, поэтому лучше иметь что-то, что позволяет избежать этих накладных расходов, названных vector.

И в качестве дополнения: vector гарантирует, что все его элементы хранятся в смежных местах хранения, поэтому вы можете применить арифметику указателя. Это не касается кругового буфера.

+0

О, я думаю, что deque - это то, что я искал. Раньше я думал, что deque не поддерживает O (1) случайный доступ, я думаю, я был неправ. :) –

+0

Хорошо, спасибо. Я думаю, что это было полезно, но @dasblinklight указал на множество моих недостатков, поэтому, я думаю, я выберу его, как принято, но ваш тоже полезен. :) –

4

Фактически, это является реалистичным требованием. Насколько мне известно, ничто в стандарте не гарантирует, что вектор не может иметь буфер до элементов (v.prefix_capacity()), равно как после (v.capacity() - v.size()). Это может гарантировать то же время выполнения для v.push_front() и v.push_back(), не требуя затрат для тех, кто его не использует. Кроме того, он может гарантировать O (1) v.pop_front(), хотя и недействительными итераторами. Представляете себе предложение?

В то же время, вы можете создать шаблон в терминах вектора, который (devector?):

  • имеет поле pre_capacity_ инициализируется 0 и поглотитель pre_capacity()
  • реализует pre_reserve(size_t i), который вызывает как:
    • reserve(capacity() - pre_capacity_ + i)
    • pre_capacity_ += i
  • реализует operator[](size_t i) и делегаты v[i + pre_capacity()]
  • реализует at(size_t i) и делегатов v.at(i + pre_capacity())
  • реализует begin() и делегирует v.begin() + pre_capacity()
  • делегаты всех остальных методов vector

Или вы можете просто отслеживайте количество элементов, которые вы нажали/выскочили с фронта :).

+0

Я думаю, что такой универсальный контейнер будет полезен, например, «deque», не так ли?:) Но как насчет такого протокола: '' ' Когда v - вектор , T - любой тип, отличный от bool, и число na между нулем и размером вектора, & v [n] == & v [0] + n идентичность должна быть верной. '' ' С буфером перед элементом, как можно гарантировать такую ​​гарантию? –

+1

У меня такая структура данных в работах, называемая [devector] (https://github.com/orlp/devector). Реализация еще не закончена (я должен когда-нибудь закончить ее). – orlp

+0

@TaylorHuang: обратите внимание, что 'push_front()' отменяет итераторы. Таким образом, '& v [0]' может уменьшаться при 'push_front()' без перераспределения. Подумайте об этом так: перераспределение выполняется 'sizeof (T)' bytes перед текущим буфером. – lorro