高级排序问题 [part 1]
●归并排序法
●归并排序法的递归实现(自顶向下)
●归并排序法的优化
●归并排序法的非递归实现(自底向上)
1 归并排序法
1.1 原理:
首先将数组分成左右两个部分,考虑将左边和右边的数组分别排序,再归并在一起,递归使用该操作
1.2 时间复杂度
使用二分法,达到O(logn)的层级,每个层级使用O(n)时间复杂度的算法进行操作,最终的时间复杂度为O(nlogn)
1.3 O(nlogn)比O(n^2)快多少
n^2 | nlogn | 倍数 | |
n = 10 | 100 | 33 | 3 |
n = 100 | 10000 | 664 | 15 |
n = 1000 | 10^6 | 9966 | 100 |
n = 10000 | 10^8 | 132877 | 753 |
n = 100000 | 10^10 | 1660964 | 6020 |
2 归并排序法的递归实现(自顶向下)
给出一个左右两部分都排好序的数组,如何合成一个有序数组?
不能使用交换操作完成排序,而是需要开辟一个同样大小的数组空间,辅助完成归并过程(空间复杂度:O(n))
使用三个索引在数组内进行追踪,蓝色箭头表示最终需要归并的位置,两个箭头分别指向两个已经排好序的数组
template<typename T>
void __merge(T arr[], int l, int mid, int r){
T aux[r-l+1];//辅助空间,大小为r-l+1,这是因为l和r都是闭区间
for(int i = l; i <= r; i++){//赋值原数组
aux[i-l] = arr[i];//i和实际存储的位置有一个大小为l偏移量
}
int i = l, j = mid + 1; //设置两个索引指向两个已经排好序的子数组,注意这里的i和上面的i意义已经不同了
for(int k = l; k <= r; k++){//k为指向最终合并数组的指针,每次选取i,j指针指向的数字中较小的数,赋值到k指针位置
//数组越界问题:在某些情况下,i已经到达了左边数组的最大边界,而此时k还没有遍历完,那么直接把j指针指向的数赋值到k指针位置
if(i > mid){
arr[k] = aux[j-l];
j++;
}
else if(j > r){
arr[k] = aux[i-l];
i++;
}
//只有在满足数组不越界的前提条件下,再执行两指针比较大小的操作
else if(aux[i-l] < aux[j-l]){//同样需要注意偏移量l
aux[k] = aux[i-l];
i++;
}
else{
aux[k] = aux[j-l];
j++;
}
}
}
//递归使用归并排序,对arr[l...r]的范围进行排序
template<typename T>
void __mergeSort(T arr[], int l, int r){
// if(l >= r)
// return;
if(r - l <= 15){//优化: 使用插入排序来提高性能
insertionSort(arr, l, r);
return;
}
int mid = l + (r-l)/2;//防止整型溢出问题
__mergeSort(arr, l, mid);//分别对左右两边进行排序,注意边界问题
__mergeSort(arr, mid+1, r);
if(arr[mid] > arr[mid+1])//优化:当arr[mid] > arr[mid+1]时才进行merge操作,避免了对已经有序的数组再次进行比较
__merge(arr, l, mid, r);//将排序好的左右两边进行merge归并操作
}
template<typename T>
void mergeSort(T arr[], int n){
__mergeSort(arr, 0, n-1);//子函数 注:以“__”开头的函数,表示私有函数,不应被用户调用,只是作为一个子函数存在
}
3 归并排序法的优化
3.1 对于近乎有序数组的排序过程进行优化
基础的归并排序算法中,再对左右数组递归进行了归并排序之后,不论顺序如何,都直接进行__meger()操作。但当arr[mid] <= arr[mid+1]时,代表整个arr数组已经有序了,这时候就不需要进行merge操作
void __mergeSort(T arr[], int l, int r){
...
int mid = l + (r-l)/2;//防止整型溢出问题
__mergeSort(arr, l, mid);//分别对左右两边进行排序,注意边界问题
__mergeSort(arr, mid+1, r);
if(arr[mid] > arr[mid+1])//优化:当arr[mid] > arr[mid+1]时才进行merge操作,避免了对已经有序的数组再次进行比较
__merge(arr, l, mid, r);//将排序好的左右两边进行merge归并操作
}
3.2 对于小数组使用插入排序
基础的归并排序算法中,递归到底的条件是数组中只剩下一个元素时返回。而当递归到元素数量很小的时候,可以转而使用插入排序来提高性能。这是因为元素数量较少时,近乎有序的概率就会增大,此时插入排序具有时间复杂度上的优势。虽然插入排序最差的时间复杂度是O(n^2),而归并排序是O(nlogn),但是对于任何的时间复杂度而言,前面都包含一个常数,而在插入排序中,时间复杂度前面的常数会小于归并排序,这就导致了在n比较小时,插入排序的速度会快于归并排序
void __mergeSort(T arr[], int l, int r){
// if(l >= r)
// return;
if(r - l <= 15){//优化: 使用插入排序来提高性能
insertionSort(arr, l, r);
return;
}
...
}
4 归并排序法的非递归实现(自底向上)
4.1 非递归实现算法思想
第一层循环对merge的元素个数size进行遍历,size从1开始,每次循环增长2倍
第二层循环为每一轮归并的过程中起始的元素个数(从零开始)
eg:第一轮对[0, sz-1]和[sz, 2*sz-1]进行归并,第二轮对 [2*sz, 3*sz-1]和[3*sz, 4sz-1]进行归并
4.2 越界问题
在第二层循环中,保证了i<n,但是i+sz-1和i+sz+sz-1可能会超过arr[]数组的界限,为此需要增加一些限制。
第一个越界问题:首先对于归并过程来说,需要将两个已经有序的数组合并成一个有序的数组,那么至少需要对两部分进行归并,如果只对一部分进行归并,那么归并是没有意义的(单部分已经有序),所以增加限制:i+sz < n,这一步保证了第二部分数组的存在,与此同时也保证了i+sz-1不会越界
第二个越界问题:在第二部分数组中,有可能出现元素个数不足的情况,即i+sz+sz-1越界,因此取min(i+sz+sz-1, n-1)
template <typename T>
void mergeSortBU(T arr[], int n){
for( int sz = 1; sz <= n ; sz += sz )
for( int i = 0 ; i < n ; i += sz+sz )
// 对 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 进行归并
__merge(arr, i, i+sz-1, min(i+sz+sz-1,n-1) );
}
同样可以使用上面提到的优化算法
template <typename T>
void mergeSortBU(T arr[], int n){
// Merge Sort Bottom Up 优化
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 )
if( arr[i+sz-1] > arr[i+sz] )
__merge(arr, i, i+sz-1, min(i+sz+sz-1,n-1) );
}
4.3 重要作用
自底向上的归并排序有一个十分重要的作用:在该排序过程中,没有使用到数组的重要特性——使用索引直接获取元素,正因为如此,这样的一个自底向上的归并排序算法,可以非常好地使用O(nlogn)时间对链表这样的数据结构进行排序
【Data Structure】高级排序问题 [part 2]