【问题标题】:Which container is most efficient for multiple insertions / deletions in C++?哪个容器对于 C++ 中的多次插入/删除最有效?
【发布时间】:2019-06-05 08:12:26
【问题描述】:

作为申请过程的一部分,我被设置了一项家庭作业挑战(顺便说一句,我被拒绝了;否则我不会写这个),我将在其中实现以下功能:

// Store a collection of integers
class IntegerCollection {
public:
  // Insert one entry with value x
  void Insert(int x);

  // Erase one entry with value x, if one exists
  void Erase(int x);

  // Erase all entries, x, from <= x < to
  void Erase(int from, int to);

  // Return the count of all entries, x, from <= x < to
  size_t Count(int from, int to) const;

然后对这些功能进行了一系列测试,其中大部分都是微不足道的。最后的测试是真正的挑战,因为它执行了 500,000 次单次插入、500,000 次计数调用和 500,000 次单次删除。

IntegerCollection 的成员变量没有指定,所以我必须选择如何存储整数。自然地,STL 容器似乎是一个好主意,保持它的分类似乎是一种保持效率的简单方法。

这是我使用vector 的四个函数的代码:

// Previous bit of code shown goes here 

private:
  std::vector<int> integerCollection;
};

void IntegerCollection::Insert(int x) {

  /* using lower_bound to find the right place for x to be inserted
  keeps the vector sorted and makes life much easier */
  auto it = std::lower_bound(integerCollection.begin(), integerCollection.end(), x);
  integerCollection.insert(it, x);
}

void IntegerCollection::Erase(int x) {

  // find the location of the first element containing x and delete if it exists
  auto it = std::find(integerCollection.begin(), integerCollection.end(), x);

  if (it != integerCollection.end()) {
    integerCollection.erase(it);
  }

}

void IntegerCollection::Erase(int from, int to) {

  if (integerCollection.empty()) return;

  // lower_bound points to the first element of integerCollection >= from/to
  auto fromBound = std::lower_bound(integerCollection.begin(), integerCollection.end(), from);
  auto toBound = std::lower_bound(integerCollection.begin(), integerCollection.end(), to);

  /* std::vector::erase deletes entries between the two pointers
  fromBound (included) and toBound (not indcluded) */
  integerCollection.erase(fromBound, toBound);

}

size_t IntegerCollection::Count(int from, int to) const {

  if (integerCollection.empty()) return 0;

  int count = 0;

  // lower_bound points to the first element of integerCollection >= from/to
  auto fromBound = std::lower_bound(integerCollection.begin(), integerCollection.end(), from);
  auto toBound = std::lower_bound(integerCollection.begin(), integerCollection.end(), to);

  // increment pointer until fromBound == toBound (we don't count elements of value = to)
  while (fromBound != toBound) {
    ++count; ++fromBound;
  }

  return count;

}

公司回复我说他们不会继续前进,因为我选择的容器意味着运行时复杂性太高。我还尝试使用listdeque 并比较了运行时间。正如我所料,我发现list 很糟糕,而vectordeque 更胜一筹。所以就我而言,我已经充分利用了糟糕的情况,但显然不是!

我想知道在这种情况下使用的正确容器是什么? deque 只有在我可以保证插入或删除到容器的末端并且 list 占用内存时才有意义。还有什么我完全忽略的吗?

【问题讨论】:

  • 您似乎希望对您的容器进行排序,对吗?那么std::multiset (或者如果你不想重复的话,或者简单的std::set)怎么样?
  • 如果您知道最小和最大可能输入的范围,您可以使用存储桶执行此操作并实现 O[1] 插入、删除等。占用的内存与 [max-min] 成正比。
  • void Erase(int from, int to); 表示排序,如果没有枚举,无序集合不会给你。如果您的集合允许重复,std::multiset 是一个可行的选择,否则 std::set 是候选。
  • Bjarne 说“使用向量”,所以如果公司拒绝了您的代码 - 向他们展示这个(非常有教育意义的)视频:youtube.com/watch?v=YQs6IC-vgmo
  • std::vector 在性能上胜过其他容器,可以处理惊人的大数据集,这仅仅是因为它在缓存位置方面无与伦比。直观地说,std::set 的某些变体会是一个更安全的选择,但值得衡量的是它开始击败std::vector 的数据集大小。当您达到 500,000 个元素大小时,std::set 很可能赶不上std::vector。但如果他们关心性能,他们就会指定/提供硬件。似乎他们只是对时间复杂度的顺序感兴趣。

标签: c++ containers


【解决方案1】:

我们不知道什么会让公司高兴。如果他们拒绝std::vector 没有简洁的理由,我无论如何都不想为他们工作。此外,我们并不真正了解确切的要求。您是否被要求提供一个性能相当好的实现?他们是否期望您通过分析一堆不同的实现来挤出所提供基准的最后百分比?

