Як я можу ефективно вибрати контейнер «Стандартна бібліотека» на C ++ 11?


135

Є добре відоме зображення (шпаргалка) під назвою "C ++ Container choice". Це схема вибору найкращого контейнера для потрібного використання.

Хтось знає, чи є вже версія C ++ 11?

Це попередній: eC ++ Вибір контейнера


6
Ніколи цього не бачив. Дякую!
WeaselFox

6
@WeaselFox: Це вже частина C ++ - Faq тут на SO.
Alok Save

4
C ++ 11 представив лише один новий істинний тип контейнерів: контейнери не упорядкованого_X. Включення їх лише заплутало б таблицю, оскільки існує ряд міркувань при вирішенні питання про те, чи підходить хеш-таблиця.
Нікол Болас

13
Джеймс правий, є більше випадків використання вектора, ніж те, що показано в таблиці. Перевага локалізації даних у багатьох випадках переважає відсутність ефективності в деяких операціях (швидше C ++ 11). Я не вважаю електронну діаграму такою корисною навіть для c ++ 03
David Rodríguez - dribeas

33
Це мило, але я думаю, що читання будь-якого загального підручника про структури даних дозволить залишити вас у такому стані, коли ви зможете не тільки заново винайти цю блок-схему за кілька хвилин, але й знати набагато більше корисних речей, за якими ця схема діаграми переглядається.
Андрій Томазос

Відповіді:


97

Не те, про що я знаю, але, мабуть, це можна зробити текстуально. Крім того, діаграма трохи відключена, бо listзагалом не такий хороший контейнер, і це не так forward_list. Обидва списки є дуже спеціалізованими контейнерами для нішевих додатків.

Щоб скласти таку діаграму, вам просто потрібно два простих рекомендації:

  • Виберіть спочатку семантику
  • Коли є кілька варіантів, перейдіть до найпростішого

Турбуватися про продуктивність спочатку марно. Найважливіші міркування "О" дійсно виникають лише тоді, коли ви починаєте обробляти кілька тисяч (або більше) предметів.

Є дві великі категорії контейнерів:

  • Асоціативні контейнери: вони мають findоперацію
  • Прості контейнери для послідовності

а потім ви можете створити кілька адаптерів на них: stack, queue, priority_queue. Я залишу адаптери тут, вони достатньо спеціалізовані, щоб бути впізнаваними.


Питання 1: Асоціативний ?

  • Якщо вам потрібно легко здійснити пошук за одним ключем, то вам потрібен асоціативний контейнер
  • Якщо вам потрібно сортувати елементи, то вам потрібен упорядкований асоціативний контейнер
  • В іншому випадку перейдіть до питання 2.

Питання 1.1: Упорядковано ?

  • Якщо вам не потрібно певне замовлення, використовуйте unordered_контейнер, інакше використовуйте його традиційний замовлений аналог.

Питання 1.2: Окремий ключ ?

  • Якщо ключ відокремлений від значення, використовуйте a map, інакше використовуйте aset

Питання 1.3: Дублікати ?

  • Якщо ви хочете зберегти дублікати, використовуйте клавішу a multi, інакше не робіть.

Приклад:

Припустимо, що у мене є декілька осіб з унікальним ідентифікатором, пов’язаним з ними, і я хотів би максимально просто отримати дані про особу з її ідентифікатора.

  1. Я хочу findфункцію, таким чином, асоціативний контейнер

    1.1. Я не міг менше дбати про замовлення, таким чином unordered_контейнер

    1.2. Мій ключ (ID) окремо від значення, з яким він асоціюється, таким чином amap

    1.3. Ідентифікатор унікальний, тому жоден дублікат не повинен повзати.

Відповідь: std::unordered_map<ID, PersonData>.


Питання 2: Стабільна пам'ять ?

  • Якщо елементи повинні бути стійкими в пам'яті (тобто вони не повинні переміщатися, коли контейнер змінений), то використовуйте деякі list
  • В іншому випадку перейдіть до питання 3.

Питання 2.1: Що ?

  • Постановка на a list; a forward_listкорисний лише для меншої площі пам'яті.

Питання 3: Динамічно розмір ?

  • Якщо контейнер має відомий розмір (під час компіляції), і цей розмір не буде змінено під час програми, а елементи можуть бути конфігурованими за замовчуванням або ви можете надати повний список ініціалізації (використовуючи { ... }синтаксис), тоді використовуйте array. Він замінює традиційний C-масив, але зручними функціями.
  • В іншому випадку перейдіть до питання 4.

Питання 4: Подвійний ?

  • Якщо ви хочете мати можливість вилучати предмети і з передньої, і ззаду, тоді використовуйте клавішу a deque, інакше використовуйте vector.

Ви помітите , що, за замовчуванням, якщо не потрібен асоціативний контейнер, ваш вибір буде vector. Виявляється, це також рекомендація Саттера і Строструпа .


