Является ли list:: size() действительно O (n)?

Недавно я заметил, что некоторые люди отмечают, что std::list::size() имеет линейную сложность.
Согласно некоторым источникам, это фактически зависит от реализации как в стандарте не сказано, какая сложность должна быть.
Комментарий в этой записи в блоге гласит:

Собственно, это зависит от того, какой STL вы используются. Microsoft Visual Studio V6 реализует size() как {return (_Size); }, тогда как gcc (по крайней мере, в версиях 3.3.2 и 4.1.0) делают это как {return std:: distance (begin(), end()); } первая имеет постоянную скорость, вторая имеет скорость o (N)

  • Итак, я предполагаю, что для толпы VС++ size() имеет постоянную сложность, поскольку Dinkumware вероятно, не изменит этот факт с VC6. Я здесь?
  • Как это выглядит в настоящее время в gcc? Если это действительно O (n), почему разработчики решили это сделать?

Ответ 1

Ответ Pre-С++ 11

Вы правы, что в стандарте не указано, какой должна быть сложность list:: size(), однако он рекомендует, чтобы он "имел постоянную сложность" (примечание A в таблице 65).

Здесь интересная статья Говарда Хиннанта, которая объясняет, почему некоторые люди думают, что list:: size() должен иметь сложность O (N) (в основном потому что они считают, что O (1) list:: size() делает list:: splice() сложностью O (N)) и почему O (1) list:: size() - хорошая идея (в авторе мнение):

Я думаю, что главными пунктами в этой статье являются:

  • существует несколько ситуаций, когда сохранение внутреннего счета, поэтому list::size() может быть O (1), приводит к тому, что операция сращивания становится линейной
  • Есть, вероятно, еще много ситуаций, когда кто-то может не знать о негативных последствиях, которые могут произойти, потому что они называют O (N) size() (например, его один пример, где list::size() вызывается при удерживании блокировки).
  • что вместо того, чтобы разрешать size() быть O (N), в интересах "наименьшего удивления", стандарт должен требовать любой контейнер, который реализует size(), чтобы реализовать его в O (1). Если контейнер не может этого сделать, он не должен реализовывать size() вообще. В этом случае пользователю контейнера будет известно, что size() недоступен, и если они все еще хотят или должны получить количество элементов в контейнере, они могут использовать container::distance( begin(), end()) для получения этого значения, но они будет полностью осознавать, что это операция O (N).

Я думаю, что я согласен с большинством его рассуждений. Однако мне не нравится его предлагаемое дополнение к перегрузкам splice(). Чтобы пройти в n, который должен быть равен distance( first, last), чтобы получить правильное поведение, похоже на рецепт для сложной диагностики ошибок.

Я не уверен, что нужно или можно было бы сделать, продвигаясь вперед, так как любое изменение окажет существенное влияние на существующий код. Но, как он есть, я думаю, что существующий код уже затронут - поведение может существенно отличаться от одного варианта реализации другого для того, что должно быть четко определено. Возможно, один комментарий о том, что размер кэша и обозначенный известный/неизвестный может работать хорошо - вы получаете амортизированное поведение O (1) - единственный раз, когда вы получаете поведение O (N), когда список изменяется некоторыми операциями splice(), Самое приятное в этом заключается в том, что это может быть сделано разработчиками сегодня без изменения стандарта (если я что-то не хватает).

Насколько я знаю, С++ 0x ничего не меняет в этой области.

Ответ 2

В С++ 11 требуется, чтобы для любого стандартного контейнера .size() была завершена с "постоянной" сложностью (O (1)). (Таблица 96 - Требования к контейнерам). Ранее в С++ 03 .size() должен иметь постоянную сложность, но не обязателен (см. Является ли std :: string size() операцией O (1)?).

Изменение в стандарте введено n2923: Указание сложности size() (Редакция 1).

Однако реализация .size() в libstdc++ все еще использует алгоритм O (N) в gcc до 4.8:

  /**  Returns the number of elements in the %list.  */
  size_type
  size() const _GLIBCXX_NOEXCEPT
  { return std::distance(begin(), end()); }

Смотрите также Почему std :: list больше на С++ 11? для деталей, почему это так.

Обновление: std::list::size() правильно O (1) при использовании gcc 5.0 в режиме С++ 11 (или выше).


Кстати, .size() в libc++ - это правильно O (1):

_LIBCPP_INLINE_VISIBILITY
size_type size() const _NOEXCEPT     {return base::__sz();}

...

__compressed_pair<size_type, __node_allocator> __size_alloc_;

_LIBCPP_INLINE_VISIBILITY
const size_type& __sz() const _NOEXCEPT
    {return __size_alloc_.first();}

Ответ 3

Мне нужно было изучить размер gcc 3.4 list:: size, поэтому я могу сказать следующее:

  • он использует std:: distance (head, tail)
  • std:: distance имеет две реализации: для типов, которые удовлетворяют RandomAccessIterator, он использует "tail-head", а для типов, которые просто удовлетворяют InputIterator, он использует алгоритм O (n), полагающийся на "итератор ++", считая, пока он не будет попадает в данный хвост.
  • std:: list не обрабатывает RandomAccessIterator, поэтому размер O (n).

Что касается "почему", я могу только сказать, что std:: list подходит для проблем, требующих последовательного доступа. Хранение размера в качестве переменной класса приведет к накладным расходам на каждую вставку, удаление и т.д., И что отходы представляют собой большое значение no-no по смыслу STL. Если вам действительно нужен постоянный размер(), используйте std:: deque.

Ответ 4

Я лично не вижу проблемы с соединением, являющимся O (N), как единственной причиной, по которой размер разрешен как O (N). Вы не платите за то, что вы не используете, является важным девизом С++. В этом случае для поддержания размера списка требуется дополнительное приращение/уменьшение для каждой вставки/стирания, проверяете ли вы размер списка или нет. Это небольшая фиксированная накладная плата, но ее все еще важно рассмотреть.

Проверка размера списка редко необходима. Итерация от начала до конца, не заботясь об общем размере, бесконечно более распространена.

Ответ 5

Я бы пошел к источнику (архив). Страница SGI STL гласит, что разрешено иметь линейную сложность. Я считаю, что руководящие принципы разработки, которым они следовали, должны были сделать реализацию списка как можно более общей и, таким образом, обеспечить большую гибкость в использовании списков.

Ответ 6

Этот отчет об ошибке: [С++ 0x] std:: list:: size complex, в мучительных подробностях фиксирует тот факт, что реализация в GCC 4.x является линейным временем и как переход к постоянному времени для С++ 11 был медленным (доступно в 5.0) из-за проблем совместимости ABI.

В man-странице серии GCC 4.9 по-прежнему имеется следующее выражение об отказе:

Поддержка С++ 11 по-прежнему экспериментальных и может изменяться несовместимыми способами в будущих выпусках.


Этот же отчет об ошибке упоминается здесь: Должен ли std:: list:: size иметь постоянную сложность в С++ 11?

Ответ 7

Если вы правильно используете списки, вы, вероятно, не заметите никаких различий.

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

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

В любом случае std больше касается правильности и стандартного поведения и "удобства пользователя", чем скорости raw.