CLRS: 将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。
归并排序利用了分治法的思想,对数组元素进行切分,递归地排序左右部分数组,然后再将两部分进行合并,最终达到排序的目的。此算法的时间复杂度是O(nlgn)。
此算法的大体思路为:
- 将待排序的数组划分为左右两个部分,分别递归地进行归并排序
- 当划分到只有一个元素时,返回
- 对划分的两个部分进行合并,即分别从已排序的两个部分中依次取最小值,放入原数组
有可能某个部分的值先被放入原数组,因此需要判断,当某个部分被取完时,另一个部分直接将剩下的元素放入原数组。但是如果每次循环都判断是否有部分被取完,会影响效率。因此可在两个部分的末尾添加哨兵元素,用于标记,阻绝再从此部分中取走元素。
具体做法:
复制左右两部分的元素到L[]和R[]中,L和R的长度需要至少比左右部分的元素长度大1。然后再将有效值后一个元素设置为哨兵值,如10000,保证此数大于待排序的所有数。即假如左部分中有已排序元素1 2 3,则L长度至少为4,元素值分别为1 2 3 10000。当某一个部分元素取完时,即L中有效的原始取完时,此时L中元素再与R中进行比较,则必然有10000>R[j],因此会选取R中有效元素放入原数组。最后当L和R中所有有效元素都取完时,循环停止,L和R中的10000不会进入到原数组,而破坏数据。
实现如下:
#include<iostream>
using namespace std;
//对由mid切分的左右块(low~mid和mid+1~high)进行合并
void merge(int* arr,int low,int mid,int high){
int m=mid-low+1; //左边块长度
int n=high-mid; //右边块长度
//左边块的容器。因为C++需要在编译时知道数组长度,因此不能L[m],故设置为100。可根据需要自行设定
int L[100]={0};
int R[100]={0}; //右边块的容器
for(int i=0;i<m;i++)
L[i]=arr[low+i]; //复制左边块
for(int j=0;j<n;j++)
R[j]=arr[mid+j+1]; //复制右边块
L[m]=10000; //哨兵,避免每次循环都判断是否L或R已空
R[n]=10000;
int i=0; //左边块的索引
int j=0; //右边块的索引
//对左右块的“首”元素循环判断,并将小的值添加到目的数组arr中
for(int k=low;k<=high;k++){
if(L[i]<=R[j]){
arr[k]=L[i];
i++; //添加的是左边块的“首”元素,需要索引i+1,以指向左边块下一个元素
}
else{
arr[k]=R[j];
j++; //同上
}
}
}
//归并排序,将数组中下标从low到high(包括)的元素进行排序
void merge_sort(int* arr,int low,int high){
if(low<high){ //当元素至少还有两个时,进行递归归并排序
int mid=(low+high)/2; //获取中间索引
merge_sort(arr,low,mid); //递归排序左部分
merge_sort(arr,mid+1,high); //递归排序右部分
merge(arr,low,mid,high); //合并两个部分
}
//low==high,则只有一个元素,不进行排序,直接返回
//low>high,错误
}
int main(){
int arr[10]={5,4,3,2,1,8,7,6,5,4};
merge_sort(arr,0,9);
for(int i=0;i<10;i++)
cout<<arr[i]<<" ";
cout<<endl;
return 0;
}
RESULT:
1 2 3 4 4 5 5 6 7 8