排序 – 归并排序与计数排序
文章目录
一、归并排序(递归)
1.排序思想
归并排序的基本思想:将已经有序的子序列合并,得到完全有序的序列。即先使每个子序列有序,再将子序列挨个有序合并。
这里先使用递归的思想进行实现,即归并当前两个子序列之前,先使两个子序列有序,将这两个子序列分别堪称新的序列,进行同样的操作,使用递归分治的思想不断递归下去,直到最后一个区间就只有一个数,则返回到上一层递归,然后进行归并。
2.代码示例
代码如下:
//归并排序
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)//递归到最后就是begin == end,一个区间就一个数
{
return;
}
int mid = (begin + end) / 2;
//[begin, mid] [mid + 1, end] 递归,分治左右区间,让子区间有序
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
//子区间有序后,进行归并[begin, mid] [mid + 1, end]
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)//当两个子区间都没有走完,进入循环
{
//从两个区间中找更小的数放在前面,拷贝到tmp
if (a[begin1] <= a[begin2])//加等于号,可以控制数据稳定,保持相同大小数据相对顺序不变
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)//左区间还没走完,直接赋值到tmp
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)//右区间还没走完,直接赋值到tmp
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int sz)
{
int* tmp = (int*)malloc(sizeof(int) * sz);//需要一个同样大小的辅助数组来帮助实现
if (tmp == NULL)
{
perror("malloc");
return;
}
_MergeSort(a, 0, sz - 1, tmp);
free(tmp);
}
3.特性总结
1.归并排序的缺点在于O(N)的空间复杂度,更多是用来解决再磁盘中外排序的问题;
2.时间复杂度:O(N * logN);
3.空间复杂度:O(N);
4.稳定性:稳定。
二、归并排序(非递归)
1.排序思想
归并排序非递归的基本思想:与递归法思想一致,只是使用非递归的方式实现。
使用非递归实现的方法是:将整个序列分为多个子区间,子区间的长度gap从1开始,相邻两个子区间为一组数据,能够进行一次归并操作,直到当前gap下的所有数据都归并完成,将数据拷贝回原数组,区间长度gap变为原来的2倍,循环执行相同操作,直到gap大于等于数组长度sz,循环结束,得到有序数组。
2.代码示例
代码如下:
void MergeSortNonR(int* a, int sz)
{
int* tmp = (int*)malloc(sizeof(int) * sz);//需要一个同样大小的辅助数组来帮助实现
if (tmp == NULL)
{
perror("malloc");
return;
}
int gap = 1;//gap是子区间的长度
while (gap <sz)//当gap小于sz时,进行循环
{
for (int i = 0; i < sz; i += 2 * gap)//一组数据两个子区间,循环完后当前gap下的数据就排完一趟了
{
//[i, i + gap - 1] [i + gap, i + 2*gap - 1]这两个区间进行归并
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//由于区间是计算出来的,当整个数据的个数不是2的n次方时,就会发生越界,所以要对边界进行修正
//只有end1,begin2,end2会发生越界
if (end1 >= sz)
{
end1 = sz - 1;
//将[begin2, end2]修正为不存在区间
begin2 = sz;
end2 = sz - 1;
}
if (begin2 >= sz)
{
begin2 = sz;
end2 = sz - 1;
}
if (end2 >= sz)
{
end2 = sz - 1;
}
//修正完后,开始归并
int j = begin1;
while (begin1 <= end1 && begin2 <= end2)//当两个子区间都没有走完,进入循环
{
//从两个区间中找更小的数放在前面,拷贝到tmp
if (a[begin1] <= a[begin2])//加等于号,可以控制数据稳定,保持相同大小数据相对顺序不变
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)//左区间还没走完,直接赋值到tmp
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)//右区间还没走完,直接赋值到tmp
{
tmp[j++] = a[begin2++];
}
}
//当前gap下的数据归并完后,拷回原数组
memcpy(a, tmp, sizeof(int) * sz);
gap *= 2;//区间长度变为2倍
}
free(tmp);
}
一、计数排序
1.排序思想
计数排序的基本思想:统计序列中每个数据出现的次数,根据统计的结果将数据按顺序放回到原来的数组中。
如果数据的范围很大,那么这种排序就会很浪费空间,适合数据范围较小,重复数据多的排序场景。
为了减小空间复杂度,可以使用相对映射来进行计数,即记录的是当前数据与最小值之间的差距,在回写时,把相对映射转为原数据回写就行了。
2.代码示例
代码如下:
//计数排序
void CountSort(int* a, int sz)
{
//计算最大和最小值
int max = a[0];
int min = a[0];
for (int i = 1; i < sz; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
//开辟统计次数的数组
int range = max - min + 1;//数据范围
int* count = (int*)malloc(sizeof(int) * range);
if (count == NULL)
{
perror("malloc");
return;
}
memset(count, 0, sizeof(int) * range);
//统计数据出现的次数
for (int i = 0; i < sz; i++)
{
count[a[i] - min]++;//相对映射,代表的是当前数据离最小值的相对距离
}
//回写,排序
int j = 0;
for (int i = 0; i < range; i++)
{
//出现几次就回写几个i + min
while (count[i]--)
{
a[j++] = i + min;
}
}
free(count);
}
3.特性总结
1.计数排序再数据范围较小,重复度较高的场景下,效率很高;
2.时间复杂度:O(MAX(sz, range));
3.空间复杂度:O(range);
4.稳定性:稳定;
总结
本文主要介绍了归并排序的递归写法和非递归写法,以及计数排序的实现。