1. 归并排序
归并排序:归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治算法的一个非常典型的应用。先递归分解,再合并数列。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合成一个有序表,称为二路归并。
具体方法:
简要分析一下步骤:
当每个子序列分解为1个元素时,可以看做该子序列有序,然后比较合并相邻两个子序列,存到临时数组中,以此类推。
为什么要借助临时数组?因为如果在原数组上归并,存在覆盖问题。
1、递归解法
void _MergeSort1(int* a, int left, int right, int* tmp)// 归并排序(子函数)
{
if (left == right) //起始下标和末端下标
return;
// 先划分
int mid = left + (right - left) / 2; // 防止越界,找到中间下标
// [left,mid] [mid+1,right] 此时无序
// 需要将左右两边都有序,拆分子问题
_MergeSort1(a, left, mid, tmp); //递归左子序列
_MergeSort1(a, mid + 1, right, tmp); //递归右子序列
// [left,mid] [mid+1,right] 此时有序
// 将a[left,mid]和a[mid+1,right]归并到tmp[left,right]上
// 下面的这段代码,最开始合并最小的两个子序列,随着逐步递归,需要合并的子序列越来越长,用while循环。
int begin1 = left;
int end1 = mid;
int begin2 = mid + 1;
int end2 = right;
int i = left; //临时数组的下标
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1];
++begin1;
}
else
{
tmp[i++] = a[begin2];
++begin2;
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1];
++begin1;
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2];
++begin2;
}
memcpy(a + left, tmp + left, sizeof(int)*(right - left + 1));
//将归并到临时数组的元素拷回原数组
}
void MergeSort1(int* a, int n) // 归并排序
{
//参数:数组、数组大小
int* tmp = (int*)malloc(sizeof(int)*n); //开辟一个新的数组空间
_MergeSort1(a, 0, n - 1, tmp); //参数:原数组名 起始坐标 末端下标 新数组名
free(tmp);
}
2、非递归解法
用gap表示在循环分解合并过程中,子序列的元素个数,以此控制各个子序列最终合并成一个序列。
void MergeSort2(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
// [begin1, end1] [begin2, end2]
int gap = 1; //子序列的元素个数
while (gap < n) //子序列的元素个数逐次递增,用来约束子序列的分解和合并
{
for (int begin = 0; begin < n; begin += 2 * gap)
{
// [begin, begin + gap - 1] [begin + gap, begin + 2 * gap - 1]
// [0, 0] [1, 1] gap = 1
// [0, 1] [2, 3] gap = 2
int begin1 = begin, end1 = begin + gap - 1; //第一个子序列的跨度
if (end1 >= n)
end1 = n - 1;
int begin2 = begin + gap, end2 = begin + 2 * gap - 1; //第二个子序列的跨度
if (end2 >= n)
end2 = n - 1;
int index = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
}
memcpy(a, tmp, sizeof(int)*n);
gap *= 2; //增加跨度
}
free(tmp);
}
总结:
归并排序更多解决的是磁盘中的外排序问题。当参加排序的数的量太大,或内存不足以存放时,需要使用外排序。外排序可以使用插入排序的思想,也可以用归并排序的思想。
缺点:需要O(N)的空间复杂度。
稳定性:稳定
时间复杂度:O(NlogN),拆分时每层只需要常数次,太小了;归并时才有时间复杂度的消耗。整棵树的高度(图中的一半 满二叉树)就是logN,树的每一层都是有N个数要比较,所以时间复杂度是O(NlogN)。
空间复杂度:O(N)
2. 计数排序(鸽巢排序)
计数排序:计数排序是一个非基于比较的排序算法,是对哈希直接定址法的变形应用。 它的优势在于在对一定范围内的整数排序时,它的时间复杂度为O( Max(N, range) ),快于任何比较排序算法。
具体方法:
1、统计相同元素出现的次数;
此时,统计次数的数组记录的是相对位置的元素。
比如:原数组:5 7 9 6 7 9 计数数组下标:0 1 2 3 4,并不是 0 1 2 3 4 5 6 7 8 9,起决定作用的是数据的范围。
2、根据统计的结果,将序列回收到原来的序列中
代码片:
void CountSort(int* a, int n) //计数排序 (鸽巢排序)
{
// 先找出序列的max和min
int max = a[0];
int min = a[0];
for (int i = 0; i < n; ++i)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
// 根据数组元素范围,确定东塔爱申请tmp数组的大小
int range = max - min + 1;
int* counta = (int*)malloc(sizeof(int)*(max - min + 1));
memset(counta, 0, sizeof(int)*range); // 初始化,将开出来的空间置0
// 1、统计次数
for (int i = 0; i < n; ++i)
counta[a[i] - min]++;
// 2、回收 此时,tmp数组中已经存在各数据出现的次数
int j = 0;
for (int i = 0; i < range; ++i)
{
while (counta[i]--)
a[j++] = i + min;
}
}
总结:
计数排序在元素范围集中的情况下,效率很高,但是适用范围和场景有限。
稳定性:稳定
时间复杂度:O( Max(N, range) )
空间复杂度:O(range) 开了范围那么大的空间