归并排序分为自顶向下(递归),和自底向上两种方法。在开始正文之前,先来简单说明一下递归归并排序的基本步骤。
简单的说,归并排序只需要四个步骤:
1)以数组中间位置为标杆,将数组分成左右两个子数组。
2)对左子数组进行归并排序。
3)对右子数组进行归并排序。
4)对分别有序的左右子数组进行一次归并操作,归并成一个数组。
下面用一个简单的例子来进行说明:
假设要对数组【 3, 1, 7, 5, 4, 8, 2, 6 】进行归并排序。
1)找到数组中间位置,即5所在位置,中间位置以左(包括5)为左子数组,即【3,1,7,5】,中间位置右边为右子数组,即【4,8,2,6】
2)对左子数组进行归并排序,结果是【1,3,5,7】
3)对右子数组进行归并排序,结果是【2,4,6,8】
4)对左右子数组进行一次归并操作,归并成一个数组,即【1,2,3,4,5,6,7,8】
根据上面的步骤,我们很容易可以写出如下的递归代码:
//对区间[l,r]的数组进行归并排序
template<typename T>
void __mergeSort(T arr[], int l, int r)
{
//找到数组中间位置,以此为标杆,划分左右子数组
int mid = ( l + r ) / 2;
//对左子数组进行归并排序
__mergeSort(arr, l, mid);
//对右子数组进行归并排序
__mergeSort(arr, mid+1, r);
//对左右子数组进行一次归并操作
__merge(arr, l, mid, r);
}
既然是递归方法,那么递归的终止条件应该是什么呢?
我们先来分析一下递归的过程:
我们在归并排序的过程中,每次将待排序的数组进行一分为二,直到最后子数组只有一个元素,此时一个只有一个元素的子数组显然已经有序,不需要再进行下面的操作,也就是说已经递归到底了,只需简单返回就行。
递归终止条件如下:
//对区间[l,r]的数组进行归并排序
template<typename T>
void __mergeSort(T arr[], int l, int r)
{
//递归终止条件,即只有一个元素时直接返回
if( l == r )
return;
//找到数组中间位置,以此为标杆,划分左右子数组
int mid = ( l + r ) / 2;
//对左子数组进行归并排序
__mergeSort(arr, l, mid);
//对右子数组进行归并排序
__mergeSort(arr, mid+1, r);
//对左右子数组进行一次归并操作
__merge(arr, l, mid, r);
}
接下来的任务就是实现归并的过程了。
从下往上看,先以2个元素为一组,将2个只有一个元素的子数组归并为一个有2个元素的数组,一共得到4个有2个元素的子数组,接着再以4个元素为一组,将4个有2个元素的子数组归并为2个有4个元素的子数组,最后,以8个元素为一组,进行最后一次归并,将2个4元素的子数组归并成一个8元素的数组。
具体的归并实现过程如下:
上图中,我们使用了一个名为aux的辅助数组,先将原数组arr的元素逐个复制过去,然后在aux数组中比较aux[i]和aux[j]的元素大小,每一轮中,将较小的那个值赋值回原数组arr中,将指向较小元素的那个指针向右移动一个单位(如上图的i指针),并将指向原数组的k指针先后移动一个单位,以此类推,最后会出现以下两种情况中的一种:
1)左子数组遍历完,即i指针超过了mid,而j还没有超过r,此时只需要将右子数组逐个复制回arr数组即可。
2)右子数组遍历完,即j指针超过r,但是i指针还没有超过mid,此时只需将左子数组逐个复制回arr数组即可。
归并过程的代码如下:
//将arr[l,mid] 和arr[mid+1,r]进行归并
template<typename T>
void __merge(T arr[], int l, int mid, int r)
{
//临时辅助数组
T aux[r-l+1];
//将原数组的对应元素复制到辅助数组中
for( int i = l; i <= r; i++ ){
aux[i-l] = arr[i];
}
int i = l;
int 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++;
}
}
}
递归归并排序到这里就基本完成了。下面来进行两种优化方案。
1)当要排序的元素个数比较少时,使用插入排序代替归并排序。我们知道,插入排序虽然是O(n^2)的排序算法,但是它前面的系数要比归并排序前面的系数小得多,即若插入排序为O(a*n^2),归并排序为O(b*nlogn),此时有a < b,当n的数值比较小时,插入排序要比归并排序更快。但是这种优化虽然会提高该算法的性能,但是并非数量级的优化,优化的效果也比较有限。优化代码如下:
//对区间[l,r]的数组进行归并排序
template<typename T>
void __mergeSort(T arr[], int l, int r)
{
//当元素个数小于等于15时,使用插入排序
if( r -l <= 15 ){
insertionSort(arr,l,r);
return;
}
int mid = ( l + r ) / 2;
__mergeSort(arr, l, mid);
__mergeSort(arr, mid+1, r);
__merge(arr, l, mid, r);
}
2)当数组已经有序时,不需要再进行归并操作,举个例子,若左子数组为【1,2,3,4】,右子数组为【5,6,7,8】此时整个数组已经有序,不需要再进行归并操作。即只有当arr[mid] > arr[mid+1]时才进行归并操作,优化代码如下:
//对区间[l,r]的数组进行归并排序
template<typename T>
void __mergeSort(T arr[], int l, int r)
{
////当元素个数小于等于15时,使用插入排序
if( r -l <= 15 ){
insertionSort(arr,l,r);
return;
}
int mid = ( l + r ) / 2;
__mergeSort(arr, l, mid);
__mergeSort(arr, mid+1, r);
//如果数组已经有序,则不需要进行归并操作
if( arr[mid] > arr[mid+1] )
__merge(arr, l, mid, r);
}
最后,再来简单说一下自底向上的归并排序。自底向上的归并排序反而比较简单,参考上面的第二张图,从下往上的归并过程,归并的大小为1,2,4,8…下一轮的size为上一轮的两倍,如此递推下去,直到完成整个归并的过程,代码如下:
template<typename T>
void mergeSortBU(T arr[], int n)
{
for(int sz = 1; sz <= n; sz += sz)
{
//对[i...i+sz-1]和[i+sz, i+sz+sz-1]进行归并
for(int i = 0; i+sz < n; i += sz + sz)
{
if(arr[i+sz-1] > arr[i+sz])
__merge(arr , i , i+sz-1 , min(i+sz+sz-1,n-1));
}
}
}
上面的代码有一些需要注意的地方。
1)必须确保右子数组存在才进行归并,即循环判断中的i+sz < n。
2)必须确保归并在整个数组的有效范围中进行,因为i+sz+sz-1可能已经超过了n-1。即为代码中的__merge(arr , i , i+sz-1 , min(i+sz+sz-1,n-1));
到此,归并排序的基本知识基本上都介绍完了,希望能够对看到这篇文章的你有所帮助。