вектор против списка в STL

239

Я заметил в Effective STL, что

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

Что это значит? Кажется, что игнорировать эффективность vectorможет все что угодно.

Может ли кто-нибудь предложить мне сценарий, в котором vectorвариант невозможен, но listдолжен использоваться?

skydoor
источник
6
Хотя это не то, что вы просили, стоит отметить, что по умолчанию вектор также означает, что вы можете легко взаимодействовать со старым кодом, библиотеками C или не шаблонными библиотеками, поскольку вектор является тонкой оболочкой вокруг «традиционного» динамического массива указателя. и размер.
18
Бьярне Строструп фактически провел тест, в котором он генерировал случайные числа, а затем добавил их в список и вектор соответственно. Вставки были сделаны так, чтобы список / вектор был упорядочен всегда. Несмотря на то, что это, как правило, «список доменов», вектор превзошел список с большим отрывом. Причина в том, что доступ к памяти медленный и кэширование работает лучше для последовательных данных. Это все доступно в его лейтмотиве от «GoingNative 2012»
уклонение от
1
Если вы хотите увидеть выступление Бьярна Страуструпа, о котором упоминал @evading, я нашел его здесь: youtu.be/OB-bdWKwXsU?t=2672
brain56

Ответы:

98

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

Проверьте гарантии сложности для каждого отдельного типа контейнера:

Каковы гарантии сложности стандартных контейнеров?

Мартин Йорк
источник
2
Вставка элементов в конце также имеет значение, поскольку это может привести к выделению памяти и затратам на копирование элементов. А также, вставка elenets в начале вектора практически невозможна,list естьpush_front
Notinlist
9
Нет, вставка элементов в конце вектора амортизируется постоянным временем. Выделение памяти происходит только изредка, и вы можете предварительно выделить вектор, чтобы предотвратить это. Конечно, если вы ДОЛЖНЫ гарантировать постоянные вставки с постоянным временем, я думаю, это все еще проблема.
Брайан
16
@Notinlist - для вас следующее «почти невозможно»? v.insert (v.begin (), i)
Мануэль
5
@ Notinlist - я согласен с вами, просто я не хотел, чтобы ОП думал, что интерфейса нет, если кто-то захочет выстрелить себе в ногу (производительность).
Мануэль
16
Бьярне Строструп фактически провел тест, в котором он генерировал случайные числа, а затем добавил их в список и вектор соответственно. Вставки были сделаны так, чтобы список / вектор был упорядочен всегда. Несмотря на то, что это, как правило, «список доменов», вектор превзошел список с большим отрывом. Причина в том, что доступ к памяти медленный и кэширование работает лучше для последовательных данных. Это все доступно в его лейтмотиве от «GoingNative 2012»
уклонение
410

вектор:

  • Смежная память.
  • Предварительно выделяет пространство для будущих элементов, поэтому требуется дополнительное пространство сверх того, что необходимо для самих элементов.
  • Каждый элемент требует только места для самого типа элемента (без дополнительных указателей).
  • Может перераспределить память для всего вектора в любое время, когда вы добавляете элемент.
  • Вставки в конце - это постоянное амортизированное время, но вставки в другом месте стоят дорого O (n).
  • Стирание в конце вектора является постоянным временем, но для остальных это O (n).
  • Вы можете получить произвольный доступ к его элементам.
  • Итераторы становятся недействительными, если вы добавляете или удаляете элементы в или из вектора.
  • Вы можете легко получить базовый массив, если вам нужен массив элементов.

список:

  • Несмежная память.
  • Нет предварительно выделенной памяти. Объем памяти для самого списка постоянен.
  • Каждому элементу требуется дополнительное пространство для узла, который содержит элемент, включая указатели на следующий и предыдущий элементы в списке.
  • Никогда не нужно перераспределять память для всего списка только потому, что вы добавляете элемент.
  • Вставки и удаления дешевы независимо от того, где они находятся в списке.
  • Дешево объединять списки со сплайсингом.
  • Вы не можете получить произвольный доступ к элементам, поэтому получение определенного элемента в списке может быть дорогостоящим.
  • Итераторы остаются действительными даже при добавлении или удалении элементов из списка.
  • Если вам нужен массив элементов, вам придется создать новый и добавить их все к нему, так как нет базового массива.

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

