【问题标题】:std::unordered_set::erase complexitystd::unordered_set::erase 复杂度
【发布时间】:2025-12-20 00:20:12
【问题描述】:

我不明白,为什么 erase 方法的 std::unordered_set 在最坏的情况下(其中 N 是元素数)复杂度为 O(N)?标准 (n4296) 说明所有三个版本的 erase 方法在最坏的情况下都具有 O(a.size()) 复杂性(a 是容器),并且仅使迭代器无效指向已擦除的元素,但不是所有的迭代器(即不会发生重新散列)。即使对于采用一个迭代器参数并且在平均情况下具有恒定复杂性的版本也是如此。我认为这是因为 erase 版本返回一个迭代器到下一个元素,这需要在擦除元素之后找到第一个非空桶,它给出了 O(a.bucket_count()) 复杂性,但不是 O(a.size())!。元素的数量与桶的数量不成正比。例如:

#include <iostream>
#include <unordered_set>
using namespace std;

int main()
{
    std::unordered_set<int> aSet;
    aSet.insert ({125, 126});
    aSet.rehash (1000);
    cout << aSet.size() << endl;
    cout << aSet.bucket_count() << endl;
}

输出是

Size: 2
Bucket count: 1031

一个容器的大小只有2,bucket_count是1031。当erase方法被调用时,它会寻找下一个非空的bucket,这个bucket可以放在最后,即复杂度是O(a.bucket_count()),但不是 O(a.size())。标准给出 O(a.size()) 复杂度的原因是什么?

【问题讨论】:

  • 认为现有的答案误解了这个问题。一个好的实现不必在每个桶中查找要擦除的元素,但最坏的情况除外,即所有桶都已满。因此,复杂度小于O(a.bucket_count()),除非a.bucket_count() 等于或大于a.size()
  • O(bucket_count()) 部分不正确。见LWG 579

标签: c++ c++11 stl containers unordered


【解决方案1】:

即使对于带有一个迭代器参数的版本也是如此 在平均情况下具有恒定的复杂性。

无序关联容器具有前向迭代器 - 它们可以通过单链表实现。

擦除节点涉及将它之前的节点重新链接到它之后的节点。在基于单链表的实现中查找迭代器指向的节点之前的节点可能是最坏的情况O(N),因为您基本上必须遍历桶(它可以包含容器中的每个元素)完全冲突的情况)。

【讨论】:

    【解决方案2】:

    最明显的原因是退化散列函数可能对所有元素产生相同的值。结果,它们都将被分类到同一个桶中。尽管不太可能,但即使使用相当好的散列函数,同样的可能也会发生,尤其是在将值映射到存储桶之后。由于散列函数的质量没有合理的规范,因此该标准不能要求更好的时间复杂度。

    【讨论】:

    • 好吧,如果我们有一个基于双向链表的实现,我不明白为什么 erase(iterator) 的复杂性不能保持不变。
    【解决方案3】:

    标准给出 O(a.size()) 复杂度的原因是什么?

    std::unordered_set 是一个散列容器。如果提供的散列函数映射到插入容器中的每个元素的相同值,那么它们将被链接在一起(可能在链表中)。因此,在最坏的情况下,单个“列表”可能包含容器中的所有项目,并且与任何“查找”操作一样,erase 更糟糕的情况是元素数量呈线性关系。

    【讨论】: