【问题标题】:Difference between std::remove and erase for vector?向量的 std::remove 和擦除之间的区别?
【发布时间】:2013-10-18 06:48:14
【问题描述】:

我有一个疑问,我想在脑海中澄清一下。我知道std::vector 在erase 和std::remove 之间的不同行为,其中第一个从向量中物理删除一个元素,减小大小,另一个只是移动一个元素,使容量保持不变。

这仅仅是出于效率原因吗?通过使用erase,std::vector中的所有元素都会被移位1,造成大量的拷贝; std::remove 只是进行“逻辑”删除,并通过移动东西使向量保持不变。如果物体很重,那么这种差异可能很重要,对吧?

【问题讨论】:

  • @larsmans,你错了。 OP 是正确的 - 有 std::remove 模板函数可以执行 OP 所说的操作。
  • @larsmans 不正确。 std::remove 是一种将集合中的元素标记为已删除的算法。
  • 参见 Scott Meyers 的“Item 32”,“Effective STL”,Addison-Wesley,2001 年,第 139-143 页。

标签: c++ vector stl


【解决方案1】:

这仅仅是出于效率的原因吗?通过使用擦除 std::vector 中的所有元素将被移动 1 导致大量副本; std::remove 仅执行“逻辑”删除,并通过移动事物使向量保持不变。如果物体很重,那么差异可能很重要,对吧?

使用这个成语的原因正是如此。在性能上有好处,但在单次擦除的情况下没有。重要的是您是否需要从向量中删除多个元素。在这种情况下,std::remove 将每个 not removed 元素仅复制一次到其最终位置,而 vector::erase 方法会将所有元素从该位置移动到末尾多次。考虑:

std::vector<int> v{ 1, 2, 3, 4, 5 };
// remove all elements < 5

如果您逐个删除元素,则会删除 1,从而导致剩余元素的副本被移动 (4)。然后您将删除 2 并将所有剩余元素移动一 (3)...如果您看到这种模式,这是一个 O(N^2) 算法。

在std::remove 的情况下,算法维护一个读写头,并在容器上迭代。对于前 4 个元素,将移动读取头并测试元素,但不会复制任何元素。仅对于第五个元素,对象将从最后一个位置复制到第一个位置,并且算法将以单个副本完成并将迭代器返回到第二个位置。这是一个O(N) 算法。后面带有范围的std::vector::erase 将导致所有剩余元素的破坏并调整容器的大小。

正如其他人所提到的,在标准库中,算法应用于迭代器,并且缺乏对被迭代序列的了解。这种设计比算法知道容器的其他方法更灵活,因为算法的单个实现可以与任何符合迭代器要求的序列一起使用。考虑例如std::remove_copy_if,它甚至可以在没有容器的情况下使用,通过使用生成/接受序列的迭代器:

std::remove_copy_if(std::istream_iterator<int>(std::cin),
                    std::istream_iterator<int>(),
                    std::ostream_iterator<int>(std::cout, " "),
                    [](int x) { return !(x%2); } // is even
                    );

该单行代码将过滤掉标准输入中的所有偶数并将其转储到标准输出,而无需将所有数字加载到容器中的内存中。这是拆分的优点,缺点是算法不能修改容器本身,只能修改迭代器引用的值。

【讨论】:

  • 这是一个很好的解释!我只专注于删除 1 个元素,而我完全忘记了 std::remove 删除了多个元素。非常感谢。
  • @AbruzzoForteeGentile:在删除单个元素的情况下,两种方法在成本方面完全相同。从被移除元素的位置到末尾的所有元素都需要复制/移动,并且只需要销毁最后一个元素。
【解决方案2】:

std::remove 只返回一个新的end() 迭代器,指向最后一个未移除元素之后的一个(从返回值到end() 的项目数将与要移除的项目数匹配,但是有不能保证它们的值与您删除的值相同 - 它们处于有效但未指定的状态)。这样做是为了使其适用于多种容器类型(基本上是 ForwardIterator 可以迭代的任何容器类型)。

std::vector::erase实际上是在调整大小后设置新的end()迭代器。这是因为vector 的方法实际上知道如何调整它的迭代器(std::list::erase、std::deque::erase 等也可以这样做)。

remove 组织给定容器以删除不需要的对象。容器的擦除功能实际上以容器需要的方式处理“删除”。这就是为什么它们是分开的。

【讨论】:

  • “在将所有已删除的项目移动到容器末尾之后” -- std::remove 不会这样做。最后的元素,从返回的位置开始,处于未指定状态。
  • 由于remove基本上是将要删除的元素与不会删除的元素交换,所以返回到end()的位置都是将要删除的元素(并且仍在有效状态),但你是对的 - 不能保证它们“匹配”被换出的元素。我会澄清一点。
  • @ZacHowland:现在最大的不同是在 C++11 中,std::remove 允许使用移动赋值,这将导致范围 [result, end()) 只是移动- 从元素。这些元素的状态取决于相关类。
  • @ZacHowland,最自然的 remove 实现不会交换任何东西,它会覆盖。 remove 实质上是将所有未删除的项移到向量的头部,覆盖已删除的项。
  • @AdamBurry 我将“交换”这个词与“替换”互换使用,但是是的,这是正确的。
【解决方案3】:

标准库算法对序列进行操作。序列由一对迭代器定义;第一个指向序列中的第一个元素,第二个指向序列的末尾。就这样;算法不关心序列来自哪里。

标准库容器保存数据值,并提供一对迭代器,指定算法使用的序列。它们还提供了成员函数,可以利用容器的内部数据结构更有效地执行与算法相同的操作。

【讨论】:

    【解决方案4】:

    有点。诸如 remove 之类的算法在迭代器上工作(这是表示集合中元素的抽象),它们不一定知道它们正在操作哪种类型的集合 - 因此不能调用集合上的成员来进行实际的删除。

    这很好,因为它允许算法在任何容器以及作为整个集合子集的范围上通用。

    另外,正如您所说,为了提高性能 - 如果您只需要访问逻辑结束位置以传递给另一个算法,则可能不需要实际删除(和销毁)元素。

    【讨论】:

      【解决方案5】:

      我认为这与需要直接访问矢量本身才能调整其大小有关。 std::remove 只能访问迭代器,所以它无法告诉向量“嘿,你现在有更少的元素”。

      请参阅 yves Baumes 关于 std::remove 为何采用这种方式设计的回答。

      【讨论】:

        【解决方案6】:

        std::remove 是一种来自 STL 的算法,它与容器完全无关。它需要一些概念,没错,但它被设计为也适用于 C 数组,它们的大小是静态的。

        【讨论】:

          【解决方案7】:

          是的,这就是它的要点。请注意,erase 也受其他性能特征不同的标准容器支持(例如,list::erase 为 O(1)),而 std::remove 与容器无关,适用于任何类型的 forward iterator(因此它也适用于裸阵列)。

          【讨论】:

            猜你喜欢
            • 2018-02-14
            • 1970-01-01
            • 2021-01-02
            • 2020-12-29
            • 1970-01-01
            • 2014-03-04
            • 2013-01-18
            • 2012-04-25
            • 2015-07-21
            相关资源
            最近更新 更多