【问题标题】:Is remove_if and then erase efficient on vector?remove_if 然后擦除向量是否有效?
【发布时间】:2017-06-26 04:29:17
【问题描述】:

虽然有几十个关于remove_if + erase for vector的问题。 我找不到这种动作的表现。 当我写:

    myVector.erase(remove_if(myVector.begin(),
                         myVector.end(),
                         some_predicate), myVector.end());

remove if 将返回一个迭代器,指向最后一个相关项 + 1(我们称之为 X)。 我相信这会在 O(n) 内发生。

但是擦除将如何工作?

  • 如果擦除尝试从 X 删除到 myVector.end(),它将是 O(n^2),因为它会导致将向量复制到新位置,并且会有 O(n) 新分配从堆中。
  • 但如果它将从 myVector.end() 删除到 X,它可以在 O(n) 内完成,更重要的是不会分配新内存(我试图避免这种情况)。

谢谢。

【问题讨论】:

  • std::remove_if() 的复杂性是 正如in the documentation 所述,正是谓词的 std::distance(first,last) 应用
  • vector::erase 在这种情况下不会导致任何重新分配。
  • "如果擦除将尝试从 X 删除到 myVector.end(),它将是 O(n^2)".... 究竟如何?
  • @OopsUser 从向量中间移除不会导致重新分配!使向量更小不需要更多内存!您不会向后移动要删除的元素,因为您将要删除它们。您将那些 这些元素之后的元素向后移动,但这仍然不会重新分配,并且当您执行 v.erase(X, v.end()) 时,之后 没有任何 元素,所以您摧毁他们。
  • @OopsUser:这不是vector.erase(x, v.end()) 的工作方式。 @Jonathan 的回答很好地解释了它是如何工作的!

标签: c++ performance vector std erase


【解决方案1】:

vector::erase 的复杂度是well-defined

线性:调用T的析构函数的次数与擦除的元素个数相同,调用T的赋值运算符的次数等于擦除元素后向量中的元素个数

它在内部的工作方式(即如何准确地删除你的元素)是无关紧要的。

remove_if的复杂度也是defined,是

恰好是谓词的 std::distance(first, last) 应用。

所以你的代码具有线性复杂度。

【讨论】:

  • @OopsUser,不,他们都没有分配内存,为什么要分配?
  • @OopsUser,是的,remove_if 将以这样一种方式交换元素,即所有需要删除的元素最终都在向量的尾部。然后你只需要砍掉那条尾巴。这就是它返回一个迭代器的原因:这个迭代器指向最后一个 不需要 需要删除的元素。该迭代器之后的所有内容都是需要删除的元素。
  • @OopsUser 你只需要在向量变大时分配。要擦除矢量末尾的元素,只需销毁它们,然后更改矢量的size()capacity() 保持不变。所以remove_if 将不需要的元素移动到最后,然后erase() 销毁它们并减少size()
  • @OopsUser,在您的情况下,擦除不会从数组中间删除。当它得到它的迭代器时,它需要删除的所有元素都已经结束了。假设您有一个数组123456,您需要删除所有偶数。运行remove_if 后,您的数组看起来像135246,并且您的迭代器指向2。然后你将它传递给erase,它会截断数组的末尾(从2 到末尾的所有元素)
  • @OopsUser,它不需要(有些实现会这样做,有些则不需要)。如果它要删除从 X 到最后的所有内容,它只会破坏它们并减小大小。
【解决方案2】:

为什么不使用 swap 'n pop 方法?我们在优化向量中的擦除方面犯了很多错误,发现这是最快的,因为它具有O(1) 复杂性。唯一的缺点是它不保持秩序。很多情况下都很好。这是此类操作的模板方法:

template<typename T>
inline typename std::vector<T>::iterator unorderedErase(std::vector<T>& p_container,
                                                        typename std::vector<T>::iterator p_it)
{
    if (p_it != p_container.end() - 1)
    {
        std::swap(*p_it, p_container.back());
        p_container.pop_back();
        return p_it;
    }
    // else
    p_container.pop_back();
    return p_container.end();
}