对于作为申请过程的一部分的家庭作业挑战来说,后者可能太多了。如果是第一个,你也可以

  • 自己动手。您获得的接口不太可能比std containers 之一更有效地实现......除非您的要求非常具体,以至于您可以编写在该特定基准下表现良好的东西。
  • std::vector 用于数据本地化。参见例如here Bjarne 自己提倡std::vector 而不是链表。
  • std::set 便于实施。看起来您希望对容器进行排序,并且您必须实现的接口非常适合 std::set 的接口。

假设容器需要保持排序,我们只比较插入和擦除:

   operation         std::set          std::vector
   insert            log(N)            N
   erase             log(N)            N

请注意,与N 相比,binary_searchvector 中查找插入/擦除位置的log(N) 可以忽略不计。

现在你必须考虑上面列出的渐近复杂度完全忽略了内存访问的非线性。实际上,数据可能在内存中很远(std::set)导致许多缓存未命中,或者它可以像std::vector 一样位于本地。 log(N) 只赢得巨大的 N。要了解500000/log(500000) 的区别大致是264101000/log(1000) 只是~100

我希望std::vector 在相当小的容器尺寸上优于std::set,但在某些时候log(N) 胜过缓存。这个转折点的确切位置取决于许多因素,只有通过剖析和测量才能可靠地确定。

【讨论】:

    【解决方案2】:

    没有人知道哪个容器对于多次插入/删除效率最高。这就像问汽车发动机最省油的设计是什么。人们总是在汽车发动机上进行创新。他们一直在制造更高效的产品。但是,我会推荐splay tree。插入或删除所需的时间是展开树不是恒定的。有些插入需要很长时间,有些只需要很短的时间。但是,每次插入/删除的平均时间总是保证为O(log n),其中n 是存储在展开树中的项目数。对数时间非常有效。它应该足以满足您的目的。

    【讨论】:

      【解决方案3】:

      首先想到的是对整数值进行哈希处理,以便可以在恒定时间内完成单次查找。

      可以对整数值进行散列运算以计算布尔或位数组的索引,用于判断整数值是否在容器中。

      通过对特定整数范围使用多个哈希表,可以从那里加快计算和删除大范围的速度。

      如果您有 0x10000 个哈希表,每个存储的 int 从 0 到 0xFFFF 并且使用 32 位整数,您可以屏蔽并移动 int 值的上半部分,并将其用作索引以找到正确的哈希表插入/删除值。

      IntHashTable containers[0x10000];
      u_int32 hashIndex = (u_int32)value / 0x10000;
      u_int32int valueInTable = (u_int32)value - (hashIndex * 0x10000);
      containers[hashIndex].insert(valueInTable);
      

      例如计数可以这样实现,如果每个哈希表都对它包含的元素数进行计数:

      indexStart = startRange / 0x10000;
      indexEnd = endRange / 0x10000;
      
      int countTotal = 0;
      for (int i = indexStart; i<=indexEnd; ++i) {
         countTotal += containers[i].count();
      }
      

      【讨论】:

      • 建议使用散列容器然后切换到二分搜索并不容易,即使假设这可能适用于手头的问题。至少应该提供一个实现的提示,才能将其作为一个好的答案。
      • 我不建议在散列容器和二进制搜索之间切换。我建议使用使用哈希表的二进制搜索来确定特定范围内是否存在整数。
      • 好的,我删除了我对二进制搜索所做的评论,并添加了一个希望更好的解释来澄清。
      【解决方案4】:

      不确定是否真的需要使用排序来删除范围。它可能基于位置。无论如何,这里有一个链接,其中包含要使用哪个 STL 容器的一些提示。 In which scenario do I use a particular STL container? 仅供参考。 Vector 可能是一个不错的选择,但正如您所知,它会进行大量的重新分配。我更喜欢双端队列,因为它不需要大块内存来分配所有项目。对于您的要求,列表可能更合适。

      【讨论】:

        【解决方案5】:

        这个问题的基本解决方案可能是std::map&lt;int, int&gt; 其中 key 是您要存储的整数, value 是出现次数。

        问题在于您无法快速删除/计算范围。换句话说,复杂度是线性的。

        为了快速计数,您需要实现自己的 complete 二叉树,因为您知道树的大小,所以您可以知道 2 个节点(上限和下限节点)之间的节点数,并且你知道你在上下界节点上左转和右转了多少次。注意,我们说的是完全二叉树,一般来说二叉树你不能让这个计算很快。

        对于快速范围移除,我不知道如何使它比线性更快。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2010-11-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-03-01
          • 2013-03-13
          • 1970-01-01
          相关资源
          最近更新 更多