【问题标题】:Complexity of std::unordered_set iterator traversalstd::unordered_set 迭代器遍历的复杂性
【发布时间】:2017-09-09 06:42:48
【问题描述】:

我最近玩了一个std::unordered_set。我怀疑我的 STL 版本会跟踪某些 FILO 数据结构中的非空存储桶(看起来像一个列表)。我想这样做是为了提供完整的std::unordered_setO(n) 时间遍历(其中n 表示unordered_set 中的元素数,m 桶和mn 大得多)。这改进了在O(m) 时间内对所有桶的简单遍历。

我已经测试过,确实遍历大型且非常稀疏的unordered_sets(使用begin - end)比简单地遍历所有存储桶要快得多。

问题:这个遍历运行时是否有标准保证?或者这只是我的特定标准库的一个特性?


这是我的测试代码:

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

void test(vector<int> data, int alloc_size) {
   unordered_set<int> set(alloc_size);
   for (auto i: data) {
      set.insert(i);
   }

   for (size_t bidx = 0; bidx < set.bucket_count(); ++bidx) {
      cout << "[B" << bidx << ":";
      for (auto bit = set.begin(bidx); bit != set.end(bidx); ++bit) {
         cout << " " << *bit;
      }
      cout << "] ";
   }

   cout << "  {";
   for (auto const & d: set) {
      cout << d << " ";
   }
   cout << "}" << endl;
}

int main() {
   test({1, 2, 0}, 3);
   test({1, 2, 0, 7}, 3);
   test({18, 6, 11, 3, 13, 4}, 20);
   test({18, 6, 11, 3, 13, 4, 34}, 20);
}

哪些打印:

[B0: 0] [B1: 1] [B2: 2] [B3:] [B4:]   {0 2 1 }
[B0: 0] [B1: 1] [B2: 7 2] [B3:] [B4:]   {0 7 2 1 }
[B0:] [B1:] [B2:] [B3: 3] [B4: 4] [B5:] [B6: 6] [B7:] [B8:] [B9:] [B10:] [B11: 11] [B12:] [B13: 13] [B14:] [B15:] [B16:] [B17:] [B18: 18] [B19:] [B20:] [B21:] [B22:]   {4 13 3 11 6 18 }
[B0:] [B1:] [B2:] [B3: 3] [B4: 4] [B5:] [B6: 6] [B7:] [B8:] [B9:] [B10:] [B11: 34 11] [B12:] [B13: 13] [B14:] [B15:] [B16:] [B17:] [B18: 18] [B19:] [B20:] [B21:] [B22:]   {4 13 3 34 11 6 18 }

似乎begin - end 遍历以相反的顺序报告存储桶,它们变为非空(参见第一行和第三行)。插入已经非空的存储桶不会更改此顺序(参见第二行和第四行)。

【问题讨论】:

    标签: c++ c++11 stl c++-standard-library


    【解决方案1】:

    简而言之:是的,这是由标准保证的。

    说明

    所有迭代器都必须具有O(n) 遍历时间复杂度(其中n 是遍历的项目数量)。这是因为迭代器上的每个操作都具有恒定的时间复杂度 (O(1)),包括将迭代器推进一个位置。

    来自标准(第 24.2.1 节第 8 节):

    迭代器的所有类别只需要在恒定时间内(摊销)对给定类别可实现的那些函数。因此,迭代器的需求表没有复杂度列。

    因此,当迭代 std::unordered_set 的项目时,时间复杂度为 O(n)(其中 n 是集合中项目的数量)。

    不相信?

    以上引用的字面意思只能保证恒定时间操作是可实现的。这并不能阻止特定实现的时间复杂度比可实现 更差。这可能是因为词的选择不当,希望没有真正的实现真正做到这一点。

    标准中唯一可以帮助解决这种歧义的地方是第 24.4.4 §1 节,其中标准对std::advancestd::distance 有这样的说法:

    这些函数模板使用+- 进行随机访问迭代器(因此它们的时间是常数);对于输入、前向和双向迭代器,他们使用++ 来提供线性时间 实现。

    因此,前向迭代器(用于std::unordered_set)上的++ 操作暗示为常数时间操作。

    总而言之,虽然第一个引号的措辞模棱两可,但第二个引号确认了意图。

    【讨论】:

    • 谢谢。但是,这不只是将迭代器类别(即前向迭代器)所需的接口限制为可以在O(1) 中实现的那些功能吗?特别是,这并不要求实现确实是O(1)?
    • @m8mble :这是一个公平的观察,您确实可以这样阅读。我试图在标准中找到更好的报价。与此同时,该标准对std::advance 的实现这么说:“对于输入、前向和双向迭代器,它们使用++ 来提供线性时间实现”。 std::advance 如果迭代器不以线性时间迭代,则不能以线性时间实现。
    • 再次感谢。至少,附录的意图似乎很清晰。有没有地方可以报告这个看似不足的地方?
    • 所有像++it这样的stdlib迭代器操作是否都采用常数O1(不是摊销常数),不管它是什么类型的迭代器?
    猜你喜欢
    • 1970-01-01
    • 2016-04-06
    • 2020-11-23
    • 2015-10-04
    • 2017-11-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-03
    相关资源
    最近更新 更多