Джонатан М Дэвис
источник
2
Кроме того, размещение в бесплатном магазине не является бесплатным. :) Добавление новых элементов в вектор выполняет O (log n) свободных распределений хранилища, но вы можете вызвать, reserve()чтобы уменьшить это до O (1). Добавление новых элементов в список (т. Е. Не объединение их) выполняет O (n) свободных распределений хранилища.
bk1e
7
Другое соображение заключается в том, что listосвобождает память, когда вы стираете элементы, но vectorне освобождает . А vectorне уменьшает свою емкость, когда вы уменьшаете его размер, если вы не используете swap()хитрость.
bk1e
@ bk1e: Я очень хочу узнать твой трюк в резервах () и swap () :)
Дзунг Нгуен,
2
@nXqd: если вам нужно добавить N элементов в вектор, вызовите v.reserve (v.size () + N), чтобы он выполнял только одно бесплатное распределение хранилища. Трюк с swap () здесь: stackoverflow.com/questions/253157/how-to-downsize-stdvector
bk1e
1
@simplename Нет. То, что он говорит, является правильным. вектор выделяет дополнительное пространство за пределами пространства для элементов, находящихся в данный момент в векторе; эта дополнительная емкость затем используется для роста вектора, так что при его выращивании амортизируется O (1).
Джонатан М Дэвис
35

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

Ганс Пассант
источник
32

Большинство ответов здесь пропускают одну важную деталь: зачем?

Что вы хотите сохранить в контейнере?

Если это набор ints, то std::listон проиграет в каждом сценарии, независимо от того, сможете ли вы перераспределить, вы удалите его только с фронта и т. Д. Списки перемещаются медленнее, каждая вставка обходится вам взаимодействием с распределителем. Было бы крайне сложно подготовить пример, где list<int>бьет vector<int>. И даже тогда, deque<int>может быть лучше или близко, не просто использование списков, которые будут иметь большие накладные расходы памяти.

Тем не менее, если вы имеете дело с большими, уродливыми сгустками данных - и немногими из них - вы не хотите перераспределять при вставке, а копирование из-за перераспределения было бы катастрофой - тогда вам, возможно, будет лучше с list<UglyBlob>чем vector<UglyBlob>.

Тем не менее, если вы переключитесь на vector<UglyBlob*>или даже vector<shared_ptr<UglyBlob> >, снова - список будет отставать.

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

Томаш Гандор
источник
1
Еще одно размышление, которое я имел, читая «Эффективный STL» Мейерса: своеобразным свойством list<T>является возможность spliceв O (1) . Если вам нужно постоянное соединение, список также может быть структурой выбора;)
Томаш Гандор
+1 - Это даже не должно быть UglyBlob- даже объекты с несколькими строковыми членами могут быть слишком дорогими для копирования, поэтому перераспределение будет стоить. Также: не пренебрегайте пространственными накладными расходами, которые vectorмогут вызвать экспоненциальный рост удерживающих объектов размером в несколько десятков байт (если вы не можете reserveзаранее).
Мартин Ба
Что касается vector<smart_ptr<Large>>против list<Large>- я бы сказал, если вам нужен произвольный доступ к элементам, это vectorимеет смысл. Если вам не нужен произвольный доступ, то он listкажется более простым и должен работать одинаково.
Мартин Ба,
19

Одной из специальных возможностей std :: list является объединение (связывание или перемещение части или целого списка в другой список).

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

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

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

