【问题标题】:Should use an insertion sort or construct a heap to improve performance?应该使用插入排序还是构建堆来提高性能?
【发布时间】:2009-07-23 12:16:37
【问题描述】:

我们有大型(100,000 多个元素)有序的结构向量(运算符

std::vector < MyType > vectorMyTypes;
std::sort(vectorMyType.begin(), vectorMyType.end());

我的问题是,在向这些向量添加新元素同时保留排序顺序时,我们会遇到性能问题。目前我们正在做类似的事情:

for ( a very large set )
{
    vectorMyTypes.push_back(newType);
    std::sort(vectorMyType.begin(), vectorMyType.end());

    ...

    ValidateStuff(vectorMyType); // this method expects the vector to be ordered
}

完全不是我们的代码的样子,因为我知道这个示例可以通过不同的方式进行优化,但是它让您了解性能可能是一个问题,因为我正在排序在每个push_back之后。

我认为我基本上有两个选择来提高性能:

  1. 使用(手工制作?)插入排序而不是 std::sort 来提高排序性能(对部分排序的向量进行插入排序非常快)

    李>
  2. 使用std::make_heapstd::push_heap创建堆以保持排序顺序

我的问题是:

  • 我应该实现插入排序吗? Boost 中有什么可以帮助我的吗?

  • 我应该考虑使用堆吗?我该怎么做?


编辑:

感谢您的所有回复。我知道我给出的示例远非最佳,它并不能完全代表我现在代码中的内容。它只是为了说明我遇到的性能瓶颈 - 也许这就是为什么这个问题没有得到很多支持:)

非常感谢您Steve,通常最简单的答案是最好的,也许是我对问题的过度分析使我对最明显的解决方案视而不见。我确实喜欢您概述的直接插入预购向量的简洁方法。

正如我所评论的,我现在只能使用向量,因此不能选择 std::set、std::map 等。

【问题讨论】:

  • “将新元素插入排序”任务在向量中处理得不好,所以问题是:你是否与向量结婚,为什么?我认为这是出于其他使用原因?
  • 我假设您不能将排序和验证阶段移到循环之外,但如果可以,那么 mmutz 的答案很好。

标签: c++ algorithm sorting stl boost


【解决方案1】:

有序插入不需要提升:

vectorMyTypes.insert(
    std::upper_bound(vectorMyTypes.begin(), vectorMyTypes.end(), newType),
    newType);

upper_bound 提供了一个有效的插入点,前提是该向量已排序开始,因此只要您只在正确的位置插入元素,就完成了。我本来是说lower_bound,但是如果向量包含多个相等的元素,那么upper_bound会选择需要较少工作的插入点。

这确实必须复制 O(n) 个元素,但是您说插入排序“快得令人难以置信”,而这更快。如果不够快,就得想办法分批添加物品,最后验证,或者放弃连续存储,改用保持秩序的容器,比如setmultiset

堆不维护底层容器中的顺序,但对优先级队列或类似的队列很有用,因为它可以快速删除最大元素。您说您想按顺序维护向量,但如果您实际上从未按顺序遍历整个集合,那么您可能不需要将它完全排序,这时堆就很有用了。

【讨论】:

  • 而与 binary_search 的 O(log n) 相比,upper_bound 是 O(n) (IIRC) 并不重要,因为插入是 O(n)
  • upper_bound 执行二进制搜索,标准保证随机访问迭代器的 O(log N)。不同之处在于它返回一个迭代器,而binary_search 返回bool
【解决方案2】:

根据 Meyers 的 Effective STL 的第 23 项,如果您的应用程序在 3 个阶段中使用其数据结构,则应使用排序向量。从书中,他们是:

  1. 设置。通过向其中插入大量元素来创建新的数据结构。在这个阶段,几乎所有的操作都是插入和擦除。在不存在的情况下很少进行查找
  2. 查找。查阅数据结构以查找特定的信息。在这个阶段,几乎所有的操作都是查找。插入和擦除很少或不存在。有这么多的查找,这个阶段的表现使得其他阶段的表现变得偶然。
  3. 重组。修改数据结构的内容。也许通过擦除所有当前数据并在其位置插入新数据。在行为上,此阶段相当于第 1 阶段。一旦此阶段完成,应用程序将返回第 2 阶段

如果您对数据结构的使用与此类似,则应使用排序向量,然后使用 binary_search 作为提及。如果没有,典型的关联容器应该这样做,这意味着 set、multi-set、map 或 multimap 因为这些结构 默认情况下是有序的

【讨论】:

  • 正是我要说的。
  • 听上去好像也需要订购套装,但是
  • 这不正是提问者想要的吗?
  • 除非您分析过您的应用并发现这些查找是关键的性能(或空间)问题,否则您应该始终更喜欢使用集合而不是排序向量:它使您的代码更清晰,更易于维护。
  • 不幸的是,我受到技术原因的限制,无法使用向量 - 这也是我的第一个想法! :)
【解决方案3】:

为什么不直接使用二分搜索来查找插入新元素的位置?然后您将准确插入所需的位置。

【讨论】:

  • 同意,但仍有为向量重新分配空间的开销,并且在新向量中插入后必须将元素向下移动 1。
  • 重新分配是摊销的常数时间,如果预先知道插入元素的数量,就可以一次性重新分配。
  • 是的,但它确实使您之前拥有的任何指针/迭代器无效。
