【问题标题】:How to efficiently delete elements from a vector given an another vector如何在给定另一个向量的情况下有效地从向量中删除元素
【发布时间】:2017-01-01 12:50:47
【问题描述】:

在给定另一个向量的情况下,从一个向量中删除元素的最佳方法是什么?

我想出了以下代码:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void remove_elements(vector<int>& vDestination, const vector<int>& vSource) 
{
    if(!vDestination.empty() && !vSource.empty())
    {
        for(auto i: vSource) {
            vDestination.erase(std::remove(vDestination.begin(), vDestination.end(), i), vDestination.end());
        }
    }
}

int main() 
{
    vector<int> v1={1,2,3};
    vector<int> v2={4,5,6};
    vector<int> v3={1,2,3,4,5,6,7,8,9};
    remove_elements(v3,v1);
    remove_elements(v3,v2);
    for(auto i:v3)
        cout << i << endl;
    return 0;
}

这里的输出将是:

7
8
9

【问题讨论】:

  • 假设向量足够大以证明甚至打扰优化是合理的,我会首先将vDestination 转换为std::list(或智能指针列表?)以避免从向量中删除昂贵的(因为它必须是连续的)并在最后返回std::vector
  • @slawekwin:我强烈怀疑这对于任何实际大小的矢量来说都会更快。列表需要额外的重定向并且不能有效地缓存。 (我知道元素删除对于列表来说是 O(1),对于向量来说是 O(N)。)
  • @Frank 我也不确定,但我相信测量它会是一个有趣的实验:)
  • 所有向量都排序了吗?
  • 如果首先创建unordered_set(来自vSource)有助于提高效率,可能值得一试。理论上应该将最坏情况从N * M 更改为N + M(假设NvDestination 大小和MvSource 大小-每个vDestination 元素查找一次,而不是最多M 查找+ 设置创建),但它取决于存储桶配置。另外,您没有提到重复项会发生什么 - 应该删除一次,还是全部删除?

标签: c++ algorithm c++11 vector


【解决方案1】:

我的版本如下,我只在向量vSource 中的所有元素都被std::remove 移动到末尾之后才应用erase,并跟踪指向向量vDestination 末尾的指针到不要白白地迭代它。

void remove_elements(vector<int>& vDestination, const vector<int>& vSource) 
{
    auto last = std::end(vDestination);
    std::for_each(std::begin(vSource), std::end(vSource), [&](const int & val) {
        last = std::remove(std::begin(vDestination), last, val);
    });
    vDestination.erase(last, std::end(vDestination));
}

查看大肠杆菌:http://coliru.stacked-crooked.com/a/6e86893babb6759c


更新

这里是模板版本,不用管容器类型:

template <class ContainerA, class ContainerB>
void remove_elements(ContainerA & vDestination, const ContainerB & vSource) 
{
    auto last = std::end(vDestination);
    std::for_each(std::begin(vSource), std::end(vSource), [&](typename ContainerB::const_reference val) {
        last = std::remove(std::begin(vDestination), last, val);
    });
    vDestination.erase(last, std::end(vDestination));
}

注意

此版本适用于没有任何约束的向量,如果您的向量已排序,您可以采取一些捷径并避免遍历向量以删除每个元素。

【讨论】:

  • this“STL-优雅”迷幻剂。
  • @ilotXXI 不确定此处是否需要“remove_if”。考虑“删除”对于所有具有相同值的元素来说都是一样的。
  • @dkg:感谢您的回答。您如何检查这是否更有效?
  • @Hayt,你是什么意思?它迭代不同的值,不一样。
  • 啊,你是对的。抱歉,我有点想念您切换了内部/外部“循环”。看看是否有性能变化会很有趣。不过,我目前无法访问编译器。我还可以看到“更大”的向量将源向量值放入 unordered_set 中,如果它们存在就进行查找以避免对源向量进行重复迭代。
【解决方案2】:

我认为最好你的意思是最快的工作。由于这是一个关于效率的问题,我进行了一个简单的基准测试来比较几种算法的效率。请注意,它们略有不同,因为问题有点未明确说明 - 出现的问题(以及作为基准的假设)是:

  • 是否保证vDestination 包含来自vSource 的所有元素? (假设:否)
  • vDestinationvSource 是否允许重复? (假设:是的,两者都有)
  • 结果向量中元素的顺序是否重要? (测试两种​​情况的算法)
  • 如果vDestination 中的每个元素与vSource 中的任何元素相等,是否应该删除它,或者只删除一对一? (假设:是的,两者都有)
  • vDestinationvSource 的大小是否有界限?其中一个总是更大还是很多更大? (测试了几个案例)
  • 在 cmets 中已经说明不需要对向量进行排序,但我已将这一点包括在内,因为它不是从问题中立即可见的(假设在任何一个向量中都没有排序)

