在遍历集合时从集合中擦除的最有效方法

Most efficient way to erase from a set while iterating over it

本文关键字:集合 有效 擦除 方法 遍历      更新时间:2023-10-16

在迭代时从集合中擦除最有效的方法是什么?以下是我想到的两种方法,哪一种是最好的?有没有更好的办法?

void WaitForFiles(std::set<string> files) {
  while (files.size() > 0) {
    std::set<string> found_files;
    for (const auto& file : files) {
      if (Exists(file)) {
        found_files.insert(file);
      }
    }
    for (const auto& found_file : found_files) {
      files.erase(file);
    }
  }
}
使用set_difference:

void WaitForFiles(std::set<string> files) {
  while (files.size() > 0) {
    std::set<string> found_files;
    for (const auto& file : files) {
      if (Exists(file)) {
        found_files.insert(file);
      }
    }
    std::set<string> difference;
    std::set_difference(files.begin(), files.end(),
                        found_files.begin(), found_files.end(),
                        std::inserter(difference, difference.end()));
    files = difference;
  }
}

注意下面的崩溃:

void WaitForFiles(std::set<string> files) {
  while (files.size() > 0) {
    for (const auto& file : files) {  // <-- seg fault after first erase
      if (Exists(file)) {
        files.erase(file);
      }
    }
  }
}

为了确定效率,请记住,在我的例子中,文件可能需要30分钟才能存在,因此Exists函数将被调用多次,并且与循环迭代的次数相比,文件集不会经常更改。

在基于范围的for循环中从集合中擦除是未定义的行为(即使它看起来有效)。基于范围的for循环在内部使用迭代器,删除元素会使迭代器失效。

但是std::set::erase返回std::set中下一个元素的有效迭代器,因此您可以使用显式迭代器循环:

for(auto itr = files.cbegin(); itr != files.cend();) {
  if (exists(*itr)) {
    std::cout << "Found file: " << *itr << "n";
    itr = files.erase(itr);
  } else
    ++itr;
}

现场演示。

std::experimental::erase_if从v2的库基础TS:

std::experimental::erase_if(files, Exists);

如果Exists是重载的或者是函数模板,使用lambda:

std::experimental::erase_if(files, [](const auto& f) { return Exists(f); });

您的前两个示例看起来不是最优的,因为它们都涉及遍历集合两次。第三个示例是不稳定的,因为在使用std::set::erase更改集合使迭代器无效之后,您继续使用迭代器。

我认为下面的代码应该是相当有效的。然而,正如其他人提到的,用std::vector代替std::set<std::string> found_files可能是值得的。

#include <set>
#include <string>
#include <iostream>
/**
 * Return true for every other call
 */
bool Exists(std::string const&)
{
    static int i = 0;
    return ++i % 2;
}
std::set<std::string> find_existing_files(std::set<std::string>& files)
{
    std::set<std::string> found_files;
    for(auto file = files.begin(); file != files.end();)
    {
        if(!Exists(*file))
            ++file; // not erasing, keep going
        else
        {
            found_files.insert(*file);
            // replacing iterator with result of erase()
            // keeps the iterator valid
            file = files.erase(file);
        }
    }
    return found_files;
}
int main()
{
    std::set<std::string> files {"a", "b", "c", "d", "e"};
    for(auto const& file: find_existing_files(files))
        std::cout << "exists : " << file << 'n';
    for(auto const& file: files)
        std::cout << "missing: " << file << 'n';
}
输出:

exists : a
exists : c
exists : e
missing: b
missing: d