【解决方案4】:

如果您需要在排序序列中插入大量元素,请使用std::merge,可能首先对新元素进行排序:

void add( std::vector<Foo> & oldFoos, const std::vector<Foo> & newFoos ) {
    std::vector<Foo> merged;
    // precondition: oldFoos _and newFoos_ are sorted
    merged.reserve( oldFoos.size() + newFoos.size() ); // only for std::vector
    std::merge( oldFoos.begin(), oldFoos.end(),
                newFoos.begin(), newFoos.end(),
                std::back_inserter( merged );
    // apply std::unique, if wanted, here
    merged.erase( std::unique( merged.begin(), merged.end() ), merged.end() );
    oldFoos.swap( merged ); // commit changes
}

【讨论】:

  • +1 如果用户的要求允许批量添加多个新元素。
【解决方案5】:

使用二分搜索来查找插入位置不会大大加快算法速度,因为插入仍然需要 O(N)(考虑在向量的开头插入 - 您必须移动每个元素向下一个以创建空间)。

一棵树(又名堆)的插入时间为 O(log(N)),性能要好得多。

http://www.sgi.com/tech/stl/priority_queue.html

请注意,除非它是平衡的,否则树在插入时仍将具有最坏情况 O(N) 性能,例如AVL 树。

【讨论】:

  • Asympotitacally 你是正确的,但在实践中,如果插入点通常足够接近vector 的末尾,则使用二分搜索可以获得可观的收益。 (请记住,比较是通过重载的函数调用完成的,并且可能非常昂贵。)
  • 当您可以获得 O(log(N)) 性能时,为什么还要满足于 O(N) 插入(最坏情况和平均情况)?
  • 因为他可能需要大量随机访问读取,这是 O(1) 与 vector 而不是对数与 set/map
【解决方案6】:

为什么不使用 boost::multi_index

注意:boost::multi_index 不提供内存连续性,这是std::vectors 的一种属性,通过该属性,元素彼此相邻地存储在单个内存块中。

【讨论】:

    【解决方案7】:

    您需要做一些事情。

    1. 您可能需要考虑使用reserve() 以避免过度重新分配整个向量。如果您知道它将增长到的大小,您可以通过自己执行resrve()s 来获得一些性能(而不是让实现使用内置的启发式自动执行它们)。

    2. 进行二分搜索以找到插入位置。然后resize 并将插入点之后的所有内容上移一位以腾出空间。

    3. 考虑一下:你真的要使用向量吗?也许setmap 更好。

    二分查找相对于lower_bound 的优势在于,如果插入点靠近向量的末尾,则无需支付 theta(n) 复杂度。

    【讨论】:

    • vector 已经有一个以大增量重新分配缓冲区的策略。除非用户提前知道最终大小,否则 Reserve() 通常不会有太大帮助。
    • 这就是明智的意思。如果他知道得更好,让他reserve。如果不是,则不是。实验会产生正确的答案。
    • 但是如果你让它做默认的事情,你就不必“每次重新分配并复制整个向量”。所以我想你可能已经提出了一个稻草人论据,支持使用reserve() 的程序员。
    • 这只是内置启发式算法的好坏以及时间/内存权衡的问题。我只是说可能是个问题。我想我会软化语言。
    【解决方案8】:
    1. 如果你想将一个元素插入到“正确”的位置,你为什么要使用 sort。使用lower_bound 找到位置并使用向量的“插入”方法插入。插入新项目仍然需要 O(N)。

    2. 堆不会帮助你,因为堆没有排序。它允许您快速获取最小元素,然后快速删除它并获取下一个最小元素。但是,堆中的数据不是按排序顺序存储的,因此如果您的算法必须按顺序遍历数据,那将无济于事。

    恐怕你的描述略过很多细节,但似乎列表不是任务的正确元素。 std::deque 更适合插入中间,你也可以考虑std::set。我建议您解释一下为什么需要对数据进行排序以获得更多有用的建议。

    【讨论】:

      【解决方案9】:

      您可能需要考虑使用 BTree 或 Judy Trie。

      • 您不想将连续内存用于大型集合,插入不应花费 O(n) 时间;
      • 您希望至少对单个元素使用二进制插入,应该对多个元素进行预排序,以便缩小搜索边界;
      • 您不希望您的数据结构浪费内存,因此每个数据元素都没有左右指针。

      【讨论】:

        【解决方案10】:

        正如其他人所说,我可能已经从链表中创建了一个 BTree,而不是使用向量。即使您解决了排序问题,向量也存在在需要增长时完全重新分配的问题,假设您事先不知道最大大小。

        如果您担心列表分配到不同的内存页面并导致与缓存相关的性能问题,请在数组中预先分配节点,(将对象池化)并将它们插入列表中。

        您可以在数据类型中添加一个值,以指示它是从堆外分配还是从池中分配。这样,如果您检测到您的池空间不足,您可以开始从堆中分配并向自己抛出一个断言或其他东西,以便您知道增加池大小(或将其设置为命令行选项。

        希望这会有所帮助,因为我看到您已经有了很多很好的答案。

        【讨论】:

          猜你喜欢
          • 2013-06-05
          • 1970-01-01
          • 2016-12-02
          • 2018-03-22
          • 1970-01-01
          • 2011-09-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多