哪个容器对C ++中的多次插入/删除最有效?

时间:2019-06-04 19:03:04

标签: c++ containers

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

// 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令人恐惧,并且vector领先deque。因此,就我而言,我已经充分利用了糟糕的情况,但显然没有!

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

5 个答案:

答案 0 :(得分:5)

我们不知道什么会使公司高兴。如果他们在没有明确理由的情况下拒绝ctrl+c,我还是不会为他们工作。而且,我们真的不知道确切的要求。是否要求您提供一种性能良好的实施方案?他们是否期望您通过分析一系列不同的实现来挤出所提供基准的最后一个百分比?

在申请过程中,后者对于家庭作业的挑战可能太多了。如果是第一个,则可以

  • 自己动手。给您提供的接口不可能比std containers之一更有效地实现...除非您的要求如此具体,以至于您可以编写在该特定基准下性能良好的东西。
  • std::vector了解数据局部性。请参见here,了解Bjarne本人主张std::vector而不是链表。
  • std::vector以简化实施。似乎您希望对容器进行排序,并且必须实现的接口非常适合std::set的接口。

假设容器需要保持排序,让我们仅比较迭代和擦除:

std::set

请注意,与 operation std::set std::vector insert log(N) N erase log(N) N 相比,log(N)的{​​{1}}可以忽略在binary_search中插入/删除的位置。

现在,您必须考虑上面列出的渐近复杂性完全忽略了内存访问的非线性。实际上,数据可能在内存(vector)中很远,从而导致许多高速缓存未命中,也可能像N一样是本地数据。 std::set仅赢取巨大的std::vector。为了了解差异,log(N)大致为N,而500000/log(500000)仅为26410

对于相当小的容器大小,我希望1000/log(1000)会胜过~100,但是在某个时候std::vector会超过缓存。该转折点的确切位置取决于许多因素,并且只能通过分析和测量来可靠地确定。

答案 1 :(得分:1)

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

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

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

如果您有0x10000个哈希表,则每个哈希表将0到0xFFFF都存储为int并使用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();
}

答案 2 :(得分:1)

没人知道哪个容器对多次插入/删除而言 MOST 有效。这就像问什么是汽车发动机最省油的设计。人们一直在创新汽车发动机。它们始终使效率更高。但是,我建议使用splay tree。插入或删除所需的时间是八卦树,该时间不是恒定的。有些插入需要很长时间,而有些插入只需要很短的时间。但是,每次插入/删除的平均时间始终保证为O(log n),其中n是存储在展开树中的项目数。对数时间非常有效。它应该足以满足您的目的。

答案 3 :(得分:0)

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

答案 4 :(得分:0)

此问题的基本解决方案可能是std::map<int, int> 其中key是您要存储的整数,value是出现的次数。

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

为了快速计数,您将需要实现自己的完整二叉树,由于您知道树的大小,因此您可以知道2个节点(上下边界节点)之间的节点数。您知道您对上限和下限节点进行了左转和右转。请注意,我们在谈论的是完整的二叉树,在一般的二叉树中,您无法快速进行计算。

要快速删除范围,我不知道如何使其快于线性。