Дядя Бен
источник
1
+1. Сращивание упускается из виду, но, к сожалению, не постоянное время, как хотелось бы. : (((Не может быть, если list :: size является постоянным временем.)
Я совершенно уверен, что list :: size является (разрешено быть) линейным по этой самой причине.
UncleBens
1
@ Роджер: list::sizeне обязательно постоянное время. См stackoverflow.com/questions/228908/is-listsize-really-on и gcc.gnu.org/ml/libstdc++/2005-11/msg00219.html
Potatoswatter
@Potatoswatter: то, что стандарт расплывчатый и, следовательно, вы не можете полагаться на «совместимые» реализации, делает его еще более сложной проблемой. Вы буквально должны избегать stdlib, чтобы получить портативную и надежную гарантию.
@ Роджер: да, к сожалению. Мой текущий проект сильно опирается на операции сращивания, и эта структура почти прямолинейна. Еще более, к сожалению, в N3000 последовательность spliceмежду различными списками задается как линейная сложность и sizeявляется конкретно постоянной. Таким образом, чтобы приспособиться к новичкам, которые выполняют итерации sizeили что-то еще, целый класс алгоритмов недосягаем для STL или любого периода «совместимых» контейнеров.
Potatoswatter
13

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

Вот как я это понимаю

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

Векторы : при использовании векторов память намного более организована, как обычные массивы: каждый n-й элемент сохраняется сразу после (n-1) -го элемента и до (n + 1) -го элемента. Почему это лучше, чем список? Потому что это позволяет быстрый произвольный доступ. Вот как: если вы знаете размер элемента в векторе, и если они находятся в памяти рядом, вы можете легко предсказать, где находится n-й элемент; вам не нужно просматривать весь элемент списка, чтобы прочитать тот, который вы хотите, с вектором, вы непосредственно читаете его, со списком, который вы не можете. С другой стороны, изменить векторный массив или изменить значение гораздо медленнее.

Списки больше подходят для отслеживания объектов, которые могут быть добавлены / удалены в памяти. Векторы больше подходят, когда вы хотите получить доступ к элементу из большого количества отдельных элементов.

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

jokoon
источник
«Модифицировать векторный массив или изменять значение гораздо медленнее» - как вы читали, это, кажется, противоречит тому, что вы сказали перед тем, как вектор предрасположен к хорошей производительности из-за их низкого уровня и непрерывности. Вы имели в виду , что Перераспределениеvector вызвано изменением его размера может быть медленным? затем согласились, но в тех случаях, когда reserve()их можно использовать, это позволяет избежать этих проблем.
underscore_d
10

В основном вектор - это массив с автоматическим управлением памятью. Данные непрерывны в памяти. Попытка вставить данные посередине - дорогостоящая операция.

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

Чтобы ответить более конкретно на ваш вопрос, я процитирую эту страницу

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

f4.
источник
9

В любое время вы не можете сделать итераторы недействительными.

dirkgently
источник
2
Но никогда не делайте поспешных выводов об итераторах, не спрашивая, достаточно ли постоянных ссылок в a deque.
Potatoswatter
8

Когда у вас много вставки или удаления в середине последовательности. например менеджер памяти.

арак
источник
так что разница между ними заключается только в эффективности, а не в функциональных проблемах.
Skydoor
Они оба моделируют последовательность элементов, конечно. Существует небольшая разница в использовании, как упомянуто @dirkgently, но вы должны посмотреть на сложность ваших «часто выполняемых» операций, чтобы решить, какую последовательность выбрать (ответ @Martin).
AraK
@skydoor - Есть несколько функциональных отличий. Например, только вектор поддерживает произвольный доступ (т. Е. Может быть проиндексирован).
Мануэль
2
@skydoor: эффективность означает производительность. Плохая производительность может испортить функционал. В конце концов, производительность - это преимущество C ++.
Potatoswatter
4

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

Джон Диблинг
источник
4

Когда вы хотите перемещать объекты между контейнерами, вы можете использовать list::splice.

Например, алгоритм разбиения графа может иметь постоянное количество объектов, рекурсивно разделенных между увеличивающимся числом контейнеров. Объекты должны быть инициализированы один раз и всегда оставаться в тех же местах в памяти. Гораздо быстрее переставить их путем перекомпоновки, чем перераспределением.

Редактировать: поскольку библиотеки готовятся к реализации C ++ 0x, общий случай объединения подпоследовательности в список становится линейной сложностью с длиной последовательности. Это связано с тем, что splice(сейчас) необходимо выполнить итерацию последовательности для подсчета количества элементов в ней. (Поскольку список должен записывать его размер.) Простой подсчет и повторное связывание списка все еще быстрее, чем любая альтернатива, и объединение всего списка или одного элемента - это особые случаи с постоянной сложностью. Но если у вас есть длинные последовательности для соединения, вам, возможно, придется покопаться в поисках лучшего, старомодного, несовместимого контейнера.

Potatoswatter
источник
2

Единственное жесткое правило, где listнеобходимо использовать, - это когда вам нужно распределить указатели на элементы контейнера.

В отличие от vector, вы знаете, что память элементов не будет перераспределена. Если это так, то у вас могут быть указатели на неиспользуемую память, которая в лучшем случае является большой нет-нет, а в худшем SEGFAULT.

(Технически a vectorof *_ptrтакже будет работать, но в этом случае вы эмулируете, listтак что это просто семантика.)

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

iPherian
источник
2

Сделайте это просто -
В конце дня, когда вы не уверены, выбирая контейнеры в C ++, используйте это изображение блок-схемы (скажите спасибо): - введите описание изображения здесь

Вектор
1. Вектор основан на заразной память
2. вектора путь для небольшого набора данных
3. вектор выполнять быстрый при обходе по данным множественного
4. вектора вставки делеции медленно на огромном наборе данных , но быстро для очень небольших

List-
1. Список основан на динамической памяти
2. Список путь для очень огромного набора данных
3. Список сравнительно медленно на обходе небольшой набор данных , но быстро на огромный набор данных
4. список вставки делеции быстро на огромный набор данных, но медленный на меньших

XYZ XYZ
источник
1

Списки - это просто оболочка для двунаправленного LinkedList в stl, таким образом, предлагая функцию, которую вы можете ожидать от d-linklist, а именно вставку и удаление O (1). В то время как векторы представляют собой заразную последовательность данных, которая работает как динамический массив. PS - проще пересекать.

Ританкар Пол
источник
0

В случае вектора и списка основные отличия, которые меня выделяют, следующие:

вектор

  • Вектор хранит свои элементы в смежной памяти. Следовательно, произвольный доступ возможен внутри вектора, что означает, что доступ к элементу вектора очень быстрый, потому что мы можем просто умножить базовый адрес на индекс элемента для доступа к этому элементу. Фактически, для этой цели требуется только O (1) или постоянное время.

  • Поскольку вектор в основном оборачивает массив, каждый раз, когда вы вставляете элемент в вектор (динамический массив), он должен изменять свой размер, находя новый непрерывный блок памяти для размещения новых элементов, что требует больших затрат времени.

  • Он не потребляет дополнительной памяти для хранения указателей на другие элементы внутри него.

список

  • Список хранит свои элементы в несмежной памяти. Следовательно, произвольный доступ невозможен внутри списка, что означает, что для доступа к его элементам мы должны использовать указатели и проходить по списку, который медленнее, чем вектор. Это занимает O (n) или линейное время, которое медленнее, чем O (1).

  • Поскольку в списке используется несмежная память, время, затрачиваемое на вставку элемента в список, намного эффективнее, чем в случае его векторного аналога, поскольку перераспределение памяти исключается.

  • Он потребляет дополнительную память для хранения указателей на элемент до и после определенного элемента.

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

Пылкий кодер
источник
0

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

Сиддеш Павар
источник