首页 > 代码库 > 归并排序
归并排序
------------------siwuxie095
归并排序法
它的工作原理如下:
归并排序是建立在归并操作上的一种有效的排序算法,而归并操作
指的是将两个已经排序的序列合并成一个序列的操作
参考链接:
参考链接1,参考链接2,参考链接3
程序 1:自顶向下(递归)的归并排序法的实现
SortTestHelper.h:
#ifndef SORTTESTHELPER_H #define SORTTESTHELPER_H
#include <iostream> #include <string> #include <ctime> #include <cassert> using namespace std;
//辅助排序测试 namespace SortTestHelper {
//生成测试数据(测试用例),返回一个随机生成的数组: //生成有n个元素的随机数组,每个元素的随机范围为[rangeL,rangeR] int *generateRandomArray(int n, int rangeL, int rangeR) { //默认rangeL要小于等于rangeR assert(rangeL <= rangeR);
int *arr = new int[n];
//对于数组中的每一个元素,将之随机成为rangeL和rangeR之间的随机数 //先设置随机种子:这里将当前的时间作为种子来进行随机数的设置 srand(time(NULL));
for (int i = 0; i < n; i++) { //rand()函数+百分号+数的范围,即 取中间的一个随机整数,再加上rangeL即可 arr[i] = rand() % (rangeR - rangeL + 1) + rangeL; } return arr; }
//生成一个近乎有序的数组 int *generateNearlyOrderedArray(int n, int swapTimes) { //先生成完全有序的数组 int *arr = new int[n]; for (int i = 0; i < n; i++) { arr[i] = i; }
//以当前时间为随机种子 srand(time(NULL));
//再随机挑选几对元素进行交换,就是一个近乎有序的数组了 for (int i = 0; i < swapTimes; i++) { int posx = rand() % n; int posy = rand() % n; swap(arr[posx], arr[posy]); }
return arr; }
template<typename T> void printArray(T arr[], int n) { for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; }
//经过排序算法排序后,再次确认是否已经完全排序 template<typename T> bool isSorted(T arr[], int n) { for (int i = 0; i < n - 1; i++) { if (arr[i]>arr[i + 1]) { return false; } } return true; }
//衡量一个算法的性能如何,最简单的方式就是看这个算法在特定数据集上的执行时间 //(1)传入排序算法的名字,方便打印输出 //(2)传入排序算法本身,即函数指针 //(3)传入测试用例:数组和元素个数 template<typename T> void testSort(string sortName, void(*sort)(T[], int), T arr[], int n) { //在排序前后分别调用clock()函数 //时间差就是该排序算法执行的时钟周期的个数 clock_t startTime = clock(); sort(arr, n); clock_t endTime = clock();
assert(isSorted(arr, n));
//endTime 减去 startTime 转为double类型,除以 CLOCKS_PER_SEC,其中: // //CLOCKS_PER_SEC 是标准库中定义的一个宏,表示每一秒钟所运行的时钟周期 //的个数,而(endTime-startTime)返回的是运行了几个时钟周期 // //这样,最终的结果就是在这段时间中程序执行了多少秒 cout << sortName << ":" << double(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl; }
//复制数组 int *copyIntArray(int a[], int n) { int *arr = new int[n]; //copy()函数在std中: //第一个参数是原数组的头指针, //第二个参数是原数组的尾指针, //第三个参数是目的数组的头指针 // //注意:copy()函数运行时会报错,需要在: //项目->属性->配置属性->C/C++->预处理器->预处理器定义 //在其中添加:_SCL_SECURE_NO_WARNINGS copy(a, a + n, arr); return arr; } }
#endif |
MergeSort.h:
#ifndef MERGESORT_H #define MERGESORT_H
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并 //注意边界问题,这里都是前闭后闭 template<typename T> void __merge(T arr[], int l, int mid, int r) {
//辅助空间或临时空间:aux数组,它的大小和传入进来的arr数组一般大 //(aux,即 auxiliary,辅助) // // 经测试,传递aux数组的性能效果并不好 T *aux = new T[r - l + 1];
//将arr数组中的所有元素全部复制到aux数组中 for (int i = l; i <= r; i++) { aux[i - l] = arr[i]; }
//具体的归并: //首先将两个索引i和j分别指向两个排好序的子数组 int i = l, j = mid + 1;
//再使用索引k进行遍历,决定合并后的arr[k]究竟是谁 for (int k = l; k <= r; k++) {
//如果i越界,即左边数组已排序完毕 //直接将arr[j-l]赋值给arr[k] if (i > mid) { arr[k] = aux[j - l]; j++; } //如果j越界,即右边数组已排序完毕 //直接将arr[i-l]赋值给arr[k] else if (j > r) { arr[k] = aux[i - l]; i++; } else if (aux[i - l] < aux[j - l]) { arr[k] = aux[i - l]; i++; } else { arr[k] = aux[j - l]; j++; } }
delete[]aux; }
// 递归使用归并排序,对arr[l...r]的范围进行排序: //(1)l 即 left,r 即 right,[l...r]是前闭后闭 //(2)l 是第一个元素的位置,r 是最后一个元素的位置 //(3)如果是[l...r),即前闭后开,那么 r 是最后一个元素的后一个位置 template<typename T> void __mergeSort(T arr[], int l, int r) {
//如果l大于等于r,则返回,否则进行归并排序 if (l >= r) { return; }
//注意:l+r 有隐含的危险,因为当l和r非常大时, //l+r 有可能会溢出 int mid = (l + r) / 2;
__mergeSort(arr, l, mid); __mergeSort(arr, mid + 1, r);
//merge操作(归并操作) __merge(arr, l, mid, r); }
//归并排序:从小到大进行排序(自顶向下、递归) template<typename T> void mergeSort(T arr[], int n) { //归并排序的本质是一次递归的排序过程,即 需要对数组 //的不同部分继续进行归并排序 // //所以需要一个子函数 __mergeSort(),它是一个私有函数, //被mergeSort()所调用(对用户来说,只需调用mergeSort()) // //参数包含数组,以及当前要处理的数组的开始位置和结束位置 __mergeSort(arr, 0, n - 1); }
//归并排序是O(n*lgn)级别的排序算法,它的思想是: // //当要排序一个数组时,首先把这个数组分成两半,想办法把左边和右边 //的数组给排序,然后再将它们归并起来 // //当对左边的数组和右边的数组进行排序的时候,先分别把左边的数组和 //右边的数组,分成两半,再对每一个部分排序,最后归并 // //这每一个部分,依然是先分半,再排序,最后归并 ... // //当分到一定细度的时候,每一部分就只有一个元素了,此时,不用排序, //它本身就是有序的,只需要简单的对它们进行一次归并即可 // //归并到上一层级之后,进一步进行归并,归并到更高的一个层级,逐层 //的向上上升 ... // //直至最后,归并到最后一层时,整个数组就全部有序了 // // // // //那么为什么要费这么大劲把数组先分半,之后再逐渐归并呢? // //假设一个数组有8个元素,进行归并排序,层层下来,就会分成3级, //到第3级的时候,每一部分只剩下一个元素 // //3级的由来: //总共有8个元素,每次二分,这样下来,经过3次除以2的计算,每一 //部分就只剩下一个元素,即 log8(以2为底) 等于 3 // // //如果有N个元素,就有log(N)的层级数,其中:如果N不是2的x次方, //则log(N)就可能是一个浮点数,取整即可 // //总之,层数是log(N)这样一个数量级的 // // //这种情况下,每一层要处理的元素的个数是一样的,虽然把每一层 //分成了不同的部分,此时,如果整个归并过程可以以O(n)的时间复 //杂度来解决的话,那么就设计出了一个O(n*lgn)级别的算法 // // //事实上,这也是 n*lgn 这个时间复杂度算法的一个主要来源: //通常是先通过二分法达到log(N)这样一个层级,然后每一层级用O(n) //级别的算法来做事情 // // //所以问题就变成了: //能不能将整个数组划分成两部分,两部分都分别排好序后,使用O(n) //的算法将它们归并到一起,形成一个新的有序的数组 // //那么当左右两部分都排好序时,如何把它们合成一个有序的数组呢? //实现这种算法时,不能像插入排序、选择排序那样直接在这个数组上 //通过交换位置来完成这个过程,而是要开辟一个同样大小的临时空间 //来辅助完成归并过程 // // //事实上,当使用这个临时空间后,归并的过程就变的非常容易,如果 //没有这个临时空间的话,归并的过程便相对的比较难,这也是归并排 //序的一个缺点,它确实能把算法的复杂度提到 n*lgn 这个级别,但是 //它比插入排序、选择排序多使用了存储空间。它需要使用O(n)的额外 //空间来完成排序,只不过在现代计算机中,时间效率比空间效率重要 //的多 // // //具体要如何利用辅助空间把两个已经排好序的数组合并成一个排好序 //的数组呢?方法如下: //整体来讲,需要使用三个索引在数组内进行追踪,其中: //(1)一个表示最终在归并的过程中,需要跟踪的位置,(合并后) //(2)另外两个分别指向两个已经排好序的数组中,当前要考虑的元素(合并前) // //简言之,就是比较两个已经排好序的数组的当前数组头部的元素的大小, //谁更小,就放到最终归并的位置
#endif |
main.cpp:
#include "SortTestHelper.h" #include "MergeSort.h"
int main() {
int n = 1000000;
int* arr = SortTestHelper::generateRandomArray(n, 0, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr, n);
delete []arr;
system("pause"); return 0; } |
运行一览:
程序 2:自顶向下(递归)的归并排序法的优化
SortTestHelper.h:
#ifndef SORTTESTHELPER_H #define SORTTESTHELPER_H
#include <iostream> #include <string> #include <ctime> #include <cassert> using namespace std;
//辅助排序测试 namespace SortTestHelper {
//生成测试数据(测试用例),返回一个随机生成的数组: //生成有n个元素的随机数组,每个元素的随机范围为[rangeL,rangeR] int *generateRandomArray(int n, int rangeL, int rangeR) { //默认rangeL要小于等于rangeR assert(rangeL <= rangeR);
int *arr = new int[n];
//对于数组中的每一个元素,将之随机成为rangeL和rangeR之间的随机数 //先设置随机种子:这里将当前的时间作为种子来进行随机数的设置 srand(time(NULL));
for (int i = 0; i < n; i++) { //rand()函数+百分号+数的范围,即 取中间的一个随机整数,再加上rangeL即可 arr[i] = rand() % (rangeR - rangeL + 1) + rangeL; } return arr; }
//生成一个近乎有序的数组 int *generateNearlyOrderedArray(int n, int swapTimes) { //先生成完全有序的数组 int *arr = new int[n]; for (int i = 0; i < n; i++) { arr[i] = i; }
//以当前时间为随机种子 srand(time(NULL));
//再随机挑选几对元素进行交换,就是一个近乎有序的数组了 for (int i = 0; i < swapTimes; i++) { int posx = rand() % n; int posy = rand() % n; swap(arr[posx], arr[posy]); }
return arr; }
template<typename T> void printArray(T arr[], int n) { for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; }
//经过排序算法排序后,再次确认是否已经完全排序 template<typename T> bool isSorted(T arr[], int n) { for (int i = 0; i < n - 1; i++) { if (arr[i]>arr[i + 1]) { return false; } } return true; }
//衡量一个算法的性能如何,最简单的方式就是看这个算法在特定数据集上的执行时间 //(1)传入排序算法的名字,方便打印输出 //(2)传入排序算法本身,即函数指针 //(3)传入测试用例:数组和元素个数 template<typename T> void testSort(string sortName, void(*sort)(T[], int), T arr[], int n) { //在排序前后分别调用clock()函数 //时间差就是该排序算法执行的时钟周期的个数 clock_t startTime = clock(); sort(arr, n); clock_t endTime = clock();
assert(isSorted(arr, n));
//endTime 减去 startTime 转为double类型,除以 CLOCKS_PER_SEC,其中: // //CLOCKS_PER_SEC 是标准库中定义的一个宏,表示每一秒钟所运行的时钟周期 //的个数,而(endTime-startTime)返回的是运行了几个时钟周期 // //这样,最终的结果就是在这段时间中程序执行了多少秒 cout << sortName << ":" << double(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl; }
//复制数组 int *copyIntArray(int a[], int n) { int *arr = new int[n]; //copy()函数在std中: //第一个参数是原数组的头指针, //第二个参数是原数组的尾指针, //第三个参数是目的数组的头指针 // //注意:copy()函数运行时会报错,需要在: //项目->属性->配置属性->C/C++->预处理器->预处理器定义 //在其中添加:_SCL_SECURE_NO_WARNINGS copy(a, a + n, arr); return arr; } }
#endif |
InsertionSort.h:
#ifndef INSERTIONSORT_H #define INSERTIONSORT_H
//插入排序:从小到大进行排序 template<typename T> void insertionSort(T arr[], int n) { //对于插入排序来说,第一个元素(索引为0)根本不用考虑 // //因为在插入排序的初始,第一个元素放在那里,它本身就已经 //有序了,不需要再把它插入到前面的任何位置,所以从第二个 //元素(索引为1)开始考察 for (int i = 1; i < n; i++) { //寻找arr[i]合适的插入位置: //对已经排好序的部分,插入时从后向前比较, //最多只会考察到j=1时,因为是从后向前比较 // //先将要插入的元素arr[i]保存到e中,在和已经排好序的部分进行比较时: //(1)如果小于当前被比较的元素arr[j-1],则将当前被比较的元素向后移一位 //(2)如果大于当前被比较的元素arr[j-1],则当前位置j就是要插入的位置 T e = arr[i]; int j;//保存元素e应该插入的位置 for (j = i; j > 0 && arr[j - 1] > e; j--) { arr[j] = arr[j - 1]; } //将之前保存在e中的元素插入到位置j arr[j] = e;
} }
// 对arr[l...r]范围的数组进行插入排序 template<typename T> void insertionSort(T arr[], int l, int r) {
for (int i = l + 1; i <= r; i++) {
T e = arr[i]; int j; for (j = i; j > l && arr[j - 1] > e; j--) { arr[j] = arr[j - 1]; } arr[j] = e;
}
}
//插入排序是 O(n^2) 级别算法复杂度的排序算法,它的思想就类似于 //在玩扑克牌时插入牌的思想,即 将拿到的一张牌插入到合适的位置, //当最后一张牌也插入后,手中的整副扑克牌也就排序完成了 #endif |
MergeSort.h:
#ifndef MERGESORT_H #define MERGESORT_H
#include "InsertionSort.h"
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并 //注意边界问题,这里都是前闭后闭 template<typename T> void __merge(T arr[], int l, int mid, int r) {
//辅助空间或临时空间:aux数组,它的大小和传入进来的arr数组一般大 //(aux,即 auxiliary,辅助) // // 经测试,传递aux数组的性能效果并不好 T *aux = new T[r - l + 1];
//将arr数组中的所有元素全部复制到aux数组中 for (int i = l; i <= r; i++) { aux[i - l] = arr[i]; }
//具体的归并: //首先将两个索引i和j分别指向两个排好序的子数组 int i = l, j = mid + 1;
//再使用索引k进行遍历,决定合并后的arr[k]究竟是谁 for (int k = l; k <= r; k++) {
//如果i越界,即左边数组已排序完毕 //直接将arr[j-l]赋值给arr[k] if (i > mid) { arr[k] = aux[j - l]; j++; } //如果j越界,即右边数组已排序完毕 //直接将arr[i-l]赋值给arr[k] else if (j > r) { arr[k] = aux[i - l]; i++; } else if (aux[i - l] < aux[j - l]) { arr[k] = aux[i - l]; i++; } else { arr[k] = aux[j - l]; j++; } }
delete[]aux; }
// 递归使用归并排序,对arr[l...r]的范围进行排序: //(1)l 即 left,r 即 right,[l...r]是前闭后闭 //(2)l 是第一个元素的位置,r 是最后一个元素的位置 //(3)如果是[l...r),即前闭后开,那么 r 是最后一个元素的后一个位置 template<typename T> void __mergeSort(T arr[], int l, int r) {
// 对于小规模数组,使用插入排序 // //当递归到元素数量非常小的时候,可以转而使用插入排序,来提高一些性能 // //这是基于两个原因: //(1)当元素数据比较小的时候,整个数组近乎有序的概率就会比较大, //此时插入排序有优势 //(2)虽然插入排序最差的时间复杂度是O(n^2)级别的,而归并排序是O(n*lgn) //级别的,但是对于时间复杂度来说,前面是有一个常数系数的 //对于这个系数而言,插入排序要比归并排序小,换句话说,当n小到一定程度时, //插入排序会比归并排序要快一些 if (r - l <= 15){ insertionSort(arr, l, r); return; }
//注意:l+r 有隐含的危险,因为当l和r非常大时, //l+r 有可能会溢出 int mid = (l + r) / 2;
__mergeSort(arr, l, mid); __mergeSort(arr, mid + 1, r);
// 对于arr[mid]<= arr[mid+1]的情况,不进行merge操作 // // 这对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失 //因为if语句的判断本身也要消耗一定的性能,但总体影响不大 if (arr[mid] > arr[mid + 1]) { //merge操作(归并操作) __merge(arr, l, mid, r); }
}
//归并排序:从小到大进行排序(自顶向下、递归) template<typename T> void mergeSort(T arr[], int n) { //归并排序的本质是一次递归的排序过程,即 需要对数组 //的不同部分继续进行归并排序 // //所以需要一个子函数 __mergeSort(),它是一个私有函数, //被mergeSort()所调用(对用户来说,只需调用mergeSort()) // //参数包含数组,以及当前要处理的数组的开始位置和结束位置 __mergeSort(arr, 0, n - 1); }
//归并排序是O(n*lgn)级别的排序算法,它的思想是: // //当要排序一个数组时,首先把这个数组分成两半,想办法把左边和右边 //的数组给排序,然后再将它们归并起来 // //当对左边的数组和右边的数组进行排序的时候,先分别把左边的数组和 //右边的数组,分成两半,再对每一个部分排序,最后归并 // //这每一个部分,依然是先分半,再排序,最后归并 ... // //当分到一定细度的时候,每一部分就只有一个元素了,此时,不用排序, //它本身就是有序的,只需要简单的对它们进行一次归并即可 // //归并到上一层级之后,进一步进行归并,归并到更高的一个层级,逐层 //的向上上升 ... // //直至最后,归并到最后一层时,整个数组就全部有序了 // // // // //那么为什么要费这么大劲把数组先分半,之后再逐渐归并呢? // //假设一个数组有8个元素,进行归并排序,层层下来,就会分成3级, //到第3级的时候,每一部分只剩下一个元素 // //3级的由来: //总共有8个元素,每次二分,这样下来,经过3次除以2的计算,每一 //部分就只剩下一个元素,即 log8(以2为底) 等于 3 // // //如果有N个元素,就有log(N)的层级数,其中:如果N不是2的x次方, //则log(N)就可能是一个浮点数,取整即可 // //总之,层数是log(N)这样一个数量级的 // // //这种情况下,每一层要处理的元素的个数是一样的,虽然把每一层 //分成了不同的部分,此时,如果整个归并过程可以以O(n)的时间复 //杂度来解决的话,那么就设计出了一个O(n*lgn)级别的算法 // // //事实上,这也是 n*lgn 这个时间复杂度算法的一个主要来源: //通常是先通过二分法达到log(N)这样一个层级,然后每一层级用O(n) //级别的算法来做事情 // // //所以问题就变成了: //能不能将整个数组划分成两部分,两部分都分别排好序后,使用O(n) //的算法将它们归并到一起,形成一个新的有序的数组 // //那么当左右两部分都排好序时,如何把它们合成一个有序的数组呢? //实现这种算法时,不能像插入排序、选择排序那样直接在这个数组上 //通过交换位置来完成这个过程,而是要开辟一个同样大小的临时空间 //来辅助完成归并过程 // // //事实上,当使用这个临时空间后,归并的过程就变的非常容易,如果 //没有这个临时空间的话,归并的过程便相对的比较难,这也是归并排 //序的一个缺点,它确实能把算法的复杂度提到 n*lgn 这个级别,但是 //它比插入排序、选择排序多使用了存储空间。它需要使用O(n)的额外 //空间来完成排序,只不过在现代计算机中,时间效率比空间效率重要 //的多 // // //具体要如何利用辅助空间把两个已经排好序的数组合并成一个排好序 //的数组呢?方法如下: //整体来讲,需要使用三个索引在数组内进行追踪,其中: //(1)一个表示最终在归并的过程中,需要跟踪的位置(合并后) //(2)另外两个分别指向两个已经排好序的数组中,当前要考虑的元素(合并前) // //简言之,就是比较两个已经排好序的数组的当前数组头部的元素的大小, //谁更小,就放到最终归并的位置
#endif |
main.cpp:
#include "SortTestHelper.h" #include "MergeSort.h"
int main() {
int n = 1000000;
int* arr = SortTestHelper::generateRandomArray(n, 0, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr, n);
delete []arr;
system("pause"); return 0; } |
运行一览:
程序 3:自底向上(迭代)的归并排序法的实现
SortTestHelper.h:
#ifndef SORTTESTHELPER_H #define SORTTESTHELPER_H
#include <iostream> #include <string> #include <ctime> #include <cassert> using namespace std;
//辅助排序测试 namespace SortTestHelper {
//生成测试数据(测试用例),返回一个随机生成的数组: //生成有n个元素的随机数组,每个元素的随机范围为[rangeL,rangeR] int *generateRandomArray(int n, int rangeL, int rangeR) { //默认rangeL要小于等于rangeR assert(rangeL <= rangeR);
int *arr = new int[n];
//对于数组中的每一个元素,将之随机成为rangeL和rangeR之间的随机数 //先设置随机种子:这里将当前的时间作为种子来进行随机数的设置 srand(time(NULL));
for (int i = 0; i < n; i++) { //rand()函数+百分号+数的范围,即 取中间的一个随机整数,再加上rangeL即可 arr[i] = rand() % (rangeR - rangeL + 1) + rangeL; } return arr; }
//生成一个近乎有序的数组 int *generateNearlyOrderedArray(int n, int swapTimes) { //先生成完全有序的数组 int *arr = new int[n]; for (int i = 0; i < n; i++) { arr[i] = i; }
//以当前时间为随机种子 srand(time(NULL));
//再随机挑选几对元素进行交换,就是一个近乎有序的数组了 for (int i = 0; i < swapTimes; i++) { int posx = rand() % n; int posy = rand() % n; swap(arr[posx], arr[posy]); }
return arr; }
template<typename T> void printArray(T arr[], int n) { for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; }
//经过排序算法排序后,再次确认是否已经完全排序 template<typename T> bool isSorted(T arr[], int n) { for (int i = 0; i < n - 1; i++) { if (arr[i]>arr[i + 1]) { return false; } } return true; }
//衡量一个算法的性能如何,最简单的方式就是看这个算法在特定数据集上的执行时间 //(1)传入排序算法的名字,方便打印输出 //(2)传入排序算法本身,即函数指针 //(3)传入测试用例:数组和元素个数 template<typename T> void testSort(string sortName, void(*sort)(T[], int), T arr[], int n) { //在排序前后分别调用clock()函数 //时间差就是该排序算法执行的时钟周期的个数 clock_t startTime = clock(); sort(arr, n); clock_t endTime = clock();
assert(isSorted(arr, n));
//endTime 减去 startTime 转为double类型,除以 CLOCKS_PER_SEC,其中: // //CLOCKS_PER_SEC 是标准库中定义的一个宏,表示每一秒钟所运行的时钟周期 //的个数,而(endTime-startTime)返回的是运行了几个时钟周期 // //这样,最终的结果就是在这段时间中程序执行了多少秒 cout << sortName << ":" << double(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl; }
//复制数组 int *copyIntArray(int a[], int n) { int *arr = new int[n]; //copy()函数在std中: //第一个参数是原数组的头指针, //第二个参数是原数组的尾指针, //第三个参数是目的数组的头指针 // //注意:copy()函数运行时会报错,需要在: //项目->属性->配置属性->C/C++->预处理器->预处理器定义 //在其中添加:_SCL_SECURE_NO_WARNINGS copy(a, a + n, arr); return arr; } }
#endif |
MergeSort.h:
#ifndef MERGESORT_H #define MERGESORT_H
#include <algorithm>
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并 //注意边界问题,这里都是前闭后闭 template<typename T> void __merge(T arr[], int l, int mid, int r) {
//辅助空间或临时空间:aux数组,它的大小和传入进来的arr数组一般大 //(aux,即 auxiliary,辅助) // // 经测试,传递aux数组的性能效果并不好 T *aux = new T[r - l + 1];
//将arr数组中的所有元素全部复制到aux数组中 for (int i = l; i <= r; i++) { aux[i - l] = arr[i]; }
//具体的归并: //首先将两个索引i和j分别指向两个排好序的子数组 int i = l, j = mid + 1;
//再使用索引k进行遍历,决定合并后的arr[k]究竟是谁 for (int k = l; k <= r; k++) {
//如果i越界,即左边数组已排序完毕 //直接将arr[j-l]赋值给arr[k] if (i > mid) { arr[k] = aux[j - l]; j++; } //如果j越界,即右边数组已排序完毕 //直接将arr[i-l]赋值给arr[k] else if (j > r) { arr[k] = aux[i - l]; i++; } else if (aux[i - l] < aux[j - l]) { arr[k] = aux[i - l]; i++; } else { arr[k] = aux[j - l]; j++; } }
delete[]aux; }
//归并排序:从小到大进行排序(自底向上、迭代) //BU 即 BottomUp template <typename T> void mergeSortBU(T arr[], int n){
//对进行merge操作(归并操作)的元素个数进行遍历 // //for循环中:sz+=sz 表示从最开始,第一次归并排序只看一个元素, //之后看两个元素,四个元素,八个元素 ... for (int sz = 1; sz <= n; sz += sz) { //具体在每一次归并的过程中,起始的元素位置,开始是0, //之后进行 i+=sz+sz,这是因为每一次归并操作是跨越两 //个sz的,即 两个sz的元素进行一次归并操作 for (int i = 0; i + sz < n; i += sz + sz) { // 对 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 进行归并操作 // //注意:首先要保证 i<n,但i+sz-1和i+2*sz-1可能已经超过了数 //组的界限,所以要再加一些限制 // //(1) //对于归并过程来说,至少要对两部分进行归并排序,否则归并是 //没有意义的,因为归并过程就是把两个有序的部分合并成一个有 //序的部分,如果只有一部分,它本身就已经有序了,已经达到了 //运行完__merge()以后的要求 // //所以要限制 i+sz<n ,这一步保证了第二部分的存在,与此同时, //也保证了 i+sz-1 不会越界 // //(2) //在数组的末尾部分,有可能不足sz那么多个元素,即 i+2*sz-1 //有可能会越界,因此,这里取 i+2*sz-1 和 n-1 的最小值 // //min()函数需要include<algorithm> __merge(arr, i, i + sz - 1, min(i + sz + sz - 1, n - 1)); } } }
//前两个程序的归并排序都是采用自顶向下、逐步递归的方式来完成归并排序的 // //这里采用自底向上、逐步迭代的方式来完成归并排序,它的思想如下: //先从左到右依次把这个数组划分成小段,每个小段两个元素,然后进行归并操作, //当归并完一轮后,再四个元素一个小段的进行归并操作...最终完成整个归并排序 // // //从统计意义(理论)上讲,递归的自顶向下的归并排序,比迭代的自底向上的 //归并排序要快一些,但实际使用中,迭代的自底向上的归并排序,是比递归的 //自顶向下的归并排序要快的 //参考链接:coding.imooc.com/learn/questiondetail/3208.html // // //自底向上的归并排序过程中没有使用数组的一个非常重要的特性,就是通过索引 //直接获取数组元素,正因为如此,它有一个非常重要的作用,即 可以非常好的 //使用 n*lgn 的时间对链表这样的数据结构进行排序
#endif |
main.cpp:
#include "SortTestHelper.h" #include "MergeSort.h"
int main() {
int n = 1000000;
int *arr = SortTestHelper::generateRandomArray(n, 0, n);
SortTestHelper::testSort("Merge Sort Bottom Up", mergeSortBU, arr, n);
delete []arr;
system("pause"); return 0; } |
运行一览:
程序 4:自底向上(迭代)的归并排序法的优化
SortTestHelper.h:
#ifndef SORTTESTHELPER_H #define SORTTESTHELPER_H
#include <iostream> #include <string> #include <ctime> #include <cassert> using namespace std;
//辅助排序测试 namespace SortTestHelper {
//生成测试数据(测试用例),返回一个随机生成的数组: //生成有n个元素的随机数组,每个元素的随机范围为[rangeL,rangeR] int *generateRandomArray(int n, int rangeL, int rangeR) { //默认rangeL要小于等于rangeR assert(rangeL <= rangeR);
int *arr = new int[n];
//对于数组中的每一个元素,将之随机成为rangeL和rangeR之间的随机数 //先设置随机种子:这里将当前的时间作为种子来进行随机数的设置 srand(time(NULL));
for (int i = 0; i < n; i++) { //rand()函数+百分号+数的范围,即 取中间的一个随机整数,再加上rangeL即可 arr[i] = rand() % (rangeR - rangeL + 1) + rangeL; } return arr; }
//生成一个近乎有序的数组 int *generateNearlyOrderedArray(int n, int swapTimes) { //先生成完全有序的数组 int *arr = new int[n]; for (int i = 0; i < n; i++) { arr[i] = i; }
//以当前时间为随机种子 srand(time(NULL));
//再随机挑选几对元素进行交换,就是一个近乎有序的数组了 for (int i = 0; i < swapTimes; i++) { int posx = rand() % n; int posy = rand() % n; swap(arr[posx], arr[posy]); }
return arr; }
template<typename T> void printArray(T arr[], int n) { for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; }
//经过排序算法排序后,再次确认是否已经完全排序 template<typename T> bool isSorted(T arr[], int n) { for (int i = 0; i < n - 1; i++) { if (arr[i]>arr[i + 1]) { return false; } } return true; }
//衡量一个算法的性能如何,最简单的方式就是看这个算法在特定数据集上的执行时间 //(1)传入排序算法的名字,方便打印输出 //(2)传入排序算法本身,即函数指针 //(3)传入测试用例:数组和元素个数 template<typename T> void testSort(string sortName, void(*sort)(T[], int), T arr[], int n) { //在排序前后分别调用clock()函数 //时间差就是该排序算法执行的时钟周期的个数 clock_t startTime = clock(); sort(arr, n); clock_t endTime = clock();
assert(isSorted(arr, n));
//endTime 减去 startTime 转为double类型,除以 CLOCKS_PER_SEC,其中: // //CLOCKS_PER_SEC 是标准库中定义的一个宏,表示每一秒钟所运行的时钟周期 //的个数,而(endTime-startTime)返回的是运行了几个时钟周期 // //这样,最终的结果就是在这段时间中程序执行了多少秒 cout << sortName << ":" << double(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl; }
//复制数组 int *copyIntArray(int a[], int n) { int *arr = new int[n]; //copy()函数在std中: //第一个参数是原数组的头指针, //第二个参数是原数组的尾指针, //第三个参数是目的数组的头指针 // //注意:copy()函数运行时会报错,需要在: //项目->属性->配置属性->C/C++->预处理器->预处理器定义 //在其中添加:_SCL_SECURE_NO_WARNINGS copy(a, a + n, arr); return arr; } }
#endif |
InsertionSort.h:
#ifndef INSERTIONSORT_H #define INSERTIONSORT_H
//插入排序:从小到大进行排序 template<typename T> void insertionSort(T arr[], int n) { //对于插入排序来说,第一个元素(索引为0)根本不用考虑 // //因为在插入排序的初始,第一个元素放在那里,它本身就已经 //有序了,不需要再把它插入到前面的任何位置,所以从第二个 //元素(索引为1)开始考察 for (int i = 1; i < n; i++) { //寻找arr[i]合适的插入位置: //对已经排好序的部分,插入时从后向前比较, //最多只会考察到j=1时,因为是从后向前比较 // //先将要插入的元素arr[i]保存到e中,在和已经排好序的部分进行比较时: //(1)如果小于当前被比较的元素arr[j-1],则将当前被比较的元素向后移一位 //(2)如果大于当前被比较的元素arr[j-1],则当前位置j就是要插入的位置 T e = arr[i]; int j;//保存元素e应该插入的位置 for (j = i; j > 0 && arr[j - 1] > e; j--) { arr[j] = arr[j - 1]; } //将之前保存在e中的元素插入到位置j arr[j] = e;
} }
// 对arr[l...r]范围的数组进行插入排序 template<typename T> void insertionSort(T arr[], int l, int r) {
for (int i = l + 1; i <= r; i++) {
T e = arr[i]; int j; for (j = i; j > l && arr[j - 1] > e; j--) { arr[j] = arr[j - 1]; } arr[j] = e;
}
}
//插入排序是 O(n^2) 级别算法复杂度的排序算法,它的思想就类似于 //在玩扑克牌时插入牌的思想,即 将拿到的一张牌插入到合适的位置, //当最后一张牌也插入后,手中的整副扑克牌也就排序完成了
#endif |
MergeSort.h:
#ifndef MERGESORT_H #define MERGESORT_H
#include "InsertionSort.h" #include <algorithm>
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并 //注意边界问题,这里都是前闭后闭 template<typename T> void __merge(T arr[], int l, int mid, int r) {
//辅助空间或临时空间:aux数组,它的大小和传入进来的arr数组一般大 //(aux,即 auxiliary,辅助) // // 经测试,传递aux数组的性能效果并不好 T *aux = new T[r - l + 1];
//将arr数组中的所有元素全部复制到aux数组中 for (int i = l; i <= r; i++) { aux[i - l] = arr[i]; }
//具体的归并: //首先将两个索引i和j分别指向两个排好序的子数组 int i = l, j = mid + 1;
//再使用索引k进行遍历,决定合并后的arr[k]究竟是谁 for (int k = l; k <= r; k++) {
//如果i越界,即左边数组已排序完毕 //直接将arr[j-l]赋值给arr[k] if (i > mid) { arr[k] = aux[j - l]; j++; } //如果j越界,即右边数组已排序完毕 //直接将arr[i-l]赋值给arr[k] else if (j > r) { arr[k] = aux[i - l]; i++; } else if (aux[i - l] < aux[j - l]) { arr[k] = aux[i - l]; i++; } else { arr[k] = aux[j - l]; j++; } }
delete[]aux; }
//归并排序:从小到大进行排序(自底向上、迭代) //BU 即 BottomUp template <typename T> void mergeSortBU(T arr[], int n){
// 对于小规模数组,使用插入排序 //高级的排序算法到底层的时候,都可以采用插入排序的方法来进行优化 for (int i = 0; i < n; i += 16) { insertionSort(arr, i, min(i + 15, n - 1)); }
for (int sz = 16; sz <= n; sz += sz) { for (int i = 0; i < n - sz; i += sz + sz) {
// 对于arr[i + sz - 1]<=arr[i + sz]的情况,不进行merge操作 // // 这对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失 //因为if语句的判断本身也要消耗一定的性能,但总体影响不大 if (arr[i + sz - 1] > arr[i + sz]) { //min()函数需要include<algorithm> __merge(arr, i, i + sz - 1, min(i + sz + sz - 1, n - 1)); } } }
}
//前两个程序的归并排序都是采用自顶向下、逐步递归的方式来完成归并排序的 // //这里采用自底向上、逐步迭代的方式来完成归并排序,它的思想如下: //先从左到右依次把这个数组划分成小段,每个小段两个元素,然后进行归并操作, //当归并完一轮后,再四个元素一个小段的进行归并操作...最终完成整个归并排序 // // //从统计意义(理论)上讲,递归的自顶向下的归并排序,比迭代的自底向上的 //归并排序要快一些,但实际使用中,迭代的自底向上的归并排序,是比递归的 //自顶向下的归并排序要快的 //参考链接:coding.imooc.com/learn/questiondetail/3208.html // // //自底向上的归并排序过程中没有使用数组的一个非常重要的特性,就是通过索引 //直接获取数组元素,正因为如此,它有一个非常重要的作用,即 可以非常好的 //使用 n*lgn 的时间对链表这样的数据结构进行排序
#endif |
main.cpp:
#include "SortTestHelper.h" #include "MergeSort.h"
int main() {
int n = 1000000;
int* arr = SortTestHelper::generateRandomArray(n, 0, n);
SortTestHelper::testSort("Merge Sort Bottom Up", mergeSortBU, arr, n);
delete []arr;
system("pause"); return 0; } |
运行一览:
【made by siwuxie095】
归并排序