在成对的 `std::vector` 上使用 `std::count` 的意外行为

问题描述

我的目标是完全删除 std::vector<std::pair<int,int>> 中出现多次的所有元素。

这个想法是使用 std::removestd::count 作为谓词的一部分。我的方法看起来像这样:

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

using std::cout;
using std::endl;
using i_pair = std::pair<int,int>;

int main()
{
    std::vector<i_pair> vec;
    vec.push_back(i_pair(0,0)); // Expected to stay
    vec.push_back(i_pair(0,1)); // Expected to go
    vec.push_back(i_pair(1,1)); // Expected to stay
    vec.push_back(i_pair(0,1)); // Expected to go

    auto predicate = [&](i_pair& p)
    {
        return std::count(vec.begin(),vec.end(),p) > 1;
    };
    auto it = std::remove_if(vec.begin(),predicate);

    cout << "Reordered vector:" << endl;
    for(auto& e : vec)
    {
        cout << e.first << " " << e.second << endl;;
    }
    cout << endl;
    
    cout << "Number of elements that would be erased: " << (vec.end() - it) << endl;

    return 0;
}

数组被重新排序,两个 (0,1) 元素都被推到最后,但是 std::remove 返回的迭代器指向最后一个元素。这意味着后续的 erase 操作只会删除一个 (0,1) 元素。

为什么会发生这种行为,我如何删除出现多次的所有元素?

解决方法

您最大的问题是 std::remove_if 对运行时向量的内容几乎没有保证。

它在最后保证,返回迭代器的 begin() 包含未删除的元素,并且从那里直到 end() 还有一些其他元素。

与此同时,您正在此操作的中间迭代容器。

std::partition 更有可能起作用,因为它保证(完成后)您“删除”的元素实际上存储在最后。

更安全的方法是进行 std::unordered_map<std::pair<int,int>,std::size_t> 并在一次传递中计数,然后在第二次传递中删除计数至少为 2 的所有内容。这也是 O(n) 而不是您的算法 O( n^2) 所以应该更快。

std::unordered_map<i_pair,std::size_t,pair_hasher> counts;
counts.reserve(vec.size()); // no more than this
for (auto&& elem:vec) {
  ++counts[elem];
}
vec.erase(std::remove_if(begin(vec),end(vec),[&](auto&&elem){return counts[elem]>1;}),end(vec));

您必须编写自己的 pair_hasher。如果你愿意接受nlgn的表现,你可以做

std::map<i_pair,std::size_t> counts;
for (auto&& elem:vec) {
  ++counts[elem];
}
vec.erase(std::remove_if(begin(vec),end(vec));