如您所见,算法在某些方面会有所不同,因此,您可以猜到,最佳算法将取决于您的用例。比较算法包括:

  1. 原件(有问题提出)- 基线
  2. 在@dkg 回答中提出
  3. 在@Revolver_Ocelot 答案中提出 + 额外排序(算法要求)并为结果预留空间 向量
  4. 在@Jarod42 回答中提出
  5. 基于集合的算法(如下所示 - 主要优化 @Jarod42 算法)
  6. 计数算法(如下所示)

基于集合的算法:

std::unordered_set<int> elems(vSource.begin(), vSource.end());
auto i = destination.begin();
auto target = destination.end();
while(i <= target) {
    if(elems.count(*i) > 0) 
        std::swap(*i, *(--target));
    else
        i++;
}
destination.erase(target, destination.end());

计数算法:

std::unordered_map<int, int> counts;     
counts.max_load_factor(0.3);     
counts.reserve(destination.size());      

for(auto v: destination) {     
    counts[v]++;     
}     

for(auto v: source) {     
    counts[v]--;     
}     

auto i = destination.begin();     
for(auto k: counts) {     
    if(k.second < 1) continue;            
    i = std::fill_n(i, k.second, k.first);     
}     
destination.resize(std::distance(destination.begin(), i));     

基准测试程序是使用Celero 库执行的,如下:

  1. 生成n伪随机ints(n in set {10,100,1000,10000, 20000, 200000})并将它们放入vector
  2. 将这些整数的一部分 (m) 复制到第二个 vector(来自集合 {0.01, 0.1, 0.2, 0.4, 0.6, 0.8} 的分数,最少 1 个元素)
  3. 启动计时器
  4. 执行删除程序
  5. 停止计时器

只有算法 3、5 和 6 是在大于 10000 个元素的数据集上执行的,因为其余的算法需要很长时间才能让我舒适地测量(您可以自己做)。

长话短说:如果您的向量包含的元素少于 1000 个,请选择您喜欢的那个。如果它们更长 - 依赖于 vSource 的大小。如果它小于vDestination 的 50% - 选择基于集合的算法,如果更多 - 对它们进行排序并选择 @Revolver_Ocelot 的解决方案(它们并列在 60% 左右,对于 vSource 为 1%,基于集合的速度快 2 倍以上vDestination) 的大小。请不要依赖顺序或提供从一开始就排序的向量 - 排序必须保持不变的要求会大大减慢进程。对您的用例、编译器、标志和硬件进行基准测试。我已附上我的基准测试的链接,以防您想复制它们。

完整的结果(文件vector-benchmarks.csv)与基准测试代码(文件tests/benchmarks/vectorRemoval.cpphere一起在GitHub上提供。

请记住,这些是我在我的计算机、我的编译器等上获得的结果 - 在你的情况下,它们会有所不同(尤其是当涉及到一种算法优于另一种算法时)。

我在 Fedora 24 上使用 GCC 6.1.1 和 -O3,在 VirtualBox 之上。

【讨论】:

    【解决方案3】:

    如果你的向量总是排序的,你可以使用set_difference:

    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <iterator>
    
    void remove_elements(std::vector<int>& vDestination, const std::vector<int>& vSource) 
    {
        std::vector<int> result;
        std::set_difference(vDestination.begin(), vDestination.end(), vSource.begin(), vSource.end(), std::back_inserter(result));
        vDestination.swap(result);
    }
    
    int main() 
    {
        std::vector<int> v1={1,2,3};
        std::vector<int> v2={4,5,6};
        std::vector<int> v3={1,2,3,4,5,6,7,8,9};
        remove_elements(v3,v1);
        remove_elements(v3,v2);
        for(auto i:v3)
            std::cout << i << '\n';
    }
    

    如果不是因为要求,该输出范围不应与任何输入范围重叠,我们甚至可以避免额外的向量。您可能可以滚动自己的set_difference 版本,允许在以vDestination.begin() 开头的范围内输出,但它超出了此答案的范围。

    【讨论】:

      【解决方案4】:

      可以用 STL 写成:

      void remove_elements(vector<int>& vDestination, const vector<int>& vSource) 
      {
          const auto isInSource = [&](int e) {
              return std::find(vSource.begin(), vSource.end(), e) != vSource.end();
          };
          vDestination.erase(
              std::remove_if(vDestination.begin(), vDestination.end(), isInSource),
              vDestination.end());
      }
      

      如果vSource 已排序,您可以将std::find 替换为std::binary_search

      【讨论】:

      • 这个解决方案也在@dkg的回答的第一条评论中
      • @Hayt:确实,我没有点击链接。
      猜你喜欢
      • 2015-04-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-19
      • 1970-01-01
      相关资源
      最近更新 更多