【问题标题】:How can I efficiently select a Standard Library container in C++11?如何在 C++11 中有效地选择标准库容器?
【发布时间】:2012-05-28 18:44:19
【问题描述】:

有一个众所周知的图像(备忘单)称为“C++ 容器选择”。这是一个为所需用途选择最佳容器的流程图。

有人知道它是否已经有 C++11 版本吗?

这是上一个:

【问题讨论】:

  • 以前从未见过。谢谢!
  • @WeaselFox:它已经是 SO 上 C++-Faq 的一部分了。
  • C++11 只引入了一种新的真正的容器类型:unordered_X 容器。包含它们只会使表格变得相当混乱,因为在决定哈希表是否合适时需要考虑许多因素。
  • James 是对的,使用向量的情况比表格显示的要多。在许多情况下,数据局部性的优势胜过某些操作缺乏效率(很快 C++11)。即使对于 c++03,我也没有发现电子图表如此有用
  • 这很可爱,但我认为阅读任何关于数据结构的普通教科书会让你处于一种状态,即你不仅可以在几分钟内重新发明这个流程图,而且还知道更多有用的东西此流程图掩盖了。

标签: c++ c++11 c++-faq


【解决方案1】:

我不知道,但我猜它可以通过文本完成。此外,图表略有偏差,因为list 通常不是一个很好的容器,forward_list 也不是。这两个列表都是针对小众应用的非常专业的容器。

要构建这样的图表,您只需要两个简单的准则:

  • 首先选择语义
  • 当有多种选择时,选择最简单的一种

起初担心性能通常是没有用的。只有当您开始处理数千(或更多)项时,才会真正考虑到大 O。

容器有两大类:

  • 关联容器:它们有一个find 操作
  • 简单序列容器

然后您可以在它们之上构建多个适配器:stackqueuepriority_queue。我将把适配器放在这里,它们足够专业,可以识别。


问题 1:联想

  • 如果您需要通过一个键轻松搜索,那么您需要一个关联容器
  • 如果您需要对元素进行排序,那么您需要一个有序的关联容器
  • 否则,请跳至问题 2。

问题 1.1:有序

  • 如果您不需要特定订单,请使用 unordered_ 容器,否则使用其传统的有序对应物。

问题 1.2:分离键 ?

  • 如果键与值分开,使用map,否则使用set

问题 1.3:重复

  • 如果要保留重复项,请使用multi,否则不要。

例子:

假设我有几个人与他们关联的唯一 ID,我想尽可能简单地从其 ID 中检索个人数据。

  1. 我想要一个find 函数,因此是一个关联容器

1.1。我不在乎订单,因此是unordered_ 容器

1.2。我的键 (ID) 与其关联的值是分开的,因此是 map

1.3。 ID 是唯一的,因此不应出现重复。

最终答案是:std::unordered_map<ID, PersonData>


问题 2:内存稳定

  • 如果元素在内存中应该是稳定的(即当容器本身被修改时它们不应该移动),那么使用一些list
  • 否则,请跳至问题 3。

问题 2.1:哪个

  • 选择listforward_list 仅对减少内存占用有用。

问题 3:动态调整大小

  • 如果容器有一个已知的大小(在编译时),并且这个大小在程序运行过程中不会改变,并且元素默认是可构造的您可以提供完整的初始化列表(使用{ ... } 语法),然后使用array。它取代了传统的 C 数组,但具有方便的功能。
  • 否则,请跳至问题 4。

问题 4:双头

  • 如果您希望能够从正面和背面移除项目,则使用deque,否则使用vector

您会注意到,默认情况下,除非您需要关联容器,否则您的选择将是 vector。原来也是Sutter and Stroustrup's recommendation

【讨论】:

  • +1,但有一些注意事项: 1) array 不需要默认的可构造类型; 2) 选择multis 与其说是允许重复,还不如说是保留它们是否重要(你可以将重复放在非multi容器中,只是碰巧只保留了一个)。
  • 这个例子有点不对劲。 1)如果我们需要“有效地”找到,我们可以在非关联容器上“找到”(不是成员函数,“”,1.1),并且 unordered_ 将是 O(1) 而不是 O(记录 n)。
  • @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 个元素将被打乱以填补创建的空白。
