【问题标题】:Insert a sorted range into std::set with hint使用提示将排序范围插入 std::set
【发布时间】:2018-09-22 18:52:53
【问题描述】:

假设我有一个std::set(根据定义已排序),并且我有另一个已排序 元素范围(为简单起见,在不同的std::set 对象中)。另外,我保证第二组中的所有值都大于第一组中的所有值。

我知道我可以有效地将一个元素插入std::set - 如果我传递一个正确的hint,这将是O(1)。我知道我可以将任何范围插入std::set,但由于没有传递hint,这将是 O(k logN)(其中 k 是新元素的数量,N 是旧元素的数量元素)。

我可以在std::set 中插入一个范围并提供一个hint 吗? 我能想到的唯一方法是使用hint 进行k 次插入,这确实推动了在我的例子中,插入操作的复杂性降至 O(k)

std::set <int> bigSet{1,2,5,7,10,15,18};
std::set <int> biggerSet{50,60,70};  

for(auto bigElem : biggerSet)
    bigSet.insert(bigSet.end(), bigElem);

【问题讨论】:

  • 为什么 k 暗示插入复杂度 O(1) 会导致 O(kN) 复杂度操作?不是 O(k) -pun 不是故意的吗?
  • @papagaga 哦,好吧,那是因为 k 乘以 1 等于 kN 以获得足够大的 1 值:) 抱歉,错字,我把它编辑掉了。
  • 我想知道std::set 是不是您要找的东西。使用它来保持排序的集合不是一个好主意。当您需要依赖插入不会使迭代器和引用无效的属性时,您可以使用std::set。为了保持排序的集合,std::vector&lt;algorithm&gt; 通常是最好的选择。而你所要求的就是两个vectors
  • @papagaga 我实际上正在使用std::maps,它在某种程度上代表了非常稀疏的直方图。我计算了其中几个直方图(大约 10 个,而不是数千个)。计算很复杂,我需要添加和更新很多很多元素,所以我需要查找/插入 O(logN) 并且 std::map 是正确的数据结构。每次计算后,我需要通过添加当前直方图的信息来更新“全局直方图”,这就是我的问题所指的。为了简单起见,我用std::set 提出了这个问题。
  • @papagaga 目前我的构造是 O(NlogN)。使用向量,我需要在每次插入之前至少 std::find 检查重复项(80% 以上的插入是重复项;所以保持所有内容然后过滤唯一是可怕的记忆方式),并将其排序结束 - O(N^2 + NlogN)。每次我需要从稀疏直方图中读取一个值时,我还需要 std::find ,这又不是很理想。

标签: c++ c++11 stdset


【解决方案1】:

首先,要进行您所说的合并,您可能想使用set(或map's)merge 成员函数,这会让您将一些现有的map 合并到这个中。这样做的好处(以及您可能不想这样做的原因,取决于您的使用模式)是被合并的项目实际上从一组移动到另一组,因此您不必分配新节点(这可以节省大量时间)。缺点是节点随后会从源集中消失,因此如果您需要每个局部直方图在合并到全局直方图后保持完整,您不想这样做。

在搜索排序向量时,通常比 O(log N) 做得更好。假设分布合理可预测,您可以使用插值搜索(通常)在 O(log log N) 附近进行搜索,通常称为“伪常数”复杂度。

鉴于您只相对不频繁地进行插入,您也可以考虑使用混合结构。这从一小部分您没有保持排序的数据开始。当您达到其大小的上限时,您对其进行排序并将其插入到已排序的向量中。然后您返回将项目添加到未排序区域。当达到限制时,再次对其进行排序并将其与现有的排序数据合并。

假设你将未排序的chunk限制为不大于log(N),搜索复杂度仍然是O(log N)——一次log(n)二分查找或log log N插值查找排序的chunk,一次log (n) 对未排序的块进行线性搜索。一旦您确认一个项目尚不存在,添加它具有恒定的复杂性(只需将其添加到未排序块的末尾)。最大的优点是它仍然可以轻松使用向量等连续结构,因此它比典型的树结构更易于缓存。

由于您的全局直方图(显然)只填充了来自本地直方图的数据,因此可能值得考虑将其保存在向量中,并且当您需要合并来自本地块之一的数据时,只需使用std::merge 获取现有的全局直方图和局部直方图,并将它们合并为一个新的全局直方图。这具有 O(N + M) 复杂度(N = 全局直方图的大小,M = 局部直方图的大小)。根据局部直方图的典型大小,这很容易成为胜利。

【讨论】:

    【解决方案2】:

    合并两个已排序的容器比排序要快得多。它的复杂性是 O(N),所以理论上你说的有道理。这就是为什么merge-sort 是最快的排序算法之一的原因。如果您点击链接,您还会发现伪代码,您所做的只是主循环的一次。
    您还会发现在 STL 中实现的算法为 std::merge。这需要任何容器作为输入,我建议使用 std::vector 作为新元素的默认容器。对向量进行排序是一项非常快速的操作。您甚至可能会发现使用排序向量而不是集合进行输出会更好。您始终可以使用 std::lower_bound 从排序向量中获得 O(Nlog(N)) 性能。
    与 set/map 相比,向量有很多优点。其中最重要的是它们很容易在调试器中可视化:-)

    (std::merge底部的代码展示了一个使用向量的例子)

    【讨论】:

      【解决方案3】:

      您可以使用特殊功能更有效地合并集合。

      如果您坚持,insert 会返回有关插入位置的信息。

      iterator insert( const_iterator hint, const value_type&amp; value );

      代码:

      std::set <int> bigSet{1,2,5,7,10,15,18};
      std::set <int> biggerSet{50,60,70};  
      
      auto hint = bigSet.cend();
      for(auto& bigElem : biggerSet)
          hint = bigSet.insert(hint, bigElem);
      

      当然,这假设您要插入新元素,这些新元素将在最终集合中结束或关闭。否则没有什么好处,只是因为来源是set(它是有序的),所以这三个中大约有一半不会被查找。

      还有一个成员函数 template&lt; class InputIt &gt; void insert( InputIt first, InputIt last );。 这可能会或可能不会在内部做这样的事情。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-12-12
        • 1970-01-01
        • 1970-01-01
        • 2011-01-08
        • 2013-03-16
        • 1970-01-01
        • 2018-03-12
        相关资源
        最近更新 更多