1、什么是归并排序?
归并排序是利用递归和分而治之的技术将数据序列划分成为越来越小的序列,将两个(或两个以上)有序子序列合并成一个新的有序序列,即把待排序序列分为若干个子序列,每个子序列是有序的,然后再把有序子序列合并为一个新的有序序列,最终将整个序列变得有序。
时间复杂度: O(nlogn)
2、效果演示
这个序列为待排序序列
将这个序列分成2个子序列,分别对它们进行排序,再合并为一个新的有序序列
同理在对上面的两个子序列排序的时候,也会将他们各自分为2个子序列,分别
对它们进行排序,再合并
继续沿用上面的思想,再对它们进行分半,此时每一个子序列只有一个元素了,
他们就是有序的
所以我们此时要做的就是向上合并
当排序合并到最后一层的时候,整个序列就变得有序了,至此排序完毕
3、动画演示
4、算法的实现(基于C++)
程序中使用了3个变量分别指向如下所示的位置
红色的箭头表示最终在归并的过程中需要跟踪的位置,两个蓝色的箭头表示已经排好序的两个子序列中当前需要考虑的元素
1 /************************************* 归并排序算法实现 **********************************/ 2 /* 将arr[left, mid]和arr[mid+1, right]两部分进行归并 */ 3 template<typename T> 4 void __merge (T arr[], int left, int mid, int right) 5 { 6 T *temp = new T[right - left + 1]; // 申请分配堆内存(这个地方还是建议不要使用栈,因为如果数据量太大可能会导致栈溢出) 7 if (NULL == temp) { // 判断申请是否成功 8 return; 9 } 10 11 for (int i = left; i <= right; i++) { // 给临时空间赋值(注意他们之间的一个偏移量) 12 temp[i - left] = arr[i]; 13 } 14 15 /* 以下就是对两个子序列进行向上归并的操作 */ 16 int i = left, j = mid + 1; // i指向第一部分的起始位置,j指向第二部分的起始位置 17 for (int k = left; k <= right; k++) { // k指向当前需要排序的位置 18 if (i > mid) { // 判断第一部分中的数据是否已经全部归位了 19 arr[k] = temp[j - left]; 20 j++; 21 } 22 else if (j > right) { // 判断第二部分中的数据是否已经全部归位了 23 arr[k] = temp[i - left]; 24 i++; 25 } 26 else if (temp[i - left] > temp[j - left]) {// 如果第二部分中对应的数据更小,则将它放在需要排序的位置中 27 arr[k] = temp[j - left]; 28 j++; 29 } 30 else { 31 arr[k] = temp[i - left]; // 如果是第一部分中对应的数据更小,则将它放在需要排序的位置中 32 i++; 33 } 34 } 35 36 delete[] temp; // 释放堆内存空间 37 } 38 39 /* 对arr[left, right]进行直接插入排序 */ 40 template<typename T> 41 void __insertSortMG (T arr[], int left, int right) 42 { 43 for (int i = left + 1; i <= right; i++) { 44 int j; 45 T temp = arr[i]; 46 47 for (j = i; j > left && arr[j - 1] > temp; j--) { 48 arr[j] = arr[j - 1]; 49 } 50 51 arr[j] = temp; 52 } 53 } 54 55 /* 递归使用归并排序,对arr[left....right]范围进行排序 */ 56 template<typename T> 57 void __mergeSort (T arr[], int left, int right) 58 { 59 int mid = (left + right) / 2; // 找出两部分的分界点位置 60 61 __mergeSort<T>(arr, left, mid); // 对第一部分子序列递归调用该函数 62 __mergeSort<T>(arr, mid + 1, right); // 对第二部分子序列递归调用该函数 63 64 __merge<T>(arr, left, mid, right); // 对两部分数据进行归并操作 65 } 66 67 template<typename T> 68 void mergeSort (T arr[], int count) 69 { 70 __mergeSort<T>(arr, 0, count - 1); // 对arr[0...count-1]范围进行归并排序 71 } 72 /******************************************************************************************/
5、算法的性能测试
将归并排序算法与前面讲的两种排序算法进行一个时间上的测试:
测试数据量50000:
测试数据量100000:
6、归并排序算法的优化
(1)第一种优化方法(在__mergeSort函数中进行优化):
1 /* 递归使用归并排序,对arr[left....right]范围进行排序 */ 2 template<typename T> 3 void __mergeSort (T arr[], int left, int right) 4 { 5 if (left >= right) { // 递归终止的判断条件 6 return; 7 } 8 9 int mid = (left + right) / 2; // 将数组分成2部分的中间索引值 10 11 __mergeSort<T>(arr, left, mid); // 对第一部分子序列递归调用该函数 12 __mergeSort<T>(arr, mid + 1, right); // 对第二部分子序列递归调用该函数 13 14 if (arr[mid] > arr[mid + 1]) { // 优化措施1: 适用于本身有序程度很高的序列 15 __merge<T>(arr, left, mid, right); // 将两部分数据进行归并操作 16 } 17 18 }
我们需要做的就是在调用__merge函数之前进行一个判断,判断第二个子序列的首元素是否大于第
一个子序列的最后一个元素,如果确实要大或者相等,则不需要调用__merge函数来进行后续的操
作了,因为这两个子序列合在一起本身就是一个有序的序列了,这个特性归结于归并排序本身的各个
子序列已经就是有序的状态了、如此一来就可以省去一定的时间开销,尤其是在序列本身的有序程度
高的时候,这个效果越能够体现出来。需要说明的是,如果你的数组序列本身有序程度非常低,建议
就不要加这个优化,毕竟条件判断也是需要消耗一定的时间的。
(2)第二种优化方法(在__merge函数中进行优化)
1 /* 对arr[left, right]进行直接插入排序 */ 2 template<typename T> 3 void __insertSortMG(T arr[], int left, int right) 4 { 5 for (int i = left + 1; i <= right; i++) { 6 int j; 7 T temp = arr[i]; 8 9 for (j = i; j > left && arr[j - 1] > temp; j--) { 10 arr[j] = arr[j - 1]; 11 } 12 13 arr[j] = temp; 14 } 15 } 16 17 /* 递归使用归并排序,对arr[left....right]范围进行排序 */ 18 template<typename T> 19 void __mergeSort (T arr[], int left, int right) 20 { 21 if (right - left <= 40) { // 优化措施2: 对小数据量排序使用直接插入排序的方法 22 __insertSortMG<T>(arr, left, right); 23 return; 24 } 25 26 int mid = (left + right) / 2; // 将数组分成2部分的中间索引值 27 28 __mergeSort<T>(arr, left, mid); // 对第一部分子序列递归调用该函数 29 __mergeSort<T>(arr, mid + 1, right); // 对第二部分子序列递归调用该函数 30 31 __merge<T>(arr, left, mid, right); // 将两部分数据进行归并操作 32 }
为什么需要这么做,理由有2个:
a、当数据量比较小的时候,序列的有序性强,那么使用插入排序会有优势。
b、虽然插入排序的时间复杂度是O(n^2),而归并排序的时间复杂度是O(nlogn),但是前面是存在
一个系数的,而插入排序的系数其实是要小于归并排序的系数,当在数据量较小的时候,往往整体的
时间复杂度还是插入排序的小。这种优化方法几乎在所有的高级算法中都是可以使用的。那我们再来
看看,使用了优化方式2之后的性能指示:
测试数据量50000:
测试数据量100000:
至于效果有多大,大家与上面的比较就能知道了,好了今天的归并排序暂时先告一段落了。