【问题标题】:What's the most performant way to create a sorted copy of a C++ vector?创建 C++ 向量的排序副本的最高效方法是什么?
【发布时间】:2017-08-12 10:47:25
【问题描述】:

给定一个 C++ 向量(假设它是双精度的,我们称之为 unsorted),创建包含 unsorted 的排序副本的新向量 sorted 的最高效方法是什么?

考虑以下简单的解决方案:

std::vector<double> sorted = unsorted;
std::sort(sorted.begin(), sorted.end());

这个解决方案有两个步骤:

  1. 创建unsorted 的完整副本。
  2. 排序。

但是,在步骤 1 的初始副本中可能会浪费大量精力,尤其是对于(例如)已经大部分排序的大型向量。

如果我手动编写此代码,我可以将排序算法的第一遍与步骤 1 结合起来,通过让第一遍从 unsorted 向量中读取值,同时将它们写入 @ 987654328@。根据算法,后续步骤可能仅适用于 sorted 中的数据。

有没有办法用 C++ 标准库、Boost 或第三方跨平台库来做这样的事情?

重要的一点是确保sorted C++ 向量的内存在排序开始之前不会不必要地初始化为零。许多排序算法需要立即对sorted 向量进行随机写入访问,因此使用reserve()push_back() 不适用于第一次通过,但resize() 会浪费时间初始化向量。


编辑:由于答案和 cmets 不一定明白为什么“幼稚的解决方案”效率低下,因此请考虑 unsorted 数组实际上已经按排序顺序排列的情况(或者只是需要一个交换来排序)。在这种情况下,无论使用哪种排序算法,对于简单的解决方案,每个值都需要至少读取两次——一次是在复制时,一次是在排序时。但是使用排序时复制解决方案,读取次数可能会减半,因此性能大约会增加一倍。无论unsorted 中的数据如何,当使用比std::sort 性能更高的排序算法(可能是O(n) 而不是O(n log n))时,都会出现类似的情况。

【问题讨论】:

  • 先复制,再排序。 std::vector 有一个高效的复制方法。
  • 在这种情况下,朴素的解决方案非常可靠。
  • 以下链接不是通用算法,但值得一读。 probablydance.com/2016/12/27/i-wrote-a-faster-sorting-algorithm
  • 如果在 STL 中添加一个insertion_sort 方法会很好,这样可以很好地完成这项任务。

标签: c++ sorting stdvector


【解决方案1】:

标准库 - 故意 - 没有在复制时排序功能,因为副本是 O(n) 而std::sort 是 O(n log n)。

因此,对于任何较大的 n 值,排序将完全支配成本。 (如果 n 很小,那也没关系)。

【讨论】:

  • 虽然std::sort 在一般情况下为 O(n log n),但它并不适用于所有可能的输入值。考虑几乎完全排序但只需要交换两个值的数据:“sort-while-copy”的速度大约是“copy-then-sort”的两倍。同样,考虑线性时间排序算法,例如radix sortbucket sort。 Boost 包含spreadsort,它适用于整数和浮点数,但需要初始复制操作。
  • 如果您对自己的数据有非常的特定知识,那么您当然通常可以做得比一般算法更好。如果您总是正好有两个元素乱序,例如,您可以交换它们,使用排序后的数组作为某个函数的输入,然后再次交换它们。那可能会更快。但是,不,标准库没有这样的东西。如果你有这方面的具体知识,使用std::sort 可能是最糟糕的选择,因为它对于已经排序的数据可能会达到 O(n^2)。
【解决方案2】:

假设双精度向量不包含像 NAN 或无穷大这样的特殊数字,那么双精度可以被视为 64 位符号 + 幅度整数,可以转换为用于最快的基数排序。这些“符号 + 幅度整数”需要转换为 64 位无符号整数。这些宏可用于来回转换 SM 代表符号 + 幅度,ULL 代表 unsigned long long (uint64_t)。假设为了使用这些宏,双打被强制转换为 unsigned long long 类型:

#define SM2ULL(x) ((x)^(((~(x) >> 63)-1) | 0x8000000000000000ull))
#define ULL2SM(x) ((x)^((( (x) >> 63)-1) | 0x8000000000000000ull))

请注意,使用这些宏会将负零视为小于正零,但这通常不是问题。

由于基数排序需要初始读取传递来生成计数矩阵(然后将其转换为逻辑桶边界的开始或结束索引),因此在这种情况下,初始读取传递将是复制传递,它也生成计数矩阵。基数为 256 的排序将使用大小为 [8][256] 的矩阵,并且在复制之后,将执行 8 次基数排序。如果向量远大于缓存大小,则占主导地位的时间因素将是每个基数排序过程中的随机访问写入。

【讨论】:

  • 我确实知道这个技巧,这是一个很好的技巧,而且我确实熟悉使用它的排序实现(请参阅我对@BoPersson 答案的评论)。不幸的是,这无助于解决使用标准库时浪费的初始副本问题,因为我能找到的所有实现似乎都是就地排序的。我本质上是在寻找一个预先编写的排序实现,它可以在std::vector&lt;double&gt; sorted = fast_sorted_copy(unsorted); 的行中工作,这样我就不需要自己动手了。
  • @JohnSpeeks - 我不知道基数排序的标准库实现。第一遍可以读取、转换、写入,还可以创建一个转换为索引的计数矩阵。 example of radix sort base 256
猜你喜欢
  • 2017-09-14
  • 2017-06-17
  • 2018-03-03
  • 1970-01-01
  • 2010-09-07
  • 1970-01-01
  • 2010-11-05
相关资源
最近更新 更多