【解决方案2】:

这是上述流程图的 C++11 版本。 [最初发布时未注明原作者Mikael Persson]

【讨论】:

  • @NO_NAME 哇,我很高兴有人费心引用来源。
【解决方案3】:

这是一个快速旋转,虽然它可能需要工作

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 版本非常不同,主要是因为我真的不喜欢链接节点。链接节点容器通常可以在性能上被非链接容器击败,除非在极少数情况下。如果您不知道这些情况是什么,并且可以访问 boost,请不要使用链接节点容器。 (std::list、std::slist、std::map、std::multimap、std::set、std::multiset)。此列表主要关注中小型容器,因为 (A) 这是我们在代码中处理的 99.99%,并且 (B) 大量元素需要自定义算法,而不是不同的容器。

【讨论】:

    【解决方案4】:

    我喜欢 Matthieu 的回答,但我将重述流程图如下:

    何时不使用 std::vector

    默认情况下,如果您需要一个容器,请使用std::vector。因此,每个其他容器只有通过提供一些替代 std::vector 的功能来证明是合理的。

    构造函数

    std::vector 要求其内容是可移动构造的,因为它需要能够随机播放项目。这对内容来说并不是一个可怕的负担(请注意,默认构造函数不需要,这要感谢emplace 等等)。但是,大多数其他容器不需要任何特定的构造函数(再次感谢emplace)。因此,如果您有一个绝对不能实现移动构造函数的对象,那么您将不得不选择其他东西。

    std::deque 将是一般替换,具有std::vector 的许多属性,但您只能在双端队列的任一端插入。中间的插入物需要移动。 std::list 对其内容没有任何要求。

    需要布尔

    std::vector<bool> 是……不是。嗯,这是标准的。但它不是通常意义上的vector,因为std::vector 通常允许的操作是被禁止的。而且它肯定不包含bools

    因此,如果您需要来自bools 容器的真实vector 行为,您将无法从std::vector<bool> 获得它。所以你必须使用std::deque<bool>

    搜索

    如果您需要在容器中查找元素,并且搜索标签不能只是索引,那么您可能需要放弃std::vector,转而使用setmap。注意关键词“可能”;排序的std::vector 有时是一个合理的选择。或者Boost.Container的flat_set/map,它实现了一个排序的std::vector

    现在有四种变体,每种都有自己的需求。

    • 当搜索标签与您要查找的项目本身不同时,请使用map。否则使用set
    • 当容器中有很多个项目并且搜索性能绝对需要O(1)而不是O(logn)时,请使用unordered
    • 如果您需要多个项目具有相同的搜索标签,请使用 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 是一个很好的容器,如果你想要尽可能少的动态分配。它只是一个 C 数组的包装器;这意味着它的大小必须在编译时知道。如果你能接受,那就使用std::array

    话虽如此,使用std::vectorreserve 确定大小对于有界std::vector 也同样有效。这样,实际大小可能会有所不同,并且您只能获得一个内存分配(除非您耗尽了容量)。

    【讨论】:

    • 好吧,我也很喜欢你的回答 :) WRT 保持向量排序,除了std::sort,还有std::inplace_merge 很有趣,可以轻松放置新元素(而不是@ 987654373@ + std::vector::insert 电话)。很高兴了解flat_setflat_map
    • 您也不能使用具有 16 字节对齐类型的向量。 vector<bool> 也是一个很好的替代品vector<char>
    • @Inverse:“您也不能使用具有 16 字节对齐类型的向量。”谁说的?如果std::allocator<T> 不支持这种对齐方式(我不知道为什么不支持),那么您始终可以使用自己的自定义分配器。
    • @Inverse:C++11 的 std::vector::resize 有一个不带值的重载(它只需要新的大小;任何新元素都将在原地默认构造)。另外,为什么编译器无法正确对齐值参数,即使它们被声明为具有这种对齐方式?
    • bitset for bool 如果你事先知道大小en.cppreference.com/w/cpp/utility/bitset
    猜你喜欢
    • 2015-12-09
    • 2012-03-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多