前言
排序算法有很多,因其思想及依赖的数据结构不同造成对应排序方法性能的不同。本文将介绍常用的八大排序算法。让大家对各类排序有所了解。
注意:本文介绍的排序均为排升序
排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
- 稳定性介绍:
特性总结
为方便大家更好理解下面实现的各类排序,先对各类排序算法特性进行总结
排序方式 | 平均情况 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
冒泡排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(N^1.3) | O(N^1.25) | O(N^1.6) | O(1) | 不稳定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不稳定 |
快速排序 | O(N*logN) | O(N*logN) | O(N^2) | O(logN~N) | 不稳定 |
归并排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(N) | 稳定 |
计数排序 | O(max(range,N)) | O(max(range,N)) | O(max(range,N)) | O(range) | 稳定 |
- 不稳定的原因在下图:
冒泡排序
开始先上盘开胃小菜:想必冒泡排序大家都不陌生了
思想:
冒泡就是将当前位置的元素与后一个位置的元素进行比较,符合排序要求就交换两个位置的元素。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void BubbleSort(int* a, int n)
{
//每一趟
for (int i = 0; i < n; i++)
{
int flag = 1;//优化
//每一轮
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 0;
}
}
//如果一轮下来都不需要交换元素,则说明数组元素已经有序,则可直接跳出循环
if (flag == 1)
{
break;
}
}
}
实现:
冒牌排序需要嵌套两层循环,外层循环控制趟数,内层循环控制排序元素的位置。每一趟排序完成会将当前轮的最值排到最后一个位置。需要注意的是内外循环结束的条件
- 外层循环控制的是趟数:每一趟只将当前趟数的最值排到最后的位置,一趟只排好一个位置,也就是需要排n-1趟。
- 内层循环控制排序位置:每一趟会将元素排好在最后面,所以每一轮就不需要排到已经排好元素的位置,所以停止条件为j < n - i - 1。
冒泡排序的时间复杂度是经典的O(N^2),这是因为嵌套两层循环,而且每轮只能排好一个元素,效率是非常低下的,即便我们增加了flag来优化:判断当前轮是否已经排完序,是则无需再排。但效率依旧很低。
选择排序
一般的选择排序一轮只选最大或者最小的元素,效率低下。我们这里直接升级一下,同时选最大和最小,一轮排一大一小两个元素。
思想:
每一次从待排序的数据元素中选出最小和最大的两个元素,存放在序列的起始,结束位置,直到全部待排序的数据元素排完 。(下图演示的是只选一个的选择排序)
//优化版,同时选最大的和最小的,将大的往后放,小的往前放,不断缩小区间
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)//相遇则说明已经排好
{
//都从开始位置选
int maxi = begin;
int mini = begin;
for (int i = begin; i <= end; i++)//遍历选出当前区间的最值
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);//一轮选出了两个最值,交换
if (maxi == begin)//要注意前一轮交换是否会影响下一轮
{
maxi = mini;//若最大值在第一个位置,则需要更新一下交换后最大值的下标位置
}
Swap(&a[end], &a[maxi]);
begin++;//控制区间
end--;
}
}
实现:
选择排序也由两层循环一起完成:外层用来检测元素是否相遇,相遇则说明已经排完序了。内层循环用来控制待排序区间:先选出最小和最大值,再将其与当前区间首尾位置元素进行交换。最后不断缩小当前区间,直到排好序。
- 需要注意的是在每轮第一次交换是否会影响到下一一次交换。
即便我们推出优化版本,但选择排序的思想注定了它的时间复杂度只能是
直接插入排序
思想:
我们将待排序元素与其前面位置的元素进行比较(前面元素已经有序),直到找到目标位置将其插入
实际中我们玩扑克牌时,就用了类似的思想
//类似插排,将要插入的数与前面的数(已有序)一一比较,将元素后移,直到找到要插入的位置
void InsertSort(int* a, int n)
{
for (int i = 1; i < n; i++)
{
int tmp = a[i];//要插入的值
int prev = i - 1;//插入值的前一位
while (prev >= 0)
{
if (a[prev] > tmp)
{
a[prev+1] = a[prev];//将元素后移
prev--;//向前继续比对
}
else//找到插入位置
{
break;
}
}
a[prev + 1] = tmp;//插入,prev位置之后
}
}
实现:
由于待排序元素前面已经是有序数组,所以我们从第二个位置开始排序。先用临时变量tmp记录待排序元素,然后将其与前面位置的元素一一比较,如果前一个大于tmp(此时排升序),则将前面的元素往后覆盖,直至找到位置break出循环。这时候再将tmp插入目标位置(目标位置为前一元素之后)
- 需要注意的是排序位置之前已经是有序数组,要记住这一点,这也是在循环里为什么不是往后覆盖数据就是break出循环的原因。
- 还有一点就是待插入元素的位置:以升序为例,如果前一元素已经小于待插入元素,则主动跳出循环,再将其插到前一元素的后面,也就是prev + 1的位置。如果待插入元素是最小值,那么只能等prev越界,此时prev + 1便为首元素位置。
直接插入排序的时间复杂度也为O(N^2) ,但与前面两个O(N^2) 的相比,直接插入排序是O(N^2)家族里的唯一牌面了,虽说是同一级别的,但在实际中也还能拿的出手
以上单位为毫秒,对100000个数据进行排序(测试方法后续再讲解)。
该测试可以看出冒泡是真的没得救了,完全上不了桌,选择排序优化后好像也还能用,插入排序与他们相比给人一种错觉:好像他们不是这个量级的。
- 他们的时间复杂度都为O(N^2) 没错,这是由于时间复杂度表示方法的原因,将他们都归为了一类,但在实际中直接插入排序最好情况下是可以达到O(N)的
- 结合动图及代码可以看到,当直接插入排序要排的是接近有序数组时,一趟下来就能将数组排好,做到O(N)的级别。
希尔排序
我们的大佬Shell,对上面的直接插入排序很看好,能够有O(N)级别的潜力,觉得很有潜力,是个潜力股,决心好好挖掘一下。
思想:
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap作为距离,把待排序数据中所有个数记录分成n个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序。
//1 当gap不为1时为预排序
//2 当gap为1时是插入排序
void ShellSort(int* a, int n)
{
int gap = n;//要保证最后gap为一,变为插入排序
while (gap > 1)//保证最后一次gap为一
{
gap = gap / 3 + 1;//逐渐缩小gap
for (int i = 0; i < n - gap; i++)//多组并排,注意边界
{
int prev = i;
int tmp = a[i + gap];
while (prev >= 0)
{
if (a[prev] > tmp)//升序
{
a[prev + gap] = a[prev];//将大于tmp的值往后覆盖
prev-=gap;//间隔gap个
}
else
{
break;
}
}
a[prev + gap] = tmp;//参照插入排序,逻辑是一样的
}
}
}
实现:
由图示和动画演示可以看出希尔排序是直接插入排序的优化:当gap不为1时是预排序。当gap为1时就是直接插入排序,此时的数据已经接近有序,再进行直接插入排序就能完全释放他的潜力,达到O(N)级别。
- gap不为1:预排序:预排序的目的是将数组接近有序。我们将距离为gap的分为一组,让每个gap组接近有序,再不断缩小gap,重复以上操作,以此让数组接近有序。(预排序的排序逻辑与直接插入排序一致,具体步骤也写在注释里了)
- 当gap为1时为直接插入排序。
对于希尔排序的时间复杂度的计算就有点麻烦了,希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定。
- 所以我们一般按以O(N^1.3) 作为希尔排序的时间复杂度。
堆排序
在此前的博客中已经对堆排序进行了讲解,请各位移步到堆排序进行查看。
堆是二叉树结构,是一种十分高效的数据结构,堆排序也是依赖这种结构,其时间复杂度为O(N*logN)的级别,而接下来的快速排序,归并排序也是基于这种结构思想来实现的,所以我们从这里开始对各种排序的排序速度的直观对比。
排序时间测试:
可以先浅浅看一下不同时间复杂对应的变化曲线。
怎么样,对这个结果感到惊讶吗?(以上单位为毫秒,对100000个数据进行排序。)
这简直就是O(N*logN)对O(N^2)的降维打击。
以下为粗略计算可以直观看出两个级别的差距(实际运算时仍有别的开销,无需具体落实差距,只需要看在哪个级别就好)
测试的代码实现:
在堆区开辟数组大小N的数组,再利用rand()产生N个随机数,测试多组时需要开对应个数组,其内容需要一样。
clock函数的使用需要包含头文件<time.h>,其功能为记录一开始遇到该函数直到第二次遇到该函数所用的时间,我们利用这两个的差值来记录每种排序消耗的时间。
void TopTest()
{
//srand((unsigned int)time(NULL));
srand(time(0));
int N = 100000;
int* a1 = (int*)malloc(sizeof(int)*N);
int* a2 = (int*)malloc(sizeof(int)*N);
int* a3 = (int*)malloc(sizeof(int)*N);
int* a4 = (int*)malloc(sizeof(int)*N);
int* a5 = (int*)malloc(sizeof(int)*N);
//确保每组数据相同
for (int i = 0; i < N; i++)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
printf("InsertSort: %d\n", end1 - begin1);
int begin2 = clock();
BubbleSort(a2, N);
int end2 = clock();
printf("BubbleSort: %d\n", end2 - begin2);
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();
printf("SelectSort: %d\n", end3 - begin3);
int begin4 = clock();
ShellSort(a4, N);
int end4 = clock();
printf("ShellSort: %d\n", end4 - begin4);
int begin5 = clock();
HeapSort(a5, N);
int end5 = clock();
printf("HeapSort: %d\n", end5 - begin5);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
}
int main()
{
TopTest();
return 0;
}
- 接下来的排序的时间测试都以此为模板,自己对应添加测试就好
测试时需要改为release版本,让程序得到最好的优化,否则可能体现不出各种排序的差距。
快速排序
快排可以算得上是各种排序算法中的名人了,就连库里面的qsort也是用快速排序写的。
思想:
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序是Hoare大佬提出的一种二叉树结构的交换排序方法,采用了类似二叉树的结构,选定基准值,划分左小右大的区间,再通过递归重复以上操作,最终达到排序的目的。
快排得益于其思想,衍生出了很多版本,我们在此介绍三个版本。
Hoare版本
开始先介绍最原汁原味的版本
//hoare版:选出基准值(默认选左边第一位),让右边先往左走,停在小于基准值的位置,左边的再出发选出大于基准值的,交换两数
//类似二叉树结构,使用递归,划分左右区间,左边的小于基准值,右边的大于基准值
int PartSort1(int *a,int left,int right)
{
int keyi = left;
while (left < right)
{ //一轮换一次 //右边找小
while (right > left && a[right] >= a[keyi])//防止极端情况下越界,=是为了防止死循环
{
right--;
}
//左边找大
while (left <right&& a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
//此时已经相遇
Swap(&a[keyi], &a[left]);
return left;//返回相遇位置的下标,再以此为划分点继续划分左右区间
}
//每次递归划分两个区间,左边的比keyi对应的值小,右边比keyi对应的值大
//二叉树结构,递归分治
void QuickSort(int* a, int begin,int end)//end为下标
{
//递归终止条件
if (begin >= end)//只有一个的时候或不存在的区间
{
return;
}
int keyi= PartSort1(a, begin, end);
//[0,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);//左区间
QuickSort(a, keyi + 1,end);//右区间
}
实现:
以上图为例,默认将下标为left(也就是区间最左边的元素)的值选做基准值。从右边开始往左找,直到找到比基准值小的元素,然后停下。这时开始从左往右找,直到找到比基准值大的元素,停下然后交换他们的值。重复这个循环直到左右下标相遇,这时再将基准值与相遇坐标的值交换,便完成了区间的划分。基准值左边的元素都是小于基准值,右边都是大于基准值的元素再通过递归继续划分区间,最终达到排序的目的。
需要注意的细节有:
- 默认左边为基准值则需从右边开始往左动,默认右边为基准值则需要从左边开始往右动。
- 递归的终止条件:当区间只有一个元素时则无需再排;由于keyi-1,keyi+1的操作会导致最终出现不存在的区间。
- 右边找小,左边找大的循环里要注意是否越界;找值时一定要大于等于或小于等于基准值,否则以下情况就会出现死循环
思想核心:
该思想最为重要的问题——如何确保相遇位置的值一定小于基准值呢?(以左边为基准值为例,右边也是同理。)
因为基准值默认为区间最左边的值,那么在左右下标相遇,与基准值交换后,基准值左边的元素都要小于基准值,基准值右边都要大于基准值。所以在左右下标相遇交换基准值时就必须保证相遇位置的元素一定是小于基准值的,否则该思想就不成立了。
对于以上疑问,可以分为两种情况:
- 情况一:L(动)遇R,R停了,L在走;R先走,R停下来的位置就以确保一定比基准值小,此时R的位置如果是相遇位置,则一定小于基准值。
- 情况二:R(动)遇L,在相遇轮,L就没动过,R在动,直到与L相遇,相遇位置就是L的位置(L的位置有可能就是区间最左侧位置或者交换过一些轮次的位置),都确保小于等于基准值。
由于快速排序是基于二叉树结构的交换排序方法,其时间复杂度一般为:O(N*logN)。
性能对比:
此时的数据量为100000个。
挖坑法
这一版本虽然叫挖坑法,但是坑却要比Hoare版少不少,而且实际排序时也像在挖坑,故叫挖坑法。
思想:
挖坑法思想与Hoare版的思想是一致的,只是在实现形式上有所区别。
//挖坑法
//仍为hoare思想,但更好理解
//第一个位置为坑,记录值,右边找小,填到坑位并变成新的坑位,然后左边找大填到坑位后变成新坑位
//直到区间收缩到只有一个位置,即最后的坑位,此时将一开始记录的值填进去,并返回坑位(即划分左右区间的位置)完成一次递归
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int hole = left;
while (left < right)
{ //右边找小
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
//左边找大
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return left;
}
//每次递归划分两个区间,左边的比keyi对应的值小,右边比keyi对应的值大
//二叉树结构,递归分治
void QuickSort(int* a, int begin,int end)//end为下标
{
if (begin >= end)//只有一个的时候或不存在的区间
{
return;
}
int keyi= PartSort2(a, begin, end);
//[0,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);//左区间
QuickSort(a, keyi + 1,end);//右区间
}
实现:
挖坑法先用Key将最初坑位(默认为区间左边第一个)的值(也可叫基准值)先保存起来,并记录坑位。然后先从右往左找小于基准值的,找到后填入上一个坑位,并更新坑的位置。然后到左边找大也是一样的逻辑。直到左右下标相遇,这时再将一开始保存的基准值key填进相遇位置,完成左右区间的划分,返回中间位置下标,完成一趟区间的划分。再通过递归直到完成排序。
由于思想是一样的,挖坑法的时间复杂度也是O(N*logN)。
前后指针法
再介绍一个前后指针法的快排版本
思路
默认最左的为基准值,让cur一直走,当cur走到的值小于基准值,prev跟走cur一起走,当cur走到的值大于基准值,prev不走,此时他们间隔的就是大于基准值的,当下标cur与++prev不等时,就可交换他们两个二对应的值,这样就相当于把大的翻滚式往前推,同时将小的放到左边。当cur越界后返回prev的位置,这样又完成了单趟的区间划分,再通过递归以上操作完成排序。
- 三种快排思想都是一致的,都是选出基准值,然后划分左右区间,左区间小于基准值,右区间大于基准值,再通过递归完成排序。
int PartSort3(int* a, int left, int right)
{
int keyi = left;//默认最左的为keyi
int prev = left;//先前指针从最左边开始
int cur = left + 1;//从第二位开始
while (cur <= right)
{ //当cur位置的值比keyi小或相等时,prev跟着跟随cur一起动
if (a[cur] <= a[keyi] && ++prev != cur)//前置++,即做到了上述要求,又可以减少不必要的交换
{
Swap(&a[prev], &a[cur]);
}
cur++;//cur一直往前走
}
Swap(&a[keyi], &a[prev]);//此时说明已经相遇
keyi = prev;
return keyi;
}
void QuickSort(int* a, int begin,int end)//end为下标
{
if (begin >= end)//只有一个的时候或不存在的区间
{
return;
}
int keyi = PartSort3(a, begin, end);
//[0,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);//左区间
QuickSort(a, keyi + 1,end);//右区间
}
实现:
仍以区间最左边元素为基准值,prev也从基准值位置出发,cur在prev的下一位开始出发;cur每轮都往前走,当cur位置的值大于基准值时,prev不走,这样cur和prev间隔的就是大于基准值的元素;当cur位置的值小于等于基准值时,让prev也往前走,并且当++prev与cur不等时,才交换这两个位置的值(如果两下标相等则为同一值,交换没有意义)。直到cur越界跳出循环,此时prev位置就是划分左右区间的位置,将其与基准值交换,返回该位置,完成一趟区间划分。再通过递归完成所有区间的划分,完成排序。
三种版本的思想相同,时间复杂度都为O(N*logN)。
快排总结:
对于这三种版本的,其核心思想都是一致的,只是实现形式不同,其实际效率也是一样的
- N为1000000。
快排小优化
三数取中
快排是一种二叉树结构的交换排序方法,其O(N*logN)的时间复杂度依赖于他的结构思想。当基准值为数组中位数时,能够快速划分好区间,效率是最快的。
但是当数组是接近有序时,也就是最坏的情况,使时间复杂度就会变成O(N^2)
- 此时快排排的是一个有序的数组
所以为了避免这种最坏的情况发生,我们通过三数取中的办法来解决这个问题。
- 三数取中也就是在排序前将数组的前中后三个位置的元素进行比较,将三者的中位数与区间的第一位交换,让其成为基准值。
- 这个方法是为了防止数组已经有序,出现最坏的情况。
int GetMidIndex(int* a, int left, int right)
{
int midi = (left + right) / 2;
if (a[left] < a[midi])
{
if (a[midi] < a[right])
{
return midi;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else//a[left] > a[midi]
{
if (a[right] > a[left])
{
return left;
}
else if (a[midi] > a[right])
{
return midi;
}
else
{
return right;
}
}
}
void QuickSort(int* a, int begin,int end)//end为下标
{
if (begin >= end)//只有一个的时候或不存在的区间
{
return;
}
int midi = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[midi]);
int keyi= PartSort1(a, begin, end);
//[0,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);//左区间
QuickSort(a, keyi + 1,end);//右区间
}
- 通过if else语句便能完成三数取中的逻辑。
- 在区间划分前加上三数取中的操作即可。
- 注意要交换的是当前区间最左侧的值与中位数,即begin位与midi位。
小区间优化:
快排是通过递归划分区间完成排序的。当区间元素个数较多时,递归可以快速划分,但是当区间元素个数不多时,直接调用别的排序要比继续递归划分更为快速。
无小区间优化
有小区间优化
- 当区间个数小于20个时直接使用希尔排序
void QuickSort(int* a, int begin,int end)//end为下标
{
if (begin >= end)//只有一个的时候或不存在的区间
{
return;
}
//小区间优化
if ((end - begin + 1) < 20)
{
ShellSort(a + begin, end - begin + 1);
return;
}
//三数取中
int midi = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[midi]);
int keyi= PartSort1(a, begin, end);
//[0,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);//左区间
QuickSort(a, keyi + 1,end);//右区间
}
- 当然这种做法只是锦上添花,并不会使快排有本质的提升。
归并排序
思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序核心步骤:
- 归并排序要好好观看动图,实际排序就是按动图这样排序的。
void _MergeSort(int* a, int begin, int end, int* tmp)//结合动图看会更加好理解
{
if (begin == end)//与快排的递归条件有些不同,这里不会存在大于的区间
{
return;
}
//小区间优化
if ((end - begin + 1) < 20)
{
ShellSort(a + begin, end - begin + 1);
return;
}
//二分,不断缩小区间,也类似二叉树,采用递归
int mid = (begin + end) / 2;
//[0 mid] [mid+1 end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid+1, end, tmp);
//已经不能分了,开始合并
//类似二叉树,有“左右子树”
int begin1 = begin, end1 = mid;
int begin2 = mid+1, end2 = end;
int i = begin;//**一直划分到不可分,所以并不是从最开始的位置归并
while (begin1 <= end1 && begin2 <= end2)//放入tmp数组,只会放完一组
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//将剩余组的放进tmp
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//拷回原数组,注意元素位置和元素个数
memcpy(a + begin, tmp + begin, sizeof(int)*(end-begin+1));
}
void MergeSort(int* a,int n)//n为个数
{
//n -= 1;//转为下标
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc failed");
return 0;
}
//需要一个额外的数组来存放归并的元素,所以多写一个子程序来实现,否则递归调用时频繁开辟新的数组
_MergeSort(a, 0, n-1 , tmp);//传下标
free(tmp);
}
实现:
准备工作:
归并排序在排序时需要一个额外的数组来存放排好序的元素,所以先开辟一个额外是数组tmp,并在子函数_MergeSort进行递归操作。
实现过程:
在子函数_MergeSort进行递归,通过将区间一分为二的方式进行区间划分到只有一个元素时停止递归 (在一开始实现时,小区间优化不要加上,先理解归并排序是如何进行的),开始排序;排序时要清楚当前区间的范围,通过begin1,end1,begin2,end2来标识当前区间,因为归并排序是两组归并成一组的,所以要标识这两个区间,才能进行排序。通过循环比较这两个区间,最终会将这两个区间元素有序放入tmp数组。最后将tmp数组元素拷贝回原本数组a:这里是归并好两个区间就拷贝回a数组,所以要注意拷贝的起始位置和要拷贝的个数(memcpy(a + begin, tmp + begin, sizeof(int)*(end-begin+1)))。
归并排序也是二叉树结构思想,其时间复杂度为:O(N*logN)。
- 可以看到归并与快排都是一个级别的,都是O(N*logN)
计数排序
思想:
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤如下:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
- 计数排序不通过比较来进行排序:其通过相对映射来确定元素的大小关系,再结合统计的元素个数来完成排序。
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
for (int i = 0; i < n; i++)//找出两个最值
{
if (a[i] < min)
{
min = a[i];
}
if (a[i] > max)
{
max = a[i];
}
}
//开辟对应范围的数组
int range = max - min + 1;//最值差加一为元素个数
//int* countA = (int*)malloc(sizeof(int) * range);
//memset(countA, 0, sizeof(int) * range);
int* countA = (int*)calloc(range, sizeof(int));//开辟对应的元素个数数组
//统计个数
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;//相对映射定位
}
//排序
int i = 0;
for (int j = 0; i < range; j++)
{
while (countA[j]--)//对应位置个数
{
a[i++] = j + min;//对应位置加min变回原来值
}
}
free(countA);
}
实现:
1.求范围:要统计出现元素的次数,首先需要知道数据的范围,所以先通过一次循环遍历找出最大最小值,这样就可以知道数据的范围了。
2.开辟统计次数的数组CountA:知道范围后便能开辟出对应个数的数组CountA(该数组用来记录数组a对应位置出现元素的个数)。
3.在CountA数组统计a数组中元素出现的次数:开辟好用来统计元素出现次数的数组CountA后,便开始统计a数组中每个元素出现的次数,通过相对映射(a数组中的元素减去最小值min后的值便是CountA数组的下标)的原则在CountA数组对应位置统计出现的次数。
4.排序:最后就是按照CountA数组中对应位置的值按相对映射原则加回min的值放回a,完成排序。
计数排序只能用于整型数据的排序,浮点型数据不能用计数排序,原因是不能使用相对映射的原则。而且对数据也有一定要求,适用于较为集中的,不是很分散的数据(要开辟数组CountA)。
但是它的时间复杂度是O(1)!
O ( 1 ) O(1) O(1)对 O ( l o g N ) O(logN) O(logN)又是一波降维打击,在效率上让快排也黯然失色。但由于其有一定限制,所以在排序算法中快排还是综合性较好的排序算法。
篇幅较长,难免有错误地方,欢迎佬们指出。