5
+1, але з деякими примітками: 1) arrayне вимагає конструктивного типу за замовчуванням; 2) вибір multis - це не стільки про те, щоб дублікати було дозволено, а більше про те, чи зберігає їх значення (ви можете помістити дублікати в неконтейнери multi, просто трапляється, що зберігається лише один).
Р. Мартиньо Фернандес

2
Приклад трохи відключений. 1) ми можемо "знайти" (не функцію-члена, "<алгоритм>") на неасоціативному контейнері, 1.1), якщо нам потрібно знайти "ефективно", а не упорядкований_ буде O (1), а не O ( журнал n).
BlakBat

4
@BlakBat: map.find(key)набагато приємніше, ніж std::find(map.begin(), map.end(), [&](decltype(map.front()) p) { return p.first < key; }));хоча, тому важливо, семантично, findце функція члена, а не функція <algorithm>. Що стосується O (1) vs O (log n), то це не впливає на семантику; Я приберу "ефективно" з прикладу і заміню його на "легко".
Матьє М.

"Якщо елементи повинні бути стабільними в пам'яті ... тоді скористайтеся деяким списком" ... хммм, я думав, dequeмає і ця властивість?
Мартін Ба

@MartinBa: Так і ні. У dequeелементі елементи стабільні лише у тому випадку, якщо ви натискаєте / стискаєте на будь-якому кінці; якщо ви почнете вставляти / стирати в середині, то до N / 2 елементів змішуються, щоб заповнити створений пробіл.
Маттьє М.

51

Мені подобається відповідь Матьє, але я збираюсь переробити блок-схему так:

Коли НЕ використовувати std :: vector

За замовчуванням, якщо вам потрібен контейнер з речами, використовуйте std::vector. Таким чином, кожен інший контейнер виправданий лише наданням певної функціональної альтернативи std::vector.

Конструктори

std::vectorвимагає, щоб його вміст було зручним для переміщення, оскільки йому потрібно вміти переміщувати елементи навколо. Це не є страшним тягарем для вмісту (зауважте, що конструктори за замовчуванням не потрібні , завдяки emplaceтощо). Однак для більшості інших контейнерів не потрібен конкретний конструктор (знову ж таки, завдяки emplace). Тож якщо у вас є об'єкт, де ви абсолютно не можете реалізувати конструктор переміщення, вам доведеться вибрати щось інше.

A std::dequeбула б загальною заміною, яка має багато властивостей std::vector, але вставляти їх можна лише на обох кінцях деки. Вставки в середині вимагають переміщення. Місце std::listне пред'являє жодних вимог до його змісту.

Потреби булі

std::vector<bool>не. Ну, це стандартно. Але це не vectorу звичному розумінні, оскільки операції, які std::vectorзазвичай дозволяють, заборонені. І це, безумовно , не містить bools .

Тому, якщо вам потрібна реальна vectorповедінка з контейнера bools, ви не збираєтесь її отримувати std::vector<bool>. Тож вам доведеться погодитись із std::deque<bool>.

Пошук

Якщо вам потрібно знайти елементи в контейнері, а пошуковий тег не може бути просто індексом, можливо, вам доведеться відмовитися std::vectorна користь setі map. Зверніть увагу на ключове слово " може "; сортування std::vector- іноді розумна альтернатива. Або Boost.Container's flat_set/map, який реалізує відсортовану std::vector.

Зараз існує чотири варіанти, кожна з яких має власні потреби.

  • Використовуйте, mapколи тег пошуку - це не те саме, що предмет, який ви шукаєте. В іншому випадку використовуйте set.
  • Використовуйте, unorderedколи у контейнері багато предметів, а ефективність пошуку абсолютно не повинна бути O(1), а не O(logn).
  • Використовуйте, multiякщо вам потрібно кілька елементів, щоб мати один і той же тег пошуку.

Замовлення

Якщо вам потрібен контейнер з предметами, який слід завжди сортувати на основі певної операції порівняння, ви можете використовувати set. Або multi_setякщо вам потрібно кілька предметів, щоб мати однакове значення.

Або ви можете використовувати відсортовану std::vector, але вам доведеться тримати її відсортованою.

Стабільність

Коли ітератори та посилання недійсні, іноді викликає занепокоєння. Якщо вам потрібен список елементів, наприклад, у вас є ітератори / покажчики на ці предмети в різних інших місцях, то std::vectorпідхід до визнання недійсним може виявитися недоцільним. Будь-яка операція вставки може спричинити недійсність, залежно від поточного розміру та потужності.

std::listнадає тверду гарантію: ітератор та пов'язані з ним посилання / покажчики недійсні лише тоді, коли сам товар вилучено з контейнера. std::forward_listчи є, якщо пам'ять викликає серйозне занепокоєння.

Якщо це занадто сильна гарантія, std::dequeнадає більш слабку, але корисну гарантію. Недійсність є результатом вставок посередині, але вставки в голову або хвіст викликають лише недійсні ітератори , а не покажчики / посилання на елементи в контейнері.

