应该使用插入排序还是构造堆来提高性能?

时间:2009-07-23 12:16:38

标签: c++ algorithm sorting stl boost

我们有大型(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创建堆以维护排序顺序

  3. 我的问题是:

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

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


    修改

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

    非常感谢你Steve,这通常是最简单的答案,而且也许是我对问题的过度分析使我对最明显的解决方案视而不见。我确实喜欢你概述的简洁方法直接插入到预先排序的矢量中。

    正如我评论的那样,我现在只能使用向量,所以std :: set,std :: map等不是一个选项。

10 个答案:

答案 0 :(得分:10)

有序插入不需要提升:

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

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

这必须复制O(n)元素,但是你说插入排序“非常快”,而且速度更快。如果速度不够快,您必须找到一种批量添加项目并在最后验证的方法,否则就放弃连续存储并切换到维护订单的容器,例如set或{{1 }}

堆不会在底层容器中维护顺序,但是对于优先级队列或类似物是有用的,因为它可以快速删除最大元素。你说你想按顺序维护向量,但如果你从来没有按顺序迭代整个集合,那么你可能不需要它完全排序,那就是堆有用的时候。

答案 1 :(得分:6)

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

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

如果您对数据结构的使用类似于此,则应使用已排序的向量,然后使用提及的binary_search。如果没有,典型的关联容器应该这样做,这意味着集,多集,地图或多图,因为这些结构默认排序

答案 2 :(得分:3)

为什么不使用二进制搜索来查找插入新元素的位置?然后,您将完全插入所需的位置。

答案 3 :(得分:1)

如果您需要将大量元素插入到已排序的序列中,请使用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
}

答案 4 :(得分:0)

为什么不使用boost::multi_index

注意:boost::multi_index不提供内存连续性,std::vectors的属性,在单个内存块中元素彼此相邻存储。

答案 5 :(得分:0)

您需要做一些事情。

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

  2. 执行二进制搜索以查找插入位置。然后resize并将插入点后的所有内容向上移动一个以腾出空间。

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

  4. 二进制搜索优于lower_bound的优点是,如果插入点接近向量的末尾,则不必支付theta(n)复杂度。

答案 6 :(得分:0)

使用二进制搜索来查找插入位置不会加速算法,因为它仍然是O(N)来进行插入(考虑插入向量的开头 - 你必须每次移动元素向下创建空间。)

树(也就是堆)将插入O(log(N)),性能要好得多。

请参阅http://www.sgi.com/tech/stl/priority_queue.html

请注意,除非是平衡的,否则树将仍然具有最差的O(N)性能,例如, AVL树。

答案 7 :(得分:0)

  1. 如果要将元素插入“右”位置,为什么要计划使用排序。使用lower_bound找到位置,然后插入,使用矢量的“插入”方法。插入新项目仍然是O(N)。

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

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

答案 8 :(得分:0)

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

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

答案 9 :(得分:0)

正如其他人所说,我可能已经从链表创建了一个BTree,而不是使用向量。即使您已经超越了排序问题,但是假设您事先并不知道最大尺寸,那么矢量会在需要增长时完全重新分配。

如果您担心列表分配在不同的内存页面上并导致缓存相关的性能问题,请在阵列中预分配您的节点(汇集对象)并将这些插入到列表中。

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

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