常见的排序算法——归并排序(六)
本文记述了多向归并排序的基本思想并给出了一份参考实现代码。在说明了算法的性能后用随机数据进行了验证。
◆ 思想
在归并排序、归并排序(二)、归并排序(三)、归并排序(四)中记述的归并排序,都是把待排序范围分成两个部分分别排序的。而多向归并排序是把待排序范围分为 K 个部分,把它们分别排序然后进行归并。
◆ 实现
排序代码采用《算法(第4版)》的“排序算法类模板”实现。(代码中涉及的基础类,如 Array,请参考算法文章中涉及的若干基础类的主要API)
// merge6.hxx
...
class Merge6
{
...
template
<
class _T,
class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
>
static
void
sort(int K, Array<_T> & a) // #1
{
Array<_T> aux = Array<_T>(a.size());
__sort__(K, a, 0, a.size()-1, aux);
}
...
template
<
class _T,
class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
>
static
void
__sort__(int K, Array<_T> & a, int lo, int hi, Array<_T> & aux)
{
if (hi <= lo) return;
int N = hi - lo + 1;
Array<int> s(K), e(K); // start and end indices for subarrays
int sz = int(std::ceil(double(N) / K));
for (int i = 0; i < K; ++i) { // #2
s[i] = lo + i * sz;
e[i] = (s[i] + sz - 1 < hi) ? s[i] + sz-1 : hi;
}
for (int i = 0; i < K; ++i) // #3
__sort__(K, a, s[i], e[i], aux);
__merge__(K, a, s, e, aux); // #4
}
...
template
<
class _T,
class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
>
static
void
__merge__(int K, Array<_T> & a, Array<int> & s, Array<int> & e, Array<_T> & aux)
{
for (int i = s[0]; i <= e[K-1]; ++i) // #5
aux[i] = a[i];
for (int i = s[0], min; i <= e[K-1]; ++i) {
min = s[0];
for (int k = 0; k < K; ++k)
if (s[k] <= e[k] && __less__(aux[s[k]], aux[min]))
min = s[k], ++s[k];
a[i] = aux[min]; // #6
}
}
...
template
<
class _T,
class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
>
static
bool
__less__(_T const& v, _T const& w)
{
return v.compare_to(w) < 0; // #7
}
...
向数 K 作为参数被传递给排序算法(#1)。在排序前,先计算每个子范围的开始和结束位置(#2),把它们分别排序(#3),然后把各个排序的结果归并起来(#4)。在归并前,先利用辅助空间临时存放 K 个子范围的元素(#5),再逐一找到 K 个子范围中的当前最小元素并将其放回到排序范围内(#6),直至 K 个子范围都归并完。将 '<' 改为 '>',即得到逆序的结果(#7)。
◆ 性能
时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|
N*log(N) | N | 是 |
◆ 验证
测试代码采用《算法(第4版)》的倍率实验方案,用随机数据验证其正确性并获取时间复杂度数据。
// test.cpp
...
time_trial(int K, int N)
{
Array<Double> a(N);
for (int i = 0; i < N; ++i) a[i] = Std_Random::random(); // #1
Stopwatch timer;
Merge6::sort(K, a); // #2
double time = timer.elapsed_time();
assert(Merge6::is_sorted(a)); // #3
return time;
}
...
test(char * argv[])
{
int T = std::stoi(argv[1]); // #4
Std_Out::printf("%10s%10s%10s%10s%10s%10s%10s%10s%10s%10s%10s\n", "N", "K = 2", "K = 3", "K = 4", "K = 5", "K = 6", "K = 7", "K = 8", "K = 9", "K = 10", "K = 11");
for (int i = 0, N = 1024; i < T; ++i, N += N) { // #5
double time2 = time_trial(2, N);
double time3 = time_trial(3, N);
double time4 = time_trial(4, N);
double time5 = time_trial(5, N);
double time6 = time_trial(6, N);
double time7 = time_trial(7, N); // #6
double time8 = time_trial(8, N);
double time9 = time_trial(9, N);
double time10 = time_trial(10, N);
double time11 = time_trial(11, N);
Std_Out::printf("%10d%10.3f%10.3f%10.3f%10.3f%10.3f%10.3f%10.3f%10.3f%10.3f%10.3f\n", N, time2, time3, time4, time5, time6, time7, time8, time9, time10, time11);
}
}
...
用 [0,1) 之间的实数初始化待排序数组(#1),打开计时器后执行排序(#2),确保得到正确的排序结果(#3)。整个测试过程要执行 T 次排序(#4)。每次执行排序的数据规模都会翻倍(#5),并得到不同 K 值下的排序时间(#6),
此测试在实验环境一中完成,
$ g++ -std=c++11 test.cpp std_out.cpp std_random.cpp stopwatch.cpp type_wrappers.cpp
N K = 2 K = 3 K = 4 K = 5 K = 6 K = 7 K = 8 K = 9 K = 10 K = 11
1024 0.028 0.023 0.018 0.022 0.018 0.021 0.025 0.025 0.026 0.020
2048 0.060 0.043 0.047 0.040 0.050 0.039 0.044 0.049 0.052 0.059
4096 0.128 0.104 0.082 0.097 0.089 0.107 0.085 0.093 0.099 0.107
8192 0.272 0.232 0.214 0.198 0.219 0.195 0.240 0.252 0.198 0.213
16384 0.575 0.428 0.380 0.458 0.423 0.381 0.426 0.484 0.547 0.571
32768 1.208 0.956 0.951 0.898 0.814 1.037 0.833 0.932 1.009 1.114
65536 2.541 2.146 1.707 1.693 2.019 1.881 2.267 2.419 1.984 2.146
131072 5.323 4.072 4.192 4.207 4.025 4.567 4.145 4.614 5.127 4.289
262144 11.132 9.178 7.587 7.649 7.590 8.870 8.134 8.823 9.945 11.181
524288 23.245 17.141 18.181 18.439 18.972 17.017 20.447 17.565 18.952 20.757
1048576 48.281 39.075 33.000 35.410 34.756 40.479 38.386 44.446 46.330 40.997
2097152 100.685 84.436 78.072 80.730 82.781 80.011 74.419 82.705 90.530 101.835
4194304 208.772 158.872 142.965 157.102 159.215 152.687 186.003 160.461 178.676 196.547
8388608 433.488 352.724 338.606 303.316 309.389 364.163 350.672 398.539 352.995 387.393
16777216 898.020 734.325 618.355 693.583 738.794 709.554 684.961 755.231 861.721 764.983
在给定的 N 下,将 K = 2, 3, ..., 11 对应的时间按照从短到长的顺序排名,得到下表中黑粗线上边的排名数据。然后在针对某个 K 下的所有排名,计算平均数、中位数和众数,得到下表中黑粗线下边的结果。
根据这些结果可知,对于随机数据而言,当 K = 4 时多向归并表现最佳。
◆ 最后
完整的代码请参考 [gitee] cnblogs/18210127 。
查看性能对比,了解此算法与其它排序算法的相似性和差异性。
写作过程中,笔者参考了《算法(第4版)》的归并排序、练习题 2.2.25、“排序算法类模板”和倍率实验。致作者 Sedgwick,Wayne 及译者谢路云。
受限于作者的水平,读者如发现有任何错误或有疑问之处,请追加评论或发邮件联系 green-pi@qq.com。作者将在收到意见后的第一时间里予以回复。 本文来自博客园,作者:green-cnblogs,转载请注明原文链接:https://www.cnblogs.com/green-cnblogs/p/18210127 谢谢!