在保留原始顺序的同时擦除/删除多个std :: vector元素的最有效方法?

时间:2010-11-06 21:33:47

标签: c++ algorithm stl performance std


我有一个std::vector<int>和第二个容器,它将迭代器或索引(没有键,我希望不断访问该元素)保存到此向量中以进行删除。 让我们假设我有一个1000个元素的向量,并想要删除其中的200个元素。在删除操作之后,未删除元素的顺序应该相同。

我在问题的第一个版本中错过了另一件事:值是唯一的。他们是身份。

你会如何在安全(关于stl规则)和有效方式中做到这一点(对于向量的决定应该是最终的)?

可能性方法我想到了:

  • erase-remove idiom (http://en.wikipedia.org/wiki/Erase-remove_idiom):最初用于删除满足条件的元素(包括线性搜索)但我考虑使用大小为1的范围,此方法可用于已经给定的迭代器和虚拟条件。 问题:保留的元素的原始顺序是否比上一种方法更高效?
  • 遍历索引并使用vector.erase(vector.begin()+index+offset)擦除元素,同时在容器中保留索引以计算偏移量。可以使用已删除元素的容器std::lower_bound n来确定每次删除迭代的偏移量。 问题:由于随机位置删除,很多binary_searches用于获取偏移量和大量移动操作。
  • 目前我正在执行以下操作:获取要删除的元素的所有迭代器。根据向量中的位置按降序对它们进行排序,然后循环它们以使用vector.erase进行最终删除。现在我没有使任何迭代器失效,除了删除本身之外没有向量重新排列操作。 问题:大量排序

那么,你会如何解决这个问题?有什么新想法吗?有什么建议吗?

感谢您的意见。

的Sascha

编辑/更新/拥有结果:我实施了擦除删除习惯用法,KennyTM也提到了谓词基于查找一个boost :: dynamic_bitset ,它的非常快。此外,我尝试了 PigBen的移动和截断方法(也由Steve Jessop提到),它也在它的while循环中访问bitset。对我的数据来说,两者似乎同样快。我试图删除100个1000个元素(无符号整数)中的100个,这100个删除了1M次并且没有显着差异。因为我认为基于stl的擦除删除成语更“自然”,我选择了这种方法(KennyTM也提到过这个论点)。

7 个答案:

答案 0 :(得分:13)

<algorithm>中,有一个remove_if function会将所有未删除的值压缩到维持订单的前端。如果这200个元素可以完全由值而不是索引确定,则可行。

这实际上是您链接到的Erase-remove习惯用法。 remove_if保证执行O(N)比较(并且最多是O(N)复制),这比排序(O(N log N))更有效,尽管您的最后一个选项实际上并不需要如果索引是根据值确定的,则进行排序(只需在复制时按相反方向扫描)。

尽管如此,使用remove_if(如果可以的话)优于其他2个选项,因为已经为您编写了实现,因此逻辑错误的可能性更小,并且更好地传达 what (不是如何)。

答案 1 :(得分:13)

如何循环遍历向量,并且对于需要删除的每个元素,将不需要删除的下一个元素复制到该位置。然后当你走到最后,截断它。

int last = 0;
for(int i=0; i<vec.size(); ++i, ++last)
{
   while(needs_to_be_removed(i))
      ++i;
   if(i >= vec.size()) break;

   vec[last] = vec[i];   
}

vec.resize(last);

答案 2 :(得分:4)

首先,不要再调用erase次,因为对于一个向量,它会将所有后面的元素向下混洗,从而使整个操作成为Ω(n * m)最坏情况下的运行时间(n为向量的大小,m为要删除的索引列表的大小)。

我认为我尝试的第一件事与您当前的代码类似:

  • 对索引进行排序
  • 创建一个大小为n - m
  • 的新向量
  • 遍历原始向量,复制indexes[0]元素,跳过元素,然后复制indexes[1] - indexes[0] - 1元素,跳过元素等等。
  • swap原始载体与新载体。

您可以使用remove_copy_if执行第三步,并使用包含状态的谓词(计算已复制的项目数以及通过排序的索引列表的距离),但< / em>由于非常繁琐和模糊的原因,这不能保证工作(具有可变状态的算法谓词是有问题的,似乎是标准不能保证相同副本的共识整个算法使用谓词)。所以我真的不建议尝试它,但它可能有助于记住你所写的内容基本上是remove_copy_if的修改版本。

你可以避免使用back_inserter而不是预先设定矢量的第二步,尽管你可能仍然提前预留空间。

[编辑:来想一想,我为什么要复制任何东西?不是实现修改后的remove_copy_if,而是实现修改后的remove_if,而只是复制到向量中的早期点。最后是erase / resize。在证明是一个问题之前我不会担心O(m log m)种索引,因为读取要删除的所有值并将它们存储在Ω(m)操作中的速度要慢得多。某种容器。然后,在remove_if的谓词中使用此容器可能是也可能不是O(1)。对于合理的m值,排序可能会更快。]

答案 3 :(得分:2)

您可以将矢量的所有元素复制到列表中,除非您的第二个容器中包含索引,然后返回到矢量。即使你的算法从向量的末尾走到前面,你的向量中的幕后工作也会有很多工作。

将第二个容器设为地图,以便自动为您排序。

编辑:

回复评论

维护地图的成本与维护另一个结构(列表或向量)然后对其进行排序相同。如果你已经这样做了,你可以将它保存为地图。抱怨地图的开销与排序列表的开销没有意义。

至于我建议算法的性能,如果m是要删除的元素数,n是元素总数,则得到O(n - m)。

当然,这主要只是在幽默你用矢量进行优化的尝试。

1 - 如果要进行随机访问删除,则不应使用向量。这不是他们擅长的,如果可能的话,使用一个列表。而且由于你似乎对相对顺序而不是绝对索引更感兴趣,我想知道为什么需要一个向量。如果您解决了整个问题,可能会有一个通用的解决方案让您使用最有效的数据结构来解决它。

2 - 不要维护第二个数据结构,而是在容器中标记需要直接删除的元素。一个简单的方法是使用容器&lt; T>使用容器&lt;的std ::对&LT; T,char&gt; &GT;并使用char来跟踪元素状态。

如果您执行1和2,则会完全删除所有复制并获得更高效的实现。

答案 4 :(得分:1)

什么元素?也许我正在认真对待你的帖子,但如果你有一个1000元素的向量,为什么不标记那些无效的那些并且首先取消擦除。显然我在这里假设你的元素并不需要大量的内存。

我只是提起这件事因为你似乎关心速度。如果已经给出的建议没有做到这一点,那么这个想法值得一提!实质上是通过不首先进行操作来加快速度。

答案 5 :(得分:1)

如果您要删除一组(例如无序)索引,可以使用:

template <typename Type>
void erase_indices(
        const std::unordered_set<size_t>& indices_to_erase,
        std::vector<Type>& vec) {
    std::vector<bool> erase_index(vec.size(), false);
    for (const size_t i: indices_to_erase) {
        erase_index[i] = true;
    }
    std::vector<bool>::const_iterator it_to_erase = erase_index.cbegin();
    typename std::vector<Type>::iterator it_erase_from = std::remove_if(
        vec.begin(), vec.end(),
        [&it_to_erase](const Type&) -> bool {
          return *it_to_erase++ == true;
        }
    );
    vec.erase(it_erase_from, vec.end());
}

这是我想到的最快的解决方案。但是,您需要 C ++ 11 。擦除索引2和5处元素的用法示例:

constexpr size_t num = 10u;
std::vector<int> vec(num);
std::iota(vec.begin(), vec.end(), 0);

std::unordered_set<size_t> indices_to_erase;
indices_to_erase.insert(2u);
indices_to_erase.insert(5u);

erase_indices(indices_to_erase, vec);

在:

0 1 2 3 4 5 6 7 8 9

后:

0 1 3 4 6 7 8 9

修改 如果想要更容易保持索引要删除的容器类型:

template <typename Type, typename Container>
void erase_indices(
        const Container& indices_to_erase,
        std::vector<Type>& vec) {
    typedef typename Container::value_type IndexType;
    static_assert(std::is_same<IndexType, std::size_t>::value,
        "Indices to be erased have to be of type std::size_t");
    std::vector<bool> erase_index(vec.size(), false);
    for (const IndexType idx_erase: indices_to_erase) {
        erase_index[idx_erase] = true;
    }
    std::vector<bool>::const_iterator it_to_erase = erase_index.cbegin();
    typename std::vector<Type>::iterator it_erase_from = std::remove_if(
        vec.begin(), vec.end(),
        [&it_to_erase](const Type&) -> bool {
          return *it_to_erase++ == true;
        }
    );
    vec.erase(it_erase_from, vec.end());
}

现在,只要该容器的value_typestd::size_t,您就可以使用Containers Library中的任何类型的容器来提供要删除的索引。用法保持不变。

答案 6 :(得分:-1)

我写了一个函数,基于Benjamin Lindley回答https://stackoverflow.com/a/4115582/2835054

#include <iostream>
#include <algorithm>
#include <vector>

template <typename elementType, typename indexType>
void remove_multiple_elements_from_vector(std::vector<elementType> &vector,
std::vector<indexType> &indexes)
{
    // 1. indexType is any integer.
    // 2. elementType is any type.
    // 3. Indexes should be unique.
    // 4. The largest index inside indexes shouldn't be larger than
    //    the largetst index in the vector.
    // 5. Indexes should be sorted in ascending order
    //    (it is done inside function).
    std::sort(indexes.begin(), indexes.end());
    indexType currentIndexInIndexesVector = 0;
    indexType last = 0;
    for(indexType i=0; i<vector.size(); ++i, ++last)
    {
       while(indexes[currentIndexInIndexesVector] == i)
       {
          ++i;
          ++currentIndexInIndexesVector;
       }
       if(i >= vector.size()) break;

       vector[last] = vector[i];   
    }

    vector.resize(last);
}


int main()
{
    std::vector<int> vector = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::vector<int> indexes = {0, 10, 5};

    for (auto &vectorElement : vector)
    {
        std::cout << vectorElement << " ";
    }    
    std::cout << "\n";

    remove_multiple_elements_from_vector<int, int>(vector, indexes);

    for (auto &vectorElement : vector)
    {
        std::cout << vectorElement << " ";
    }
}