【问题标题】:Erase some of a vector's elements in a for-each loop without iterating the whole vector在 for-each 循环中擦除向量的一些元素,而不迭代整个向量
【发布时间】:2018-10-09 07:19:48
【问题描述】:

我有一个向量,我正在其中搜索一个元素,同时使用 for-each 循环遍历该向量。如果在搜索过程中发现任何无效元素,我想将它们从向量中删除。

基本上,我想做这样的事情:

for (auto el : vec) {
    if (el == whatImLookingFor) {
        return el;
    } else if (isInvalid(el)) {
        vec.erase(el);
    }
}

我查看了一些其他问题,例如 thisthis,但都建议使用 std::remove_if。这将遍历整个向量并删除所有无效元素,而不是仅在找到我要查找的元素之前进行迭代,然后忽略之后的任何元素。

什么是这样做的好方法?

【问题讨论】:

  • 你不需要迭代器作为std::vector<T>::erase() 的参数吗???
  • 您链接到的 first question 在顶部有一个代码 sn-p 这基本上是您想要的 - 只需插入检查元素是否是您想要的元素,如果是 @987654328 @.
  • 如果要修改基础范围,Range-for 通常不是正确的工具。
  • 找不到元素返回什么?
  • 另见std::vector iterator invalidation。它可能是重复的,因为它处理由erase 引起的无效迭代器。

标签: c++ c++11 iterator


【解决方案1】:

我很好奇它的性能,所以我跑了一个quick naive benchmark 比较Benjamin's find then partial cleanhnefatl's for-loop。 Benjamin 的确实更快:快 113 倍。感人的。

但我很好奇大部分计算的去向,因为它大于 remove_iffind 的总和,这两个函数是实际遍历数组的唯一函数。然而,事实证明,他的代码中的vec.erase 行实际上非常慢。这是因为在remove_if 中,他正在清理从开始到找到值的位置的区域,这导致数组中间与无效值之间存在间隙。然后vec.erase 必须复制剩余的值以填补空白并最终调整数组的大小。

我用全尺寸的remove_if/vec.erasefind 进行了第三次测试,所以最后出现了间隙,不需要复制,只是截断。结果证明它快了大约 15%,并且使整个向量保持干净。请注意,这假设测试有效性是便宜的。正如克里斯在 cmets 中指出的那样,如果不仅仅是几个简单的比较,本杰明的答案将毫无疑问地获胜。

代码

auto p = std::remove_if(vec.begin(), vec.end(), isInvalid);
vec.erase(p, vec.end());
return std::find(vec.begin(), vec.end(), whatImLookingFor);

The Benchmark

【讨论】:

  • 有趣的在线基准测试工具(有点像Godbolt.org,但有计时而不是 asm 输出),我不知道。看起来您实际上使用clang -O3 进行了基准测试(好),但是基准链接提出的优化被禁用(ridiculous and useless,特别是对于将整个实现作为调用其他小函数的小函数的 C++ 模板函数)。不过,使用 -O3 的比率实际上比使用 -O0 时更大
  • @JMerdich 好答案。在现实情况下,检查元素是否无效可能会在计算上更昂贵,这可能超过更快的erase
  • 这两种算法都需要将所有剩余元素向下移动,然后擦除最后的空白空间。不同之处在于,部分清理必须移动搜索键之后的所有元素,而完全清理只移动有效元素。假设密钥之后的所有元素都是有效的,并且两种算法都产生了同样多的数据? quick-bench.com/SZpfwb2x0uNnI3N2grysEvTms6Y
  • @bipll 我不确定你从哪里得到 O(n^2),因为我的代码进行了 3 次调用,它们都是 O(n) 或更好(remove_if 重新洗牌正如它检查的那样,但它仍然是线性的)。克里斯提出了一个很好的观点,我认为isInvalid 很便宜。如果不是这样,本杰明的代码会更快。
  • @bipll:嗯,是吗?我在答案中多次表示。但是,由于任何解决方案都必须遍历向量以填补空白,因此如果清理成本低,您还不如清理它。我只是说 OP 应该考虑它以及他的要求。
