归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
所谓归并,就是将两组有序的数据合成为一组有序的数据,例如有如下两个有序数组,将两个有序数组归并为一个有序数组,这就是一次归并操作。
归并排序类似于二叉树的后序遍历,即先将左右的区间都排为有序后,然后才开始进行归并操作,因为归并必须要满足两个数组是有序的。
例如有如下数组。需要先将该数组每次都分割为两个区间,直到最后每个区间都只有一个值,此时比较两个值大小,然后将该两个值归并到一个数组中,此时就得到一个有两个元素的有序数组,然后再与另一个有两个元素的有序数组进行归并,然后就得到了一个有四个元素的有序数组,然后再与另一个有四个元素的有序数组进行归并,就得到了一个有八个元素的有序数组。就这样依次递归归并,直到最后目标数组为有序数组。综上可以知道归并排序也用到了分治思想。
每次将两个有序序列归并为一个有序序列后,此时只是在tmp辅助数组中有序,而在arr中还是两个有序序列并没有归并为一个有序序列,所以最后要做的就是将tmp数组中这一块归并后的有序序列拷贝到arr数组中。
void _MergeSort(int* arr, int begin, int end, int* tmp)
{
if (begin >= end)
{
return;
}
//分治递归,让子区间先有序
int mid = (begin + end) / 2;
_MergeSort(arr, begin, mid, tmp);
_MergeSort(arr, mid + 1, end, tmp);
int begin1 = begin;
int end1 = mid;
int begin2 = mid + 1;
int end2 = end;
int i = begin1;
//在arr[begin1,end1]和arr[begin2,end2]两个区间中开始归并
while ((begin1 <= end1) && (begin2 <= end2))
{
//如果arr[begin1]的值小,就让arr[begin1]的值先进入tmp数组
if (arr[begin1] < arr[begin2])
{
tmp[i++] = arr[begin1++];
}
//如果arr[begin2]的值小于等于arr[begin1],就让arr[begin2]的值进入tmp数组
else
{
tmp[i++] = arr[begin2++];
}
}
//如果数组1中的数据还没有归并到tmp中完,就直接将剩下的都放到tmp中
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
//如果数组2中的数据还没有归并到tmp中完,就直接将剩下的都放到tmp中
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
//把归并数据拷贝到原数据
memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* arr, int n)
{
//归并排序需要先申请一个辅助数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("MergeSort malloc fail");
exit(-1);
}
_MergeSort(arr, 0, n - 1, tmp);
}
下面的图为归并排序中递归到最后begin和end相等时,就不再进行递归,然后此时返回上一个函数,执行后面的归并操作。归并操作就是同时遍历两个有序序列,然后比较元素大小,将小的元素放入到tmp数组中,当两个有序序列中有一个遍历完时,就将另一个剩下的元素都直接加到tmp数组中,此时tmp数组中[begin1,end2]之间就是归并为一个的有序序列了。
归并排序的时间复杂度为O(N * log2N),因为向下递归的复杂度为O(log2N),然后遍历一遍数组的复杂度为O(N),所以总的时间复杂度为O(N * log2N)
归并排序的空间复杂度为O(N),因为需要创建一个tmp辅助数组用来存归并后的序列。
在使用递归进行归并排序时,我们分析了函数递归到最后时,两个需要归并的数组中每个数组里面只有一个元素,所以直接比较两个元素大小,然后再添加到tmp数组中,然后再返回上一层函数中;此时该层函数中两个需要归并的数组中每个数组里面有两个元素,并且这两个元素已经为有序,所以这次归并就是同时开始遍历这两个数组,然后选出最小的元素加入tmp数组中,然后再遍历下一个元素。依次类推上去,直到回到第一次调用函数,此时数组arr中左边的数组有序,右边的数组也有序,此时将左右两个数组进行归并,然后放入到tmp中,等左右两个数组的元素都进入tmp中时,此时tmp数组为有序数组,而arr数组中还是左右两组数据有序,此时将tmp中的内容拷贝到arr,arr也变为有序数组了。
利用循环实现归并排序时,我们也可以先将数组中每一个数为一组,然后让两个数归并为一个含有两个元素的有序数组,然后再让两个含有两个元素的有序数组归并为一个含有四个元素的有序数组,然后再让两个含有四个元素的有序数组归并为一个含有八个元素的有序数组,就这样一直循环下去,直到总数组有序。
例如如下数组的归并过程。
当gap为1时,代表两个需要归并的数组中每个数组只有一个元素。
当gap为2时,代表两个需要归并的数组中每个数组有两个元素。
当gap为4时,代表两个需要归并的数组中每个数组有四个元素。
void MergeSortNonR(int* arr, int n)
{
//归并排序需要先申请一个辅助数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("MergeSort malloc fail");
exit(-1);
}
int i = 0;
//gap为每次需要归并的两个数组中有几个元素
int gap = 1;
//当gap为n/2时,此时为最后一次归并
while (gap < n)
{
//每次都从数组起始位置开始,然后选2*gap个元素,前gap个为一个数组,后gap个为一个数组,然后让这两个数组进行归并
for (i = 0; i < n; i += 2 * gap)
{
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
int j = begin1;
//归并操作
while ((begin1 <= end1) && (begin2 <= end2))
{
if (arr[begin1] < arr[begin2])
{
tmp[j++] = arr[begin1++];
}
else
{
tmp[j++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = arr[begin2++];
}
}
//此时arr中从起始位置开始,让每gap个元素为一个数组,则这些数组都是有序的,
//然后将这些数组每两组归并为一个更大的有序数组
gap = gap * 2;
}
//此时tmp辅助数组的数据经过一步步归并成为了有序的,而arr数组中的数据还是无序的,所以要将tmp中的内容拷贝到arr中。
memcpy(arr, tmp, sizeof(int) * n);
//释放辅助数组的空间
free(tmp);
}
上面的代码当排序2的幂次方个数据时没有问题,但是当排序数据的长度不是2的幂次方时,就会出现数组越界情况。例如当数组中有10个元素时,此时gap=1,还没有出现越界情况。
但是当gap为2时,此时当i=8时,begin1=8, end1=9, 但是begin2=10, end2=11,此时arr[begin2]和arr[end2]已经越界访问数组arr了。
当gap为4时,此时当i=8时,begin1=8, 但是en1=11, begin2=12, end2=15,此时
arr[end1]和arr[begin2]和arr[end2]都已经越界访问数组arr了。
当gap为8时,此时当i=0时,begin1=0, end1=7, begin2=8, 但是end2=15,此时
arr[end2]就已经越界访问数组arr了。
我们通过上面的分析知道了在什么情况时会造成越界访问,但是我们应该怎么修改代码呢?我们可以判断当begin1、end1、begin2、end2造成越界访问时,此时我们将该区间设置为不存在的区间,即进行边界修正,则在循环时该区间就不会进入循环,就不会造成越界访问了。
情况1:例如当求出begin2=10时,此时判断begin2>=n,则将begin2=10,end2=9,此时就不会进行下面的while ((begin1<=end1) && (begin2<=end2))循环,因为begin2>end2。
即此时会直接进入while (begin1 <= end1)循环中,然后将区间[begin1,end1]内的数据都放到tmp的[begin1,end1]区间内。
情况2:例如当end1=11,此时end1越界,则就需要设置end1=n-1,即将[begin1,n-1]区间内的元素归并即可。并且要将begin2=n,end2=n-1,即将[begin2,end2]设置为不存在区间,这样就不会进入while ((begin1<=end1) && (begin2<=end2))循环,因为begin2>end2。
此时会直接进入while (begin1 <= end1)循环中,然后将区间[begin1,n-1]内的元素都归并到tmp数组中即可。
情况3:例如当end2=15时,此时只有end2越界了,所以直接将end2=n-1,此时还会进入while ((begin1<=end1) && (begin2<=end2))循环中,因begin2<=end2。
此时两个需要归并的有序数组中,第二个数组只有两个元素,但是也可以正常进行归并操作。
void MergeSortNonR(int* arr, int n)
{
//归并排序需要先申请一个辅助数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("MergeSort malloc fail");
exit(-1);
}
int i = 0;
//gap为每次需要归并的两个数组中有几个元素
int gap = 1;
//当gap为n/2时,此时为最后一次归并
while (gap < n)
{
//每次都从数组起始位置开始,然后选2*gap个元素,前gap个为一个数组,后gap个为一个数组,然后让这两个数组进行归并
for (i = 0; i < n; i += 2 * gap)
{
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
//printf("%d %d %d %d\n", begin1, end1, begin2, end2);
//先判断求出来的区间是否有越界区间
//如果end1越界,则此时归并时只归并[begin1,n-1]区间
if (end1 >= n)
{
//此时end1已经越界,则将end1直接为n-1,
end1 = n - 1;
//此时end1=i+gap-1都越界了,所以begin2=i+gap和end2=i+2*gap-1都会越界
//所以此时将begin2=n,end2=n-1,此时begin2>=en2,就不会进入循环
begin2 = n;
end2 = n - 1;
}
//此时[begin2,end2]区间越界,则归并时就不用归并该区间
if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
//此时end2越界,则归并时只需归并[begin2,n-1]区间即可
if (end2 >= n)
{
end2 = n - 1;
}
int j = begin1;
//归并操作
while ((begin1 <= end1) && (begin2 <= end2))
{
if (arr[begin1] < arr[begin2])
{
tmp[j++] = arr[begin1++];
}
else
{
tmp[j++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = arr[begin2++];
}
}
//此时tmp辅助数组的数据为有序的,而arr数组中的数据还是无序的,所以要将tmp中的内容拷贝到arr中。
memcpy(arr, tmp, sizeof(int) * n);
//此时arr中从起始位置开始,让每gap个元素为一个数组,则这些数组都是有序的,
//然后将这些数组每两组归并为一个更大的有序数组
gap = gap * 2;
}
//释放辅助数组的空间
free(tmp);
}
上面代码为当遇到越界时,我们使用修正边界来使越界的区间变为不存在区间,那样归并时就不会将这些不存在区间的数据进行归并。下面的代码为当我们遇到越界时,如果是end1和begin2越界,直接不再进行下面的归并,直接跳出这次循环即可,而如果是end2越界,就将end2调整为n-1,此时就要进入
while ((begin1<=end1) && (begin2<=end2))循环中进行[begin1,end1]和[begin2,n-2]区间内数据的归并操作。
//将越界的元素直接不进行归并操作
void MergeSortNonR(int* arr, int n)
{
//归并排序需要先申请一个辅助数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("MergeSort malloc fail");
exit(-1);
}
int i = 0;
//gap为每次需要归并的两个数组中有几个元素
int gap = 1;
//当gap为n/2时,此时为最后一次归并
while (gap < n)
{
//每次都从数组起始位置开始,然后选2*gap个元素,前gap个为一个数组,后gap个为一个数组,然后让这两个数组进行归并
for (i = 0; i < n; i += 2 * gap)
{
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
//先判断求出来的区间是否有越界区间
//end1越界或者begin2越界,则可以不归并了
if ((end1 >= n) || (begin2 >= n))
{
break;
}
//此时end2越界,则归并时只需归并[begin2,n-1]区间即可
if (end2 >= n)
{
end2 = n - 1;
}
int j = begin1;
//归并操作
while ((begin1 <= end1) && (begin2 <= end2))
{
if (arr[begin1] < arr[begin2])
{
tmp[j++] = arr[begin1++];
}
else
{
tmp[j++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = arr[begin2++];
}
}
//此时tmp辅助数组的数据为有序的,而arr数组中的数据还是无序的,所以要将tmp中的内容拷贝到arr中。
//此时arr中从起始位置开始,让每gap个元素为一个数组,则这些数组都是有序的,
memcpy(arr, tmp, sizeof(int) * n);
//然后将这些数组每两组归并为一个更大的有序数组
gap = gap * 2;
}
//释放辅助数组的空间
free(tmp);
}
计数排序思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
然后再次遍历arr数组,并且将arr[i]-min作为tmp的下标,在tmp数组中记录arr[i]元素出现的次数,即每当遍历一个元素,就将tmp[arr[i]-min]++。
然后再将tmp中记录的数据写回到arr数组中,即arr[i]=i+min,其中tmp数组中记录该数据出现几次,就在arr数组中写入几次,此时得到的arr数组就为有序数组。
这种相对映射的方法也适合有负数出现的情况,例如有如下数组。
先遍历一遍arr数组,得到最大值max和最小值min,然后求出辅助数组长度为max-min+1,然后就创建一个辅助数组tmp,并且将tmp中全部初始化为0。
然后再次遍历arr数组,并且将arr[i]-min作为tmp的下标,在tmp数组中记录arr[i]元素出现的次数,即每当遍历一个元素,就将tmp[arr[i]-min]++。虽然arr[0]为-5,但是ar[[i]-(-5)=0,所以tmp[0]中存的就是-5出现的次数。
然后再将tmp中记录的数据写回到arr数组中,即arr[i]=i+min,而tmp[0]中记录了-5出现的次数,arr[0]=i+min=0+(-5)=-5。tmp数组中记录该数据出现几次,就在arr数组中写入几次,此时得到的arr数组就为有序数组。
void CountSort(int* arr, int n)
{
int min = arr[0];
int max = arr[0];
int i = 0;
//选出arr中最大最小值
for (i = 1; i < n; i++)
{
if (arr[i] < min)
{
min = arr[i];
}
if (arr[i] > max)
{
max = arr[i];
}
}
//然后求出辅助数组tmp的长度
int len = max - min + 1;
//给辅助数组申请空间
int* tmp = (int*)malloc(sizeof(int) * len);
if (tmp == NULL)
{
perror("CountSort malloc fail");
exit(-1);
}
//将辅助数组中都初始化为0
memset(tmp, 0, sizeof(int) * len);
//然后将arr数组中元素出现的次数记录到tmp数组中
for (i = 0; i < n; i++)
{
tmp[arr[i] - min]++;
}
//回写 -- 即将数据再写入到arr数组中
int j = 0;
for (i = 0; i < len; i++)
{
//出现几次就写入几次
while (tmp[i]--)
{
arr[j++] = i + min;
}
}
free(tmp);
}
通过上述可以看到,计数排序的时间复杂度最优的情况下为O(N),但是计数排序还需要O(N)的空间复杂度,所以计数排序适合范围集中,重复数据多的情况。
并且计数排序有很多局限性:
1.如果是浮点数、字符串就不能使用计数排序了。
2.如果数据范围很大,那么空间复杂度就会很高,计数排序就不适合了。