1.排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
2.计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
1.找出待排序数组a中的最小值min和最大值max.
2.动态开辟一个数组countA,长度为max-min+1,全部初始话为0(使用calloc()函数就可以做到)。
3.遍历待排序数组a,如果a[i]的值为m,那countA[m-min]++。
为什么a[i]的值为m,却是countA[m-min]++,因为数组的下标最小是0,如果待排序数组中存在负数,那m-min一定不会小于0,这样肯定可以在countA数组的下标中找到找到对应的值。
4.再遍历countA数组,countA数组下标为j的位置的值为几,我就在a数组中连续存几个j+min。
为什么是存j+min,因为前面第3步是countA[m-min]++,这只是把它变回原来的真实值。
//计数排序
void CountSort(int* a, int n)
{
//统计相对范围
int max = a[0], min = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i] > max)
{
max = a[i];
}
}
int range = max - min + 1;
//动态开辟数组,要初始化为0
int* countA = (int*)calloc(range, sizeof(int));
if (countA == NULL)
{
perror("malloc fail");
exit(-1);
}
//统计次数
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
//排序
int k = 0;
for (int i = 0; i < range; i++)
{
while (countA[i]--)
{
//这一步还原成真实值
a[k++] = i + min;
}
}
free(countA);
countA = NULL;
}
计数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,range)) (N表示待排序数组元素个数,range表示待排序数组中最大值和最小值的相对差,当range很大,和N已经不是一个级别了,那时间复杂度就是O(range),否则是O(N))。
- 空间复杂度:O(range) (动态开辟了一个数组,长度为range)。
- 稳定性:稳定
3.归并排序(递归实现)
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
动图如下:
( 就是先递归出去,在回退的时候再去进行排序)动图的静态展示:
具体分析代码:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
{
return;
}
int mid = (begin + end) / 2;
//[begin,mid][mid+1,end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
//归并两个子序列,并放到新数组tmp里
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
//当归并的两个子序列都没有比完时
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//当归并的两个子序列某一个已经比完时
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//当两个子序列合并完之后,再将合并后的序列放回到数组a里
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
//归并排序(递归实现)
void MergeSort(int* a, int n)
{
动态开辟一个和数组a等长的新数组tmp
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
//调用函数
_MergeSort( a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
3.归并排序(非递归实现)
归并排序的递归实现在待排序元素特别多的时候,递归的深度会很深,这回造成栈溢出,所以我们也得掌握它的非递归实现。
根据上面这张图我们可以发现左边的分解并没有排序,而只是在将数组的元素自上而下的分解,分解好以后,右边再去归并排序,这里的分解是有规律的。那我们可以不用递归来分解,直接像左边一样自下而上的合并排序,每一层的合并排序通过控制合并子序列元素个数来实现。如控制自下而上的第一层中每个子序列里都是一个元素(在数组下标范围内)
//归并排序(非递归实现)
void MergeSortNonR(int* a, int n)
{
//动态开辟数组tmp
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
//一开始控制待合并的子序列元素个数为1个
int rangeN = 1;
while (rangeN < n)
{
//每次合并的两个子序列是相邻的,两个子序列的范围是[i,i+2*rangeN-1]
//所以每两个子序列合并完,i=i+2*rangeN
for(int i = 0; i < n; i += 2 * rangeN)
{
//每次合并都是两个子序列,分两组,范围分别是[i,i+rangeN-1][i+range n,i+2rangeN-1]
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
//这里管if()的系列判断是在判断待合并的两个子序列是否超出了数组的下标范围
//如果超出了,就需要适当的调整。(begin1是永远不会超出范围的,因为beign1=i,而i<n)
if (end1 >= n)
{
//end1超出范围,那就将它调到数组的最后一个下标处
end1 = n - 1;
//begin2=n,end2=n-1,这样在后面的判断中就可以判断出这个子序列并不存在
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)
{
//begin2超出范围,begin2=n,end2=n-1,这样后面就可以判断出该子序列不存在
begin2 = n;
end2 = n - 1;
}
else if (end2 >= n)
{
//如果end2超出范围,end2=n-1,这样[begin2,end2]就在范围内了
end2 = n - 1;
}
int j = i;
//判断两个待排序子序列是否都没排完
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
//如果有一个子序列排完了,判断哪一个排完了
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//归并一部分,拷贝一部分
//注意这里为什么用end2-i+1来求元素个数,因为如果两个子序列没超范围,end2-i+1是没有问题的
//如果有超的情况,前面将end2=n-1,这样end2-i+1也是正确的。
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
//一层被排完,往上一层排的时候,rangeN 翻倍
rangeN *= 2;
}
free(tmp);
tmp = NULL;
}
下图是当rangeN=2 数组长度n为不同值时,最后一组的两个子序列可能遇到的各种情况(rangeN等于其它值时也是在这四种情况之内)
非递归实现的归并排序最容易出问题的就是最后一组的两个子序列排序时的边界问题