常见的排序算法——归并排序(三)

news/2024/9/22 1:21:15

本文记述了归并排序的 3 项改进和一份参考实现代码,并在说明了算法的性能后用随机数据进行了验证。

◆ 思想

本文实现了《算法(第4版)》书中提到的 3 项改进,

  • 对小规模子数组使用插入排序。减少在小规模数组中的递归调用能改进整个算法。
  • 测试数组是否已经有序。任意有序的子数组算法的运行时间变成线性的。
  • 不将元素复制到辅助数组。可以节省将数组元素复制到用于归并的辅助数组所用的时间。

◆ 实现

排序代码采用《算法(第4版)》的“排序算法类模板”实现。(代码中涉及的基础类,如 Array,请参考算法文章中涉及的若干基础类的主要API)

// merge3.hxx...class Merge3
{...template<class _T,class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type>staticvoidsort(Array<_T> & a){int N = a.size();Array<_T> aux = Array<_T>(N);for (int k = 0; k < N; ++k)             // #3aux[k] = a[k];__sort__(aux, 0, N-1, a);               // #4}...template<class _T,class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type>staticvoid__sort__(Array<_T> & a, int lo, int hi, Array<_T> & aux){if (hi - lo <= 15) {                            // #1for (int i = lo+1; i <= hi; ++i)for (int j = i; j > lo && __less__(aux[j], aux[j-1]); --j)__exch__(aux, j, j-1);} else {int mi = lo + (hi - lo) / 2;__sort__(aux, lo, mi, a);                   // #5__sort__(aux, mi+1, hi, a);if (__less__(a[mi], a[mi+1]))               // #2for (int i = lo; i <= hi; ++i)aux[i] = a[i];else__merge__(a, lo, mi, hi, aux);}}...template<class _T,class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type>staticvoid__merge__(Array<_T> & a, int lo, int mi, int hi, Array<_T> & aux){int i = lo, j = mi+1;for (int k = lo; k <= hi; ++k)if      (i > mi)                aux[k] = a[j++];else if (j > hi)                aux[k] = a[i++];else if (__less__(a[j], a[i]))  aux[k] = a[j++];else                            aux[k] = a[i++];}...template<class _T,class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type>staticbool__less__(_T const& v, _T const& w){return v.compare_to(w) < 0;         // #6}...

当待排序的子数组规模较小时,采用插入排序(#1)。测试数组是否已经有序,可确保有序的子数组算法的运行时间变成线性的(#2)。为了节省将数组元素复制到用于归并的辅助数组所用的时间,排序开始前先做一次复制(#3),然后在递归调用的每个层次交换输入数组和辅助数组的角色,先将数据从输入数组排序到辅助数组(#5),再将数据从辅助数组排序到输入数组(#4)。将 '<' 改为 '>',即得到逆序的结果(#6)。

◆ 性能

时间复杂度 空间复杂度 是否稳定
N*log(N) N

◆ 验证

测试代码采用《算法(第4版)》的倍率实验方案,用随机数据验证其正确性并获取时间复杂度数据。

// test.cpp...time_trial(int N)
{Array<Double> a(N);for (int i = 0; i < N; ++i) a[i] = Std_Random::random();    // #1Stopwatch timer;Merge3::sort(a);                     // #2double time = timer.elapsed_time();assert(Merge3::is_sorted(a));            // #3return time;
}...test(char * argv[])
{int T = std::stoi(argv[1]);          // #4double prev = time_trial(512);Std_Out::printf("%10s%10s%7s\n", "N", "Time", "Ratio");for (int i = 0, N = 1024; i < T; ++i, N += N) {            // #5double time = time_trial(N);Std_Out::printf("%10d%10.3f%7.2f\n", N, time, time/prev);   // #6prev = time;}
}...

用 [0,1) 之间的实数初始化待排序数组(#1),打开计时器后执行排序(#2),确保得到正确的排序结果(#3)。整个测试过程要执行 T 次排序(#4)。每次执行排序的数据规模都会翻倍(#5),并以上一次排序的时间为基础计算倍率(#6),

此测试在实验环境一中完成,

$ g++ -std=c++11 test.cpp std_out.cpp std_random.cpp stopwatch.cpp type_wrappers.cpp$ ./a.out 15N      Time  Ratio1024     0.007   2.332048     0.015   2.144096     0.032   2.138192     0.070   2.1916384     0.149   2.1332768     0.315   2.1165536     0.667   2.12131072     1.405   2.11262144     2.952   2.10524288     6.189   2.101048576    12.941   2.092097152    27.072   2.094194304    56.800   2.108388608   118.166   2.0816777216   245.443   2.08

可以看出,随着数据规模的成倍增长,排序所花费的时间将是上一次规模的 2.1? 倍,且在不断变小。将数据反映到以 2 为底数的对数坐标系中,可以得到如下图像,

test_data

O(N*log(N)) 代表了线性对数级别复杂度下的理论排序时间,该行中的数据是以 Time 行的第一个数据为基数逐一乘 2 + 2/log(N) 后得到的结果(因为做的是倍率实验,所以乘 (2*N*log(2*N)) / (N*log(N)),化简得到 2 + 2/log(N),即乘 2+2/log(1024),2+2/log(2048),2+2/log(4096),... 2+2/log(16777216);因为是二分归并,所以 log 的底数为 2)。

◆ 最后

完整的代码请参考 [gitee] cnblogs/18191308 。

写作过程中,笔者参考了《算法(第4版)》的归并排序、练习题 2.2.11、“排序算法类模板”和倍率实验。致作者 Sedgwick,Wayne 及译者谢路云。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/31570.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

团队作业4.7——Scrum Day 7(2024.5.13)

Scrum冲刺博客集合Scrum冲刺博客 链接第1篇Scrum冲刺博客 https://www.cnblogs.com/Shangrila12581/p/18181060第2篇Scrum冲刺博客 https://www.cnblogs.com/Shangrila12581/p/18181084第3篇Scrum冲刺博客 https://www.cnblogs.com/Shangrila12581/p/18182774第4篇Scrum冲刺博客…

u-boot网络移植

使用的板子为正点原子的开发板,移植官方当前最新的u-boot修改网口配置信息 主要修改设备树的信息,设备树位于:arch/arm/dts/imx6ul-14x14-evk.dtsi 硬件电路图修改fec2信息 未修改前的信息如下:修改网口1器件的ID信息,网口1使用的ID是0&fec2 {pinctrl-names = "d…

C#循环体特点

C#循环体特点 while 先判断条件,后执行循环体每次执行,均需先验证条件,比如读取每一行数据前检查是否有数据 do...while 先执行1次循环体,再判断条件,第一次执行无需验证条件,比如输入密码,若错误则重新输入 for 与循环次数有关的元素都放在(::)里面,已知循环次数,…

【Azure Contianer Apps】在云上使用容器应用时收集日志遇见延迟问题

问题描述 把应用部署到Azure Container Apps(容器应用),可以在Container App Environemnt级别设置诊断日志,把日志收集到Event Hub / Log Analystic Workspace / Storage Account中。 虽然,这样能把日志导出到目标源中。但在检查日志时候,发现延迟很大,通常要等待7-10分钟…

Rust winit 0.30.0版本简介

不久前,Rust著名的跨平台窗体管理库winit发布了它的0.30.0版本,较之前的0.2x.x版本,新增了19个的模块API,改动大约19个模块API,移除了大约8个模块API。可见本次升级改动之大,主要是对事件循环、窗口管理的重构。鉴于目前网上较多的文章都是基于0.2x版本的winit的代码,存…

4-常用标签

标题标签<h1> - <h6>为了使网页更具有语义化,我们经常会在页面中用到标题标签。HTML提供了6个等级的网页标题加了标题的文字会变的加粗,字号也会一次变大。 一个标题独占一行段落和换行标签在网页中,要把文字有条理地显示出来,就需要将这些文字分段显示。在HTM…

智慧安防系统:构建更安全的社区环境

随着科技的不断进步,人们的生活质量得到了显著提高。然而,与此同时,社会治安问题也日益凸显。为了维护社会的和谐稳定,提高人们的生活安全感,智慧安防系统应运而生。本文将为您详细介绍智慧安防系统的项目背景、需求分析、建设目标、建设内容、技术方案、安全设计以及结语…

桌面图标间距Bug:Win10/Win11桌面图标占用空间变成长方形怎么办?

在使用Windows 10或Windows 11操作系统时,桌面图标的间距突然变得很大,变成了长方形。阅读全文:https://itxiaozhang.com/win10-win11-desktop-icon-bug-rectangular-fix/ 此教程配合视频学习效果最佳,视频教程在文章末尾。问题描述 在使用Windows 10或Windows 11操作系统时…