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

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

Actually, it depends on which STL you are using. Microsoft Visual Studio V6 implements size() as {return (_Size); } whereas gcc (at least in versions 3.3.2 and 4.1.0) do it as { return std::distance(begin(), end()); } The first has constant speed, the second has o(N) speed

  1. Поэтому я предполагаю, что для толпы VC++ size() имеет постоянную сложность, поскольку Dinkumware вероятно, не изменил бы этот факт со времен VC6. Я прав?
  2. Как это выглядит сейчас в gcc? Если это действительно O (n), почему разработчики так хотят?
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
64
0
23 643
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Ответ принят как подходящий

Pre-C++ 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 () . Приятно то, что разработчики могут сделать это сегодня без изменения стандарта (если я чего-то не упускаю).

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

Ответ правильный, но рассуждения о размере списка плавны. Ваше предложение подвержено несогласованным параметрам и нарушает принцип, согласно которому пользователь предоставляет всю информацию только один раз.

PierreBdR 23.10.2008 13:31

Также должна быть возможность сохранить стык O (1), но пометить размер как «неизвестный». Тогда size () по-прежнему будет O (N) наихудшим случаем, но наихудший случай происходит не чаще одного раза за «недружественное» соединение. Таким образом, производительность всех операций строго превосходит всегда-O (N) size (). Предупреждение: я не продумал это до конца.

Steve Jessop 23.10.2008 16:09

«строго превосходит» - на самом деле это ложь, так как в splice есть несколько дополнительных проверок, чтобы выяснить, в каком случае вы находитесь, и арифметика с размерами во всех мутаторах. Сказал вам, что я не подумал об этом. Но сложность никогда не бывает хуже, а иногда и лучше.

Steve Jessop 23.10.2008 16:12

@PierreBdR - Если неясно, я не автор статьи, я указал на нее, потому что подумал, что в ней есть некоторые интересные моменты. Я отредактировал ответ, чтобы сделать его более ясным (а также добавил несколько моих собственных мыслей и включил идеи из этих комментариев).

Michael Burr 23.10.2008 23:39

Последний черновик C++ 0x требует, чтобы size() имел постоянную временную сложность (это изменение требований к контейнеру было внесено в N3000).

James McNellis 16.03.2010 22:18

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

SGI - не совсем «источник». Он основан на оригинальном (HP?) STL, но Стандарт отклонился от этого. SGI просто говорит, что делает их реализация, а не то, что стандарт говорит, что она должна делать.

James Curran 23.10.2008 23:28

И ссылка все равно разорвана.

Lightness Races in Orbit 20.03.2018 22:54

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

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

Очевидно, комитет C++ 11 с вами не согласился. Жалость.

Mark Ransom 27.12.2011 01:14

Раньше мне приходилось заглядывать в list::size gcc 3.4, поэтому я могу сказать следующее:

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

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

В C++ 11 требуется, чтобы для стандартного контейнера Любые операция .size() выполнялась с «постоянной» сложностью (O (1)). (Таблица 96 - Требования к контейнерам). Ранее в C++ 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 в режиме C++ 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();}

это следует принять, к сожалению, люди не смотрят на старый Q. :)

NoSenseEtAl 12.12.2012 14:51

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

Справочная страница для серии GCC 4.9 по-прежнему включает следующий отказ от ответственности:

Support for C++11 is still experimental, and may change in incompatible ways in future releases.


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

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

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

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

В любом случае std - это больше о правильности, стандартном поведении и «удобстве для пользователя», чем о чистой скорости.

Я не понимаю, как желание знать, в крайнем случае, сколько элементов в списке, означает неправильное использование списка.

Lightness Races in Orbit 08.08.2018 16:29

Другие вопросы по теме