Виконання вставки

std::vector забезпечує лише дешеву вставку в кінці (і навіть тоді вона стає дорогою, якщо ви підірвете ємність).

std::listє дорогим з точки зору продуктивності (кожен щойно вставлений елемент коштує розподілу пам'яті), але він є послідовним . Він також пропонує періодично незамінну можливість перемішувати предмети практично без витрат на продуктивність, а також торгувати предметами з іншими std::listконтейнерами того ж типу без втрати продуктивності. Якщо вам потрібно перетасувати речі навколо багато , використання std::list.

std::dequeзабезпечує введення / видалення постійного часу на голові та хвості, але вставка посередині може бути досить дорогою. Тож якщо вам потрібно додати / вилучити речі спереду та ззаду, std::dequeможливо, це вам потрібно.

Слід зазначити, що завдяки семантиці переміщення std::vectorпродуктивність вставки може бути не такою поганою, як раніше. Деякі реалізації реалізували форму копіювання елементів на основі семантичного переміщення (так звана "своптимізація"), але тепер, коли переміщення є частиною мови, це відповідає мандату.

Немає динамічних виділень

std::array- прекрасний контейнер, якщо ви хочете отримати найменші динамічні виділення. Це просто обгортка навколо масиву С; це означає, що його розмір повинен бути відомий під час компіляції . Якщо ви можете з цим жити, то використовуйте std::array.

Як сказано, використання std::vectorта reserveрозмір розміру буде працювати так само добре для обмежених std::vector. Таким чином, фактичний розмір може змінюватись, і ви отримуєте лише одне розподілення пам'яті (якщо тільки ви не збільшите ємність).


1
Ну, мені дуже подобається ваша відповідь :) WRT, зберігаючи впорядкований вектор, окрім того std::sort, є також std::inplace_mergeцікавий, щоб легко розмістити нові елементи (а не std::lower_bound+ std::vector::insertдзвінок). Приємно дізнатися про flat_setі flat_map!
Матьє М.

2
Ви також не можете використовувати вектор з 16-байтними вирівняними типами. Також гарною заміною vector<bool>є vector<char>.
Зворотний

@ Inverse: "Ви також не можете використовувати вектор з 16-байтними вирівняними типами." Хто каже? Якщо std::allocator<T>це не підтримує це вирівнювання (і я не знаю, чому б цього не було), ви завжди можете використовувати власний спеціальний розподільник.
Нікол Болас

2
@ Inverse: C ++ 11's std::vector::resizeмає перевантаження, яке не приймає значення (воно просто приймає новий розмір; будь-які нові елементи будуть створені за замовчуванням на місці). Крім того, чому компілятори не можуть правильно вирівняти параметри значення, навіть коли їм оголошено таке вирівнювання?
Нікол Болас

1
bitsetдля bool, якщо ви знаєте розмір заздалегідь en.cppreference.com/w/cpp/utility/bitset
bendervader


1

Ось швидке віджимання, хоча воно, певно, потребує роботи

Should the container let you manage the order of the elements?
Yes:
  Will the container contain always exactly the same number of elements? 
  Yes:
    Does the container need a fast move operator?
    Yes: std::vector
    No: std::array
  No:
    Do you absolutely need stable iterators? (be certain!)
    Yes: boost::stable_vector (as a last case fallback, std::list)
    No: 
      Do inserts happen only at the ends?
      Yes: std::deque
      No: std::vector
No: 
  Are keys associated with Values?
  Yes:
    Do the keys need to be sorted?
    Yes: 
      Are there more than one value per key?
      Yes: boost::flat_map (as a last case fallback, std::map)
      No: boost::flat_multimap (as a last case fallback, std::map)
    No:
      Are there more than one value per key?
      Yes: std::unordered_multimap
      No: std::unordered_map
  No:
    Are elements read then removed in a certain order?
    Yes:
      Order is:
      Ordered by element: std::priority_queue
      First in First out: std::queue
      First in Last out: std::stack
      Other: Custom based on std::vector????? 
    No:
      Should the elements be sorted by value?
      Yes: boost::flat_set
      No: std::vector

Ви можете помітити, що це дико відрізняється від версії C ++ 03, насамперед через те, що я дійсно не люблю пов'язані вузли. З'єднані контейнери з вузлами зазвичай можуть бути обіграні по продуктивності не пов'язаним контейнером, за винятком кількох рідкісних ситуацій. Якщо ви не знаєте, що це за ситуації, і у вас є доступ до підвищення, не використовуйте пов'язані контейнери для вузлів. (std :: список, std :: slist, std :: map, std :: multimap, std :: set, std :: multiset). Цей список зосереджується здебільшого на малих та середньосторонніх контейнерах, оскільки (А) це 99,99% від того, що ми маємо справу з кодом, і (В) Великій кількості елементів потрібні спеціальні алгоритми, а не різні контейнери.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.