【解决方案2】:

您仍应使用std::remove_if,只需提前致电std::find

auto el = std::find(vec.begin(), vec.end(), whatImLookingFor);
auto p = std::remove_if(vec.begin(), el, isInvalid);

// returns the iterator, not the element itself.
// if the element is not found, el will be vec.end()
return vec.erase(p, el);

这通常比一次删除一个元素更有效。

【讨论】:

  • 第一行的v是从哪里来的?
【解决方案3】:

正如@BenjaminLindley 和@JMerdich 所指出的,对于所述问题,两遍方法可能更简单、更有效。

在实际情况下,可能需要进行一些昂贵的计算来确定元素是否无效或确定元素是否是我们正在寻找的元素:

在这种情况下,两遍方法变得不太理想,因为它会导致我们进行两次昂贵的计算。

但可以在循环内多次调用 std::vector::erase 的情况下执行单遍方法。自己写std::remove_if 并不难,那么我们可以让它同时做这两项检查。最后我们仍然只调用一次std::vector::erase

std::vector<T>::iterator 
findAndRemoveInvalid(std::vector<T>& vec, U whatImLookingFor) {

  // Find first invalid element - or element you are looking for
  auto first = vec.begin();
  for(;first != vec.end(); ++first) {
    auto result = someExpensiveCalculation(*first);
    if (result == whatImLookingFor)
      return first;  
    if (isInvalid(result))
      break;
  }
  if (first == vec.end())
    return first;

  // Find subsequent valid elements - or element you are looking for
  auto it = first + 1;
  for(;it != vec.end(); it++) {
    auto result = someExpensiveCalculation(*it);
    if (result == whatImLookingFor)
      break;
    if (!isInvalid(result)) {
      *first++ = std::move(*it);  // shift valid elements to the start
      continue;
    }
  }

  // erase defunct elements and return iterator to the element
  // you are looking for, or vec.end() if not found.
  return vec.erase(first, it);
}

Live demo.

这显然更复杂,所以首先衡量性能。

【讨论】:

    【解决方案4】:

    这是一种保持循环结构的直观方法——虽然它只执行单遍,但由于repeated erasing it's likely to be less efficient 而不是两遍方法。对于这种更有效的方法,you should use Benjamin Lindley's answer


    修改您提供的answer to the first link 中的代码,看起来像这样符合您的规范:

    for (auto i = vec.begin(); i != vec.end();)
    {
        if (*i == whatImLookingFor)
            return i;
        else if (isInvalid(*i))
            i = vec.erase(i);       // slow, don't use this version for real
        else
            ++i;
    }
    
    • 如果我们找到您要搜索的元素,我们会返回一个迭代器。
    • 如果我们沿途发现无效元素(但不是在所需元素之后),我们会将其删除。
    • 我们通过保留从erase 返回的迭代器来避免迭代器失效。

    您仍然需要处理未找到元素的情况,可能通过返回vec.end()

    【讨论】:

    • 在一个循环中多次调用std::vector::erase 通常效率很低。每次它都会导致被擦除项目之后的所有元素都移动一个元素。您应该更喜欢 Benjamin Lindley 的回答。
    • @hnefatl:我的回答也忽略了被搜索的元素之后的元素。
    • @BenjaminLindley 哎呀,我错过了你第二行中偷偷摸摸的el。很好的回答,我 +1!
    【解决方案5】:

    如果使用break 退出的简单循环对您来说过于原始,我建议使用std::find() 获取搜索元素的迭代器,然后使用vector.erase() 删除其他元素。

    【讨论】:

    • 我认为 OP 问题的重点是他们希望一次执行两个操作 - 擦除无效元素,然后返回与谓词匹配的元素。这个答案似乎只是关于删除与谓词匹配的元素。
    • 修复了,方法基本相同,只是交换了擦除的范围。
    猜你喜欢
    • 1970-01-01
    • 2014-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-29
    • 2021-01-02
    • 2020-06-01
    相关资源
    最近更新 更多