【问题标题】:Sorting a small array into a large sorted array将一个小数组排序为一个大排序数组
【发布时间】:2021-12-28 02:15:22
【问题描述】:

合并一个大的排序数组和一个小的未排序数组的最佳算法是什么?

我将从我的特定用例中举例说明我的意思,但不要觉得被它们束缚:我主要是想对问题给出一种感觉。

8 MB 排序数组和 92 kB 未排序数组(缓存内排序)
2.5 GB 排序数组和 3.9 MB 未排序数组(内存排序)
34 GB 排序数组和 21 MB 未排序数组 (out-of-memory sort)

【问题讨论】:

    标签: algorithm performance sorting optimization language-agnostic


    【解决方案1】:

    您可以实现一个基于块的算法来有效地解决这个问题(无论数组的输入大小如何,只要一个比另一个小得多)。

    首先,您需要对小数组进行排序(如果您不需要自定义比较器,可以使用 radix sortbitonic sort)。 然后想法是将大数组切割成完全适合 CPU 缓存的块(例如 256 KiB)。 对于每个块,使用二进制搜索找到小数组中最后一项的索引 traditional merge algorithm 快得多,因为由于二分搜索和按块插入的少量项目,所需的比较次数要少得多。

    对于较大的输入,您可以使用并行实现。这个想法是同时处理一组多个块(即超级块)。 超级块比经典块大得多(例如> = 2 MiB)。 每个线程一次在一个超级块上工作。对小数组执行二进制搜索,以了解每个超级块中插入了多少值。 这个数字在线程之间共享,因此每个线程都知道它可以独立于其他线程安全地写入输出的位置(可以使用并行扫描算法在大规模并行架构上执行此操作)。然后将每个超级块拆分为经典块,并使用前面的算法独立解决每个线程中的问题。 这种方法即使在小输入数组不适合缓存时也应该更有效,因为整个小数组中的二进制搜索操作的数量将大大减少。

    算法的(摊销)时间复杂度是O(n (1 + log(m) / c) + m (1 + log(c))),其中m 是大数组的长度,n 是小数组的长度,c 是块大小(此处忽略超块为清楚起见,但它们仅将复杂性更改为常数因子,如常数 c 所做的)。

    替代方法/优化:如果您的比较运算符很便宜并且可以使用 SIMD 指令向量化,那么您可以优化传统的合并算法。由于分支(在一般情况下几乎无法预测),并且因为它不能轻松/有效地矢量化,传统方法非常慢。然而,由于大数组比小数组大得多,传统算法会在小数组之间从大数组中选取大量连续值。这意味着您可以选择大数组的 SIMD 块并将值与小数组之一进行比较。如果所有 SIMD 项都小于从小数组中挑选的项,那么您可以非常有效地一次写入整个 SIMD 块。否则需要先写SIMD chunk的一部分,再写小数组的item,再切换到下一个。最后一个操作显然效率较低,但它应该很少发生,因为小数组比大数组小得多。注意小数组还是需要先排序的。

    【讨论】:

    • “二分法”是指正常的二分搜索吗?
    • 确实如此。感谢您指出这一点。我认为“二分法”是法语“recherche dichotomique”的错误翻译;)。
    • 这太完美了,谢谢!我认为我们可以针对这种特殊情况改进标准合并算法,看来您已经找到了利用这种情况特征的好方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-20
    • 2011-08-22
    • 1970-01-01
    • 1970-01-01
    • 2011-07-06
    • 1970-01-01
    相关资源
    最近更新 更多