归并排序是一个复杂度为O(nlogn)的算法,一般出现复杂度有logn的情况,都是采用了一种类似树形结构的方法。
原理
归并排序将数据如下图不断二分,如果分开的部分已经排好序,那么只需要将这两个部分合并即可。
但是如果分到最后一层,那么只有一个数据就是排好序的,将所有的数据向上归并即可。
分完层之后就是对数据进行归并处理,这个问题看起来很简单,但是细细想来还挺复杂的。因为只对原数组进行操作并不能达到目的,此时需要另外开辟一个空间进行辅助操作。这也就是说插入排序需要额外使用O(n)的空间复杂度。
开辟了额外的空间后,需要三个指针i,j,k,k指向原数组,i和j分别指向一分为二的数组。每次对比 i 和 j 的大小,将小的那一个放入到 k 的位置,并且相应的索引向后移动。
为了能够让索引指向正确的位置,我们还需要对数组进行额外的定义。首先将分层数组的索引范围定义为 [left , right], 注意这是一个左闭右闭的区间。其次分层后左边数组最右边的索引定义为middle,此时两个数组的索引分别为 [left , middle] ,[middle+1 , right]。
实现
在实现归并排序时,思考一下解决逻辑可以发现,最后要完成整个数组的排序,需要将原数组一分为二进行排序。而第二层数组需要排序则需要将第二层数组分别一分为二排序。那么这里就可以使用递归解决该问题:先将数组分层,然后将分层排好序后归并。代码如下(建议从下至上看,子函数在上面)
template <typename T>
void __merge(T arr[], int l, int mid, int r){
T *aux = new T[r-l+1];
for(int i=l; i<=r; i++) //注意是<=,因为数组区间为[l,r]
aux[i-l] = arr[i]; //aux与arr数组索引之间有l的偏移量
int i=l, j=mid+1;
for(int k=l; k<=r; 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]){
arr[k] = aux[i-l];
i++;
}
else{
arr[k] = aux[j-l];
j++;
}
}
delete[] aux;
}
template <typename T>
void __mergeSort(T arr[] , int l , int r){ //对[l,r]的数组归并排序
if(l>=r) //当前数组只有一个元素,无需分层
return;
int mid = (r-l)/2 + l; //可防止溢出
__mergeSort(arr,l,mid);
__mergeSort(arr,mid+1,r);
__merge(arr,l,mid,r); //归并操作
}
template <typename T>
void mergeSort(T arr[] , int n){
__mergeSort(arr , 0 , n-1);
}
时间复杂度
可以看出一个长度为n的数组,将它按照二分法分层最多能分为 log 2 n \log_{2}n log2n层,在进行归并排序时,需要对原数组进行遍历赋值,时间复杂度为 n n n,所以整个排序的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
对归并算法的改进
通过算法模拟可以发现对于几乎有序的数组,插入排序比归并排序运行速度更快,这是因为对于几乎有序的数组,插入排序可以退化为 O ( n ) O(n) O(n)级别。因为归并算法对于有序数组仍然进行归并操作,但是对于有序数组是不用归并的。此时只需要判断mid中的元素是否小于mid+1,如果小于说明数组有序,就不用进行归并。
template <typename T>
void __mergeSort(T arr[] , int l , int r){ //对[l,r]的数组归并排序
if(l>=r) //当前数组只有一个元素,无需分层
return;
int mid = (r-l)/2 + l; //可防止溢出
__mergeSort(arr,l,mid);
__mergeSort(arr,mid+1,r);
if(arr[mid] > arr[mid+1])
__merge(arr,l,mid,r); //归并操作
}
此外插入算法虽然是 O ( n 2 ) O(n^2) O(n2)的算法,但是常数项比归并算法小,所以在数据量较小时的运算速度比归并算法要快。那么在一个数组只有少数成员时可以采用插入算法。
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;
}
}
template <typename T>
void __mergeSort(T arr[] , int l , int r){ //对[l,r]的数组归并排序
if(r-l<=15)
insertionSort(arr,l,r);
int mid = (r-l)/2 + l; //可防止溢出
__mergeSort(arr,l,mid);
__mergeSort(arr,mid+1,r);
if(arr[mid] > arr[mid+1])
__merge(arr,l,mid,r); //归并操作
}
自底向上的归并排序
上面讲的是采用自顶向下的,将原数组不断二分——子数组排序——归并。我们也可以采用自底向上的归并方法。将数组先一个一组归并,再两个一组归并,一直到归并完成。
在整个过程中依然需要归并这个操作,只是将递归形式换成了迭代。
实现
template <typename T>
void mergeSortBU(T arr[], int n){
for(int size=1; size<=n; size += size){
for(int i=0; i+size<n; i += size + size){
//归并必须保证两个数组存在,所以第二个数组左边索引必须小于n
//对arr[i,i+size-1]和arr[i+size,i+2*size-1]归并
__merge(arr, i, i+size-1, min(i+size+size-1,n-1));
//i+2*size-1可能越界,即第二个数组数据不足
}
}
}
参考:https://coding.imooc.com/learn/list/71.html