归并排序又叫合并排序,是用分治策略对n个元素进行排序的算法。
将待排序的元素分成大小大致相同的两个子集合(这两个子集合都是有序的),对两个子集合经行排序,最终将排好序的子集合合并成为所需要的排好序的集合
该排序最主要的为合并left-mid,mid+1-right两个子集合的合并算法
void Merge(int* dest, int* src, int left, int mid, int right)
{
int i = left, j = mid + 1;//i是左子集合的第一个元素下标,j是右子集合的第一个
int k = left;//k是目标集合的下标
while (i <= mid && j <= right)
{
dest[k++] = src[i] < src[j] ? src[i++] : src[j++];
}
while (i <= mid)//左子集合没有遍历完
{
dest[k++] = src[i++];
}
while (j <= right)//右没有遍历完
{
dest[k++] = src[j++];
}
}
得到一个乱序数组时,先分再排——一分为二,分到只有一个数时为有序,因为合并是合的“有序”数组,所以这时候就可以递归的使用合并算法进行排序。
递归分解
void MergePass(int* tmp,int* nums, int left, int right)
{
if (left < right)
{
int mid = left+ ((right - left) >> 1);
MergePass(tmp, nums, left, mid);//分左边
MergePass(tmp, nums, mid + 1, right);//分右边
Merge(tmp, nums,left,mid,right);//分完有序了合并
Copy(nums, tmp, left, right);
}
}
完整代码
void Merge(int* dest, int* src, int left, int mid, int right)
{
int i = left, j = mid + 1;//i是左子集合的第一个元素下标,j是右子集合的第一个
int k = left;//k是目标集合的下标
while (i <= mid && j <= right)
{
dest[k++] = src[i] < src[j] ? src[i++] : src[j++];
}
while (i <= mid)//左子集合没有遍历完
{
dest[k++] = src[i++];
}
while (j <= right)//右没有遍历完
{
dest[k++] = src[j++];
}
}
void Copy(int* dest, int* src, int left, int right)
{
for (int i = left; i <= right; i++)
{
dest[i] = src[i];
}
}
void MergePass(int* tmp,int* nums, int left, int right)
{
if (left < right)
{
int mid = left+ ((right - left) >> 1);
MergePass(tmp, nums, left, mid);//分左边
MergePass(tmp, nums, mid + 1, right);//分右边
Merge(tmp, nums,left,mid,right);//分完有序了合并
Copy(nums, tmp, left, right);
}
}
void MergeSort(int* nums, int n)
{
if (n < 2 || nums == nullptr)return;
int* tmp = new int[n];
MergePass(tmp,nums, 0, n-1);
delete[]tmp;
}
非递归(自下而上)
递归和非递归的区别在于分解的函数和排序函数。
非递归时第一轮传入s=1,将nums里的数组分为一个数一块,利用归并函数归并,i控制归并的范围,如下图:56和23归并,50和45归并,在while中将所有数都归并到tmp中时,退出MergePass分解函数;因为上一轮合并了两个元素为一组有序子集合,第二轮开始s = 2,将tmp中的数组再用分解、合并函数,之后倒回nums中,以此循环,直到所有数都有序。
分解函数中使用i保存两个需要归并的子集合中,左子集开始位置的下标,i每次往前移动2*s-1(s:每个子集的数据个数)到下一对需要合并的子集开始位置。
合并到最后时不一定是2^n(左右子集合数量相对)个,不能再用之前的i控制的范围(第六行)合并,while时注意i位置判断i+2*s-1<=n-1时才能一对相同长度子数组合并,化简成i<=n-2*s,如果右子序列的元素个数比左子序列少,另外使用一个合并函数将传入的right范围改成n-1,即数组末尾。如果最后一组只有左子集合,没有右子集合,则将左子集合直接放入排好的数组中(左子集合是有序的)
void MergePass(int* tmp, int* nums, int n,int s)
{
int i = 0;
while (i+2*s-1<=n-1)//i再往后走一个待归并的长度还在数组范围内,i<=n-2*s
{
Merge(tmp, nums, i, i+s-1,i+s*2-1);
i += s * 2;
}
if(n-1>=i+s)//尾部剩余有数据可以和前面一块归并
Merge(tmp, nums, i, i + s - 1,n - 1);//右子序列的元素个数比左子序列少
else//后面没有数据,不产生归并操作,直接装到目标数组中
{
for(int j = i;j<n;j++)
{
tmp[i]=nums[i];
}
}
}
void MergeSort(int* nums, int n)
{
if (n < 2 || nums == nullptr)return;
int* tmp = new int[n];
int s = 1;
while(s<n)
{
MergePass(tmp, nums, n, s);
s += s;
MergePass(nums,tmp, n, s);
s += s;
}
delete[]tmp;
}
优化方法
设置一个阀值,当划分到的子集合较小时,直接使用插入排序或快速排序替代归并。
//Mergesort中
const int cutoff = 10;
if(right<=left+cutoff+1)
{
QuickSort(nums,left,right);
return;
}
如果已经排序好了,就不用排序了(即两个待排序的子集合中,左子集合的最右边的值<右集合中最左边的值)
if(muns[mid]<=nums[mid+1])return;