【讨论】:

  • 太棒了,不遍历容器如何获取需要删除的元素的迭代器?
  • remove_if+erase 的总复杂度是O(N)(直到常数),不是O(n^2)。当然,如果你能比线性搜索更快地获得迭代器(例如,如果你的向量是预先排序的)你会得到更好的性能。
  • @SingerOfTheFall remove_if 不能这样工作,因为它必须保留未删除元素的顺序,因此它不只是进行一次交换。
  • 和移动元素以保持顺序会在大型容器中花费一些严重的 CPU 周期。我们在 memmove 调用中得到了大量的痕迹,只是为了擦除。因此,如果不需要保留顺序,这样的交换确实是最快的方法。
  • std::move 或类似的东西被调用。这将在擦除后移动所有元素,以保持顺序。最坏的情况,删除一个巨大容器中的第一个元素,你会看到会发生什么:)
【解决方案3】:

考虑这个向量:

|0|1|2|3|4|5|6|7|8|9|

我们使用remove_if 删除所有4 的倍数的元素:

std::remove_if(v.begin(), v.end(), [](auto i){ return i != 0 && !(i%4); });

这开始使用迭代器 X 遍历向量,直到找到谓词返回 true 的元素:

|0|1|2|3|4|5|6|7|8|9|
         X

这是我们要删除的第一个元素。

接下来,它创建另一个指向下一个元素 Y = X+1 的迭代器,并检查 *Y 的谓词:

|0|1|2|3|4|5|6|7|8|9|
         X Y

谓词是假的,所以我们想保留那个元素,所以它将下一个元素分配给我们要删除的元素,通过*X = std::move(*Y)

|0|1|2|3|5|5|6|7|8|9|
         X Y            *X = std::move(*Y)

所以我们有两个迭代器,X 和 Y,其中 X 指向“输出”中的下一个元素(即我们不删除的元素),Y 是下一个要考虑删除的元素。

我们将两个迭代器移动到下一个位置,检查 Y 的谓词(再次为假),然后进行另一个赋值:

|0|1|2|3|5|6|6|7|8|9|
           X Y          *X = std::move(*Y)

然后在下一个位置再次执行相同的操作:

|0|1|2|3|5|6|7|7|8|9|
             X Y       *X = std::move(*Y)

然后它继续前进,但发现谓词对 Y 为真

|0|1|2|3|5|6|7|7|8|9|
               X Y

所以它只是增加 Y,跳过该元素,因此不会将其复制到 X 的“输出”位置:

|0|1|2|3|5|6|7|7|8|9|
               X   Y 

谓词对 Y 不成立,因此将其分配给 X:

|0|1|2|3|5|6|7|9|8|9|
               X   Y     *X = std::move(*Y)

然后它再次增加 X 和 Y

|0|1|2|3|5|6|7|9|8|9|
                 X   Y

现在 Y 已经结束,所以我们返回 X(它指向输出序列的末尾,即我们想要保留的元素)。

remove_if 返回 X 之后,我们调用v.erase(X, v.end()),因此向量为从 X 到末尾的每个元素调用析构函数:

|0|1|2|3|5|6|7|9|~|~|
                 X   end

然后设置大小,使向量在 X 处结束:

|0|1|2|3|5|6|7|9|
                 end

v.capacity() &gt;= v.size()+2 之后,因为最后两个元素使用的内存仍然存在,但未使用。

【讨论】:

  • @RustyX 不!您不会破坏旧元素并构建新元素,而是为现有元素分配新值。
  • @Caleth,不会的!这是一项任务,它不会破坏任何东西。
  • @OopsUser 擦除会将元素从X+2 移动到end() 向前2 个位置,然后减小大小。本质上是for(;it2 != end(); ++it1, ++it2) {*it1 = std::move(*it2); } /*destruct elements from it1 to end */ size-=2;
  • @OopsUser 看到这个程序的输出:coliru.stacked-crooked.com/a/552e181a61d48033(你移动你想要保留的元素,然后销毁最后的元素)。除非您正在增加向量,否则您永远不需要重新分配,因为您总是有足够的空间来使用。
猜你喜欢
  • 2012-02-21
  • 2021-07-03
  • 2014-10-23
  • 1970-01-01
  • 2018-06-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-10-05
相关资源
最近更新 更多