【问题标题】:Most efficient way of erasing/deleting multiple std::vector elements while retaining original order?在保留原始顺序的同时擦除/删除多个 std::vector 元素的最有效方法?
【发布时间】:2011-05-06 03:25:31
【问题描述】:


我有一个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 进行最终删除。现在我没有使任何迭代器失效,并且除了删除本身之外没有向量重新排列操作。 问题:很多排序

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

感谢您的意见。

萨沙

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

【问题讨论】:

  • 有点吹毛求疵,但是当您在第一句话中说“没有键”时,您似乎在谈论价值观。数组(IMO)的“键”是索引。你在说什么。

标签: c++ algorithm stl performance std


【解决方案1】:

<algorithm> 中有一个remove_if function,它将所有未删除的值挤压到前面以保持顺序。如果这 200 个元素可以完全由值而不是索引确定,则此方法有效。

这本质上是您链接到的擦除删除习语。 remove_if 保证执行 O(N) 比较(并且最多 O(N) 复制),这将比排序 (O(N log N)) 更有效,尽管您的最后一个选项实际上不需要排序索引由值确定(复制时只需反向扫描)。

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

【讨论】:

  • 好的,谢谢您的回答。现在我唯一的问题是我没有用于 remove_if 函数的“快速”谓词,而是“已经决定”的索引/迭代器。似乎我必须将我的决定转换为一个位集,它告诉我每个元素是否应该删除以使用在恒定时间内运行的谓词。 O(n) 更多的工作。但渐近地它应该更好。也许我应该重写我的“决策算法”来给出一个比特集。再次感谢您的回答,特别是提到保留了元素的原始顺序。
【解决方案2】:

您可以将向量的所有元素复制到列表中,除非您的第二个容器中的索引,然后再复制回向量。即使使用从向量末端到前端的算法,向量的幕后也有很多工作要做。

使您的第二个容器成为地图,以便自动为您排序索引。

编辑:

回复评论

维护地图的成本在最坏的情况下与维护另一个结构(列表或向量)然后对其进行排序相同。如果您已经这样做了,您不妨将其保留为地图。抱怨地图的开销与排序列表的开销是没有意义的。

至于我建议的算法的性能,如果 m 是要删除的元素的数量,n 是元素的总数,那么它的结果是 O(n - m)。

当然,这主要是在嘲笑您使用向量进行优化的尝试。

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

2 - 无需维护第二个数据结构,而是直接在其容器中标记需要删除的元素。一个简单的方法是使用容器 使用容器> 并使用 char 来跟踪元素状态。

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

【讨论】:

  • 对我来说似乎效率不高,但我可能是错的。您提到的这项工作与 KennyTM 在他的回答中给出的渐近保证相比如何?如果您正在谈论调整大小操作,那么我会说在最终删除之前没有操作(因为删除只是交换)。而且地图总是会带来巨大的开销(个人意见,不喜欢平衡搜索树)。
  • 还是不信。我不再需要排序步骤(使用擦除删除习语),因此持有地图的渐近论点消失了。对于“不要使用向量”-> 我写道,这个决定是最终决定。我知道向量的优缺点(至少我是这么认为的)。使用向量的原因:原始迭代性能和缓存效率(当然这对列表和映射不利)。我做的迭代比删除要多得多。我的问题是:您是否认为您的解决方案比我有一个恒定谓词的擦除删除成语更快?
【解决方案3】:

第一件事是,不要调用erase 的次数超过你必须的次数,因为对于一个向量,它会将所有后面的元素打乱,给整个操作一个 Ω(n*m) 最坏情况下的运行时间(n向量的大小,m 要删除的索引列表的大小)。

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

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

您也许可以使用remove_copy_if 和一个包含状态的谓词(计算它复制了多少项目以及它在排序的索引列表中的距离)来完成第三步,但是 由于极其繁琐和晦涩的原因,这不能保证有效(具有可变状态的算法谓词是有问题的,标准不保证谓词的相同副本似乎是共识在整个算法中使用)。所以我真的不建议尝试它,但请记住,您所写的基本上是 remove_copy_if 的修改版本。

您可以使用back_inserter 来避免第二步,而不是调整向量的大小,尽管您可能仍会提前保留空间。

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

【讨论】:

  • 您编辑的解决方案听起来有点像 PigBen 提出的解决方案,还是我误解了?所以不再涉及排序吗?
  • @sascha:是的,非常相似。我没有看到PigBen的回答。不同的是,他有一个函数needs_to_be_removed,我算在内。另外,我更华夫饼。
【解决方案4】:

如何循环遍历向量,对于每个需要删除的元素,将不需要删除的下一个元素复制到该位置。然后当你到达末尾时,将其截断。

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);

【讨论】:

  • 听起来很简单。我会试试看。谢谢你的回答。
  • 这相当于vec.erase(std::remove_if(vec.begin(), vec.end(), needs_to_be_removed), vec.end());
  • @robinjam:不,不是。 remove_if 根据元素的值删除。此解决方案根据元素在向量中的位置进行删除。
  • @BenjaminLindley:没错。抱歉,我误读了您的代码 sn-p 中的一行。
  • while(needs_to_be_removed(i)) 循环可以在这里超出范围 [0,vec.size()) 吗?如果整个向量被标记为删除,i 将采用vec.size() 的值,needs_to_be_removed 必须处理。
【解决方案5】:

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

我之所以提出这个只是因为您似乎关心速度。如果已经给出的建议不能解决问题,那么这个想法可能值得考虑!从本质上讲,首先不进行操作可以加快速度。

【讨论】:

  • 就像假设的一样,并且在开头提到,元素是小的整数类型(int)。我认为其他人的建议起到了作用,我现在很满意,但尽管如此,我喜欢你的解决方案。唯一的问题是,如果在遍历这些元素时,缓存效率仍然很高(因为我们现在有更多的元素。)但对于某些应用程序来说,这似乎是一个很好的解决方案。
【解决方案6】:

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

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());
}

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

【讨论】:

    【解决方案7】:

    我已经根据 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 << " ";
        }
    }
    

    【讨论】:

    • 我喜欢这个实现,除了它通过排序改变了indexes 变量。那不是我们正在操作的变量,因此不应更改它。最好按值传递并排序我们的本地副本
    • 你是对的。但是向量可能很大。带索引的向量几乎可以和带值的向量一样大。所以我决定通过引用传递带有索引的向量。
    猜你喜欢
    • 1970-01-01
    • 2016-03-31
    • 1970-01-01
    • 2021-07-17
    • 1970-01-01
    • 2011-09-30
    